1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.mvstore.db; |
7 | |
8 | import java.io.InputStream; |
9 | import java.lang.Thread.UncaughtExceptionHandler; |
10 | import java.nio.channels.FileChannel; |
11 | import java.util.ArrayList; |
12 | import java.util.HashMap; |
13 | import java.util.List; |
14 | import java.util.Map; |
15 | import java.util.concurrent.ConcurrentHashMap; |
16 | |
17 | import org.h2.api.ErrorCode; |
18 | import org.h2.api.TableEngine; |
19 | import org.h2.command.ddl.CreateTableData; |
20 | import org.h2.engine.Constants; |
21 | import org.h2.engine.Database; |
22 | import org.h2.engine.Session; |
23 | import org.h2.message.DbException; |
24 | import org.h2.mvstore.DataUtils; |
25 | import org.h2.mvstore.FileStore; |
26 | import org.h2.mvstore.MVMap; |
27 | import org.h2.mvstore.MVStore; |
28 | import org.h2.mvstore.MVStoreTool; |
29 | import org.h2.mvstore.db.TransactionStore.Transaction; |
30 | import org.h2.mvstore.db.TransactionStore.TransactionMap; |
31 | import org.h2.store.InDoubtTransaction; |
32 | import org.h2.store.fs.FileChannelInputStream; |
33 | import org.h2.store.fs.FileUtils; |
34 | import org.h2.table.TableBase; |
35 | import org.h2.util.BitField; |
36 | import org.h2.util.New; |
37 | |
38 | /** |
39 | * A table engine that internally uses the MVStore. |
40 | */ |
41 | public class MVTableEngine implements TableEngine { |
42 | |
43 | /** |
44 | * Initialize the MVStore. |
45 | * |
46 | * @param db the database |
47 | * @return the store |
48 | */ |
49 | public static Store init(final Database db) { |
50 | Store store = db.getMvStore(); |
51 | if (store != null) { |
52 | return store; |
53 | } |
54 | byte[] key = db.getFileEncryptionKey(); |
55 | String dbPath = db.getDatabasePath(); |
56 | MVStore.Builder builder = new MVStore.Builder(); |
57 | if (dbPath == null) { |
58 | store = new Store(db, builder); |
59 | } else { |
60 | String fileName = dbPath + Constants.SUFFIX_MV_FILE; |
61 | MVStoreTool.compactCleanUp(fileName); |
62 | builder.fileName(fileName); |
63 | builder.pageSplitSize(db.getPageSize()); |
64 | if (db.isReadOnly()) { |
65 | builder.readOnly(); |
66 | } else { |
67 | // possibly create the directory |
68 | boolean exists = FileUtils.exists(fileName); |
69 | if (exists && !FileUtils.canWrite(fileName)) { |
70 | // read only |
71 | } else { |
72 | String dir = FileUtils.getParent(fileName); |
73 | FileUtils.createDirectories(dir); |
74 | } |
75 | } |
76 | if (key != null) { |
77 | char[] password = new char[key.length / 2]; |
78 | for (int i = 0; i < password.length; i++) { |
79 | password[i] = (char) (((key[i + i] & 255) << 16) | |
80 | ((key[i + i + 1]) & 255)); |
81 | } |
82 | builder.encryptionKey(password); |
83 | } |
84 | if (db.getSettings().compressData) { |
85 | builder.compress(); |
86 | // use a larger page split size to improve the compression ratio |
87 | builder.pageSplitSize(64 * 1024); |
88 | } |
89 | builder.backgroundExceptionHandler(new UncaughtExceptionHandler() { |
90 | |
91 | @Override |
92 | public void uncaughtException(Thread t, Throwable e) { |
93 | db.setBackgroundException(DbException.convert(e)); |
94 | } |
95 | |
96 | }); |
97 | try { |
98 | store = new Store(db, builder); |
99 | } catch (IllegalStateException e) { |
100 | int errorCode = DataUtils.getErrorCode(e.getMessage()); |
101 | if (errorCode == DataUtils.ERROR_FILE_CORRUPT) { |
102 | if (key != null) { |
103 | throw DbException.get( |
104 | ErrorCode.FILE_ENCRYPTION_ERROR_1, |
105 | e, fileName); |
106 | } |
107 | } else if (errorCode == DataUtils.ERROR_FILE_LOCKED) { |
108 | throw DbException.get( |
109 | ErrorCode.DATABASE_ALREADY_OPEN_1, |
110 | e, fileName); |
111 | } else if (errorCode == DataUtils.ERROR_READING_FAILED) { |
112 | throw DbException.get( |
113 | ErrorCode.IO_EXCEPTION_1, |
114 | e, fileName); |
115 | } |
116 | throw DbException.get( |
117 | ErrorCode.FILE_CORRUPTED_1, |
118 | e, fileName); |
119 | } |
120 | } |
121 | db.setMvStore(store); |
122 | return store; |
123 | } |
124 | |
125 | @Override |
126 | public TableBase createTable(CreateTableData data) { |
127 | Database db = data.session.getDatabase(); |
128 | Store store = init(db); |
129 | MVTable table = new MVTable(data, store); |
130 | table.init(data.session); |
131 | store.tableMap.put(table.getMapName(), table); |
132 | return table; |
133 | } |
134 | |
135 | /** |
136 | * A store with open tables. |
137 | */ |
138 | public static class Store { |
139 | |
140 | /** |
141 | * The map of open tables. |
142 | * Key: the map name, value: the table. |
143 | */ |
144 | final ConcurrentHashMap<String, MVTable> tableMap = |
145 | new ConcurrentHashMap<String, MVTable>(); |
146 | |
147 | /** |
148 | * The store. |
149 | */ |
150 | private final MVStore store; |
151 | |
152 | /** |
153 | * The transaction store. |
154 | */ |
155 | private final TransactionStore transactionStore; |
156 | |
157 | private long statisticsStart; |
158 | |
159 | private int temporaryMapId; |
160 | |
161 | public Store(Database db, MVStore.Builder builder) { |
162 | this.store = builder.open(); |
163 | if (!db.getSettings().reuseSpace) { |
164 | store.setReuseSpace(false); |
165 | } |
166 | this.transactionStore = new TransactionStore( |
167 | store, |
168 | new ValueDataType(null, db, null)); |
169 | transactionStore.init(); |
170 | } |
171 | |
172 | public MVStore getStore() { |
173 | return store; |
174 | } |
175 | |
176 | public TransactionStore getTransactionStore() { |
177 | return transactionStore; |
178 | } |
179 | |
180 | public HashMap<String, MVTable> getTables() { |
181 | return new HashMap<String, MVTable>(tableMap); |
182 | } |
183 | |
184 | /** |
185 | * Remove a table. |
186 | * |
187 | * @param table the table |
188 | */ |
189 | public void removeTable(MVTable table) { |
190 | tableMap.remove(table.getMapName()); |
191 | } |
192 | |
193 | /** |
194 | * Store all pending changes. |
195 | */ |
196 | public void flush() { |
197 | FileStore s = store.getFileStore(); |
198 | if (s == null || s.isReadOnly()) { |
199 | return; |
200 | } |
201 | if (!store.compact(50, 4 * 1024 * 1024)) { |
202 | store.commit(); |
203 | } |
204 | } |
205 | |
206 | /** |
207 | * Close the store, without persisting changes. |
208 | */ |
209 | public void closeImmediately() { |
210 | if (store.isClosed()) { |
211 | return; |
212 | } |
213 | store.closeImmediately(); |
214 | } |
215 | |
216 | /** |
217 | * Commit all transactions that are in the committing state, and |
218 | * rollback all open transactions. |
219 | */ |
220 | public void initTransactions() { |
221 | List<Transaction> list = transactionStore.getOpenTransactions(); |
222 | for (Transaction t : list) { |
223 | if (t.getStatus() == Transaction.STATUS_COMMITTING) { |
224 | t.commit(); |
225 | } else if (t.getStatus() != Transaction.STATUS_PREPARED) { |
226 | t.rollback(); |
227 | } |
228 | } |
229 | } |
230 | |
231 | /** |
232 | * Remove all temporary maps. |
233 | * |
234 | * @param objectIds the ids of the objects to keep |
235 | */ |
236 | public void removeTemporaryMaps(BitField objectIds) { |
237 | for (String mapName : store.getMapNames()) { |
238 | if (mapName.startsWith("temp.")) { |
239 | MVMap<?, ?> map = store.openMap(mapName); |
240 | store.removeMap(map); |
241 | } else if (mapName.startsWith("table.") || mapName.startsWith("index.")) { |
242 | int id = Integer.parseInt(mapName.substring(1 + mapName.indexOf("."))); |
243 | if (!objectIds.get(id)) { |
244 | ValueDataType keyType = new ValueDataType(null, null, null); |
245 | ValueDataType valueType = new ValueDataType(null, null, null); |
246 | Transaction t = transactionStore.begin(); |
247 | TransactionMap<?, ?> m = t.openMap(mapName, keyType, valueType); |
248 | transactionStore.removeMap(m); |
249 | t.commit(); |
250 | } |
251 | } |
252 | } |
253 | } |
254 | |
255 | /** |
256 | * Get the name of the next available temporary map. |
257 | * |
258 | * @return the map name |
259 | */ |
260 | public synchronized String nextTemporaryMapName() { |
261 | return "temp." + temporaryMapId++; |
262 | } |
263 | |
264 | /** |
265 | * Prepare a transaction. |
266 | * |
267 | * @param session the session |
268 | * @param transactionName the transaction name (may be null) |
269 | */ |
270 | public void prepareCommit(Session session, String transactionName) { |
271 | Transaction t = session.getTransaction(); |
272 | t.setName(transactionName); |
273 | t.prepare(); |
274 | store.commit(); |
275 | } |
276 | |
277 | public ArrayList<InDoubtTransaction> getInDoubtTransactions() { |
278 | List<Transaction> list = transactionStore.getOpenTransactions(); |
279 | ArrayList<InDoubtTransaction> result = New.arrayList(); |
280 | for (Transaction t : list) { |
281 | if (t.getStatus() == Transaction.STATUS_PREPARED) { |
282 | result.add(new MVInDoubtTransaction(store, t)); |
283 | } |
284 | } |
285 | return result; |
286 | } |
287 | |
288 | public void setCacheSize(int kb) { |
289 | store.setCacheSize(Math.max(1, kb / 1024)); |
290 | } |
291 | |
292 | public InputStream getInputStream() { |
293 | FileChannel fc = store.getFileStore().getEncryptedFile(); |
294 | if (fc == null) { |
295 | fc = store.getFileStore().getFile(); |
296 | } |
297 | return new FileChannelInputStream(fc, false); |
298 | } |
299 | |
300 | /** |
301 | * Force the changes to disk. |
302 | */ |
303 | public void sync() { |
304 | flush(); |
305 | store.sync(); |
306 | } |
307 | |
308 | /** |
309 | * Compact the database file, that is, compact blocks that have a low |
310 | * fill rate, and move chunks next to each other. This will typically |
311 | * shrink the database file. Changes are flushed to the file, and old |
312 | * chunks are overwritten. |
313 | * |
314 | * @param maxCompactTime the maximum time in milliseconds to compact |
315 | */ |
316 | public void compactFile(long maxCompactTime) { |
317 | store.setRetentionTime(0); |
318 | long start = System.currentTimeMillis(); |
319 | while (store.compact(95, 16 * 1024 * 1024)) { |
320 | store.sync(); |
321 | store.compactMoveChunks(95, 16 * 1024 * 1024); |
322 | long time = System.currentTimeMillis() - start; |
323 | if (time > maxCompactTime) { |
324 | break; |
325 | } |
326 | } |
327 | } |
328 | |
329 | /** |
330 | * Close the store. Pending changes are persisted. Chunks with a low |
331 | * fill rate are compacted, but old chunks are kept for some time, so |
332 | * most likely the database file will not shrink. |
333 | * |
334 | * @param maxCompactTime the maximum time in milliseconds to compact |
335 | */ |
336 | public void close(long maxCompactTime) { |
337 | try { |
338 | if (!store.isClosed() && store.getFileStore() != null) { |
339 | boolean compactFully = false; |
340 | if (!store.getFileStore().isReadOnly()) { |
341 | transactionStore.close(); |
342 | if (maxCompactTime == Long.MAX_VALUE) { |
343 | compactFully = true; |
344 | } |
345 | } |
346 | String fileName = store.getFileStore().getFileName(); |
347 | store.close(); |
348 | if (compactFully && FileUtils.exists(fileName)) { |
349 | // the file could have been deleted concurrently, |
350 | // so only compact if the file still exists |
351 | MVStoreTool.compact(fileName, true); |
352 | } |
353 | } |
354 | } catch (IllegalStateException e) { |
355 | int errorCode = DataUtils.getErrorCode(e.getMessage()); |
356 | if (errorCode == DataUtils.ERROR_WRITING_FAILED) { |
357 | // disk full - ok |
358 | } else if (errorCode == DataUtils.ERROR_FILE_CORRUPT) { |
359 | // wrong encryption key - ok |
360 | } |
361 | store.closeImmediately(); |
362 | throw DbException.get(ErrorCode.IO_EXCEPTION_1, e, "Closing"); |
363 | } |
364 | } |
365 | |
366 | /** |
367 | * Start collecting statistics. |
368 | */ |
369 | public void statisticsStart() { |
370 | FileStore fs = store.getFileStore(); |
371 | statisticsStart = fs == null ? 0 : fs.getReadCount(); |
372 | } |
373 | |
374 | /** |
375 | * Stop collecting statistics. |
376 | * |
377 | * @return the statistics |
378 | */ |
379 | public Map<String, Integer> statisticsEnd() { |
380 | HashMap<String, Integer> map = New.hashMap(); |
381 | FileStore fs = store.getFileStore(); |
382 | int reads = fs == null ? 0 : (int) (fs.getReadCount() - statisticsStart); |
383 | map.put("reads", reads); |
384 | return map; |
385 | } |
386 | |
387 | } |
388 | |
389 | /** |
390 | * An in-doubt transaction. |
391 | */ |
392 | private static class MVInDoubtTransaction implements InDoubtTransaction { |
393 | |
394 | private final MVStore store; |
395 | private final Transaction transaction; |
396 | private int state = InDoubtTransaction.IN_DOUBT; |
397 | |
398 | MVInDoubtTransaction(MVStore store, Transaction transaction) { |
399 | this.store = store; |
400 | this.transaction = transaction; |
401 | } |
402 | |
403 | @Override |
404 | public void setState(int state) { |
405 | if (state == InDoubtTransaction.COMMIT) { |
406 | transaction.commit(); |
407 | } else { |
408 | transaction.rollback(); |
409 | } |
410 | store.commit(); |
411 | this.state = state; |
412 | } |
413 | |
414 | @Override |
415 | public String getState() { |
416 | switch(state) { |
417 | case IN_DOUBT: |
418 | return "IN_DOUBT"; |
419 | case COMMIT: |
420 | return "COMMIT"; |
421 | case ROLLBACK: |
422 | return "ROLLBACK"; |
423 | default: |
424 | throw DbException.throwInternalError("state="+state); |
425 | } |
426 | } |
427 | |
428 | @Override |
429 | public String getTransactionName() { |
430 | return transaction.getName(); |
431 | } |
432 | |
433 | } |
434 | |
435 | } |