001: /*
002: * Copyright 2004-2008 H2 Group. Licensed under the H2 License, Version 1.0
003: * (http://h2database.com/html/license.html).
004: * Initial Developer: H2 Group
005: */
006: package org.h2.log;
007:
008: import java.sql.SQLException;
009: import java.util.Comparator;
010: import java.util.HashMap;
011:
012: import org.h2.api.DatabaseEventListener;
013: import org.h2.engine.Constants;
014: import org.h2.engine.Database;
015: import org.h2.engine.Session;
016: import org.h2.message.Trace;
017: import org.h2.store.DataPage;
018: import org.h2.store.DiskFile;
019: import org.h2.store.Record;
020: import org.h2.store.Storage;
021: import org.h2.util.FileUtils;
022: import org.h2.util.ObjectArray;
023: import org.h2.util.ObjectUtils;
024:
025: /**
026: * The transaction log system is responsible for the write ahead log mechanism
027: * used in this database. A number of {@link LogFile} objects are used (one for
028: * each file).
029: */
030: public class LogSystem {
031:
032: public static final int LOG_WRITTEN = -1;
033:
034: private Database database;
035: private ObjectArray activeLogs;
036: private LogFile currentLog;
037: private String fileNamePrefix;
038: private HashMap storages = new HashMap();
039: private HashMap sessions = new HashMap();
040: private DataPage rowBuff;
041: private ObjectArray undo;
042: // TODO log file / deleteOldLogFilesAutomatically:
043: // make this a setting, so they can be backed up
044: private boolean deleteOldLogFilesAutomatically = true;
045: private long maxLogSize = Constants.DEFAULT_MAX_LOG_SIZE;
046: private boolean readOnly;
047: private boolean flushOnEachCommit;
048: private ObjectArray inDoubtTransactions;
049: private boolean disabled;
050: private int keepFiles;
051: private boolean closed;
052: private String accessMode;
053:
054: public LogSystem(Database database, String fileNamePrefix,
055: boolean readOnly, String accessMode) throws SQLException {
056: this .database = database;
057: this .readOnly = readOnly;
058: this .accessMode = accessMode;
059: closed = true;
060: if (database == null) {
061: return;
062: }
063: this .fileNamePrefix = fileNamePrefix;
064: rowBuff = DataPage.create(database,
065: Constants.DEFAULT_DATA_PAGE_SIZE);
066: }
067:
068: public void setMaxLogSize(long maxSize) {
069: this .maxLogSize = maxSize;
070: }
071:
072: public long getMaxLogSize() {
073: return maxLogSize;
074: }
075:
076: public boolean containsInDoubtTransactions() {
077: return inDoubtTransactions != null
078: && inDoubtTransactions.size() > 0;
079: }
080:
081: private void flushAndCloseUnused() throws SQLException {
082: currentLog.flush();
083: DiskFile file = database.getDataFile();
084: if (file == null) {
085: return;
086: }
087: file.flush();
088: if (containsInDoubtTransactions()) {
089: // if there are any in-doubt transactions
090: // (even if they are resolved), can't update or delete the log files
091: return;
092: }
093: Session[] sessions = database.getSessions();
094: int firstUncommittedLog = currentLog.getId();
095: int firstUncommittedPos = currentLog.getPos();
096: for (int i = 0; i < sessions.length; i++) {
097: Session session = sessions[i];
098: int log = session.getFirstUncommittedLog();
099: int pos = session.getFirstUncommittedPos();
100: if (pos != LOG_WRITTEN) {
101: if (log < firstUncommittedLog
102: || (log == firstUncommittedLog && pos < firstUncommittedPos)) {
103: firstUncommittedLog = log;
104: firstUncommittedPos = pos;
105: }
106: }
107: }
108: for (int i = activeLogs.size() - 1; i >= 0; i--) {
109: LogFile l = (LogFile) activeLogs.get(i);
110: if (l.getId() < firstUncommittedLog) {
111: l.setFirstUncommittedPos(LOG_WRITTEN);
112: } else if (l.getId() == firstUncommittedLog) {
113: if (firstUncommittedPos == l.getPos()) {
114: l.setFirstUncommittedPos(LOG_WRITTEN);
115: } else {
116: l.setFirstUncommittedPos(firstUncommittedPos);
117: }
118: }
119: }
120: for (int i = 0; i < activeLogs.size(); i++) {
121: LogFile l = (LogFile) activeLogs.get(i);
122: if (l.getFirstUncommittedPos() == LOG_WRITTEN) {
123: // must remove the log file first
124: // if we don't do that, the file is closed but still in the list
125: activeLogs.remove(i);
126: i--;
127: closeOldFile(l);
128: }
129: }
130: }
131:
132: public void close() throws SQLException {
133: if (database == null) {
134: return;
135: }
136: synchronized (database) {
137: if (closed) {
138: return;
139: }
140: if (readOnly) {
141: for (int i = 0; i < activeLogs.size(); i++) {
142: LogFile l = (LogFile) activeLogs.get(i);
143: l.close(false);
144: }
145: closed = true;
146: return;
147: }
148: // TODO refactor flushing and closing files when we know what to do exactly
149: SQLException closeException = null;
150: try {
151: flushAndCloseUnused();
152: if (!containsInDoubtTransactions()) {
153: checkpoint();
154: }
155: } catch (SQLException e) {
156: closeException = e;
157: }
158: for (int i = 0; i < activeLogs.size(); i++) {
159: LogFile l = (LogFile) activeLogs.get(i);
160: try {
161: // if there are any in-doubt transactions
162: // (even if they are resolved), can't delete the log files
163: if (l.getFirstUncommittedPos() == LOG_WRITTEN
164: && !containsInDoubtTransactions()) {
165: closeOldFile(l);
166: } else {
167: l.close(false);
168: }
169: } catch (SQLException e) {
170: // TODO log exception
171: if (closeException == null) {
172: closeException = e;
173: }
174: }
175: }
176: closed = true;
177: if (closeException != null) {
178: throw closeException;
179: }
180: }
181: }
182:
183: void addUndoLogRecord(LogFile log, int logRecordId, int sessionId) {
184: getOrAddSessionState(sessionId);
185: LogRecord record = new LogRecord(log, logRecordId, sessionId);
186: undo.add(record);
187: }
188:
189: public boolean recover() throws SQLException {
190: if (database == null) {
191: return false;
192: }
193: synchronized (database) {
194: if (closed) {
195: return false;
196: }
197: undo = new ObjectArray();
198: for (int i = 0; i < activeLogs.size(); i++) {
199: LogFile log = (LogFile) activeLogs.get(i);
200: log.redoAllGoEnd();
201: }
202: database.getDataFile().flushRedoLog();
203: database.getIndexFile().flushRedoLog();
204: int end = currentLog.getPos();
205: Object[] states = sessions.values().toArray();
206: inDoubtTransactions = new ObjectArray();
207: for (int i = 0; i < states.length; i++) {
208: SessionState state = (SessionState) states[i];
209: if (state.inDoubtTransaction != null) {
210: inDoubtTransactions.add(state.inDoubtTransaction);
211: }
212: }
213: for (int i = undo.size() - 1; i >= 0 && sessions.size() > 0; i--) {
214: database.setProgress(
215: DatabaseEventListener.STATE_RECOVER, null, undo
216: .size()
217: - 1 - i, undo.size());
218: LogRecord record = (LogRecord) undo.get(i);
219: if (sessions.get(ObjectUtils
220: .getInteger(record.sessionId)) != null) {
221: // undo only if the session is not yet committed
222: record.log.undo(record.logRecordId);
223: database.getDataFile().flushRedoLog();
224: database.getIndexFile().flushRedoLog();
225: }
226: }
227: currentLog.go(end);
228: boolean fileChanged = undo.size() > 0;
229: undo = null;
230: storages.clear();
231: if (!readOnly && fileChanged
232: && !containsInDoubtTransactions()) {
233: checkpoint();
234: }
235: return fileChanged;
236: }
237: }
238:
239: private void closeOldFile(LogFile l) throws SQLException {
240: l.close(deleteOldLogFilesAutomatically && keepFiles == 0);
241: }
242:
243: public void open() throws SQLException {
244: String path = FileUtils.getParent(fileNamePrefix);
245: String[] list = FileUtils.listFiles(path);
246: activeLogs = new ObjectArray();
247: for (int i = 0; i < list.length; i++) {
248: String s = list[i];
249: LogFile l = null;
250: try {
251: l = LogFile.openIfLogFile(this , fileNamePrefix, s);
252: } catch (SQLException e) {
253: database.getTrace(Trace.LOG).debug(
254: "Error opening log file, header corrupt: " + s,
255: e);
256: // this can happen if the system crashes just
257: // after creating a new file (before writing the header)
258: // rename it, so that it doesn't get in the way the next time
259: FileUtils.delete(s + ".corrupt");
260: FileUtils.rename(s, s + ".corrupt");
261: }
262: if (l != null) {
263: if (l.getPos() == LOG_WRITTEN) {
264: closeOldFile(l);
265: } else {
266: activeLogs.add(l);
267: }
268: }
269: }
270: activeLogs.sort(new Comparator() {
271: public int compare(Object a, Object b) {
272: return ((LogFile) a).getId() - ((LogFile) b).getId();
273: }
274: });
275: if (activeLogs.size() == 0) {
276: LogFile l = new LogFile(this , 0, fileNamePrefix);
277: activeLogs.add(l);
278: }
279: currentLog = (LogFile) activeLogs.get(activeLogs.size() - 1);
280: closed = false;
281: }
282:
283: Storage getStorageForRecovery(int id) throws SQLException {
284: boolean dataFile;
285: if (id < 0) {
286: dataFile = false;
287: id = -id;
288: } else {
289: dataFile = true;
290: }
291: Integer i = ObjectUtils.getInteger(id);
292: Storage storage = (Storage) storages.get(i);
293: if (storage == null) {
294: storage = database.getStorage(null, id, dataFile);
295: storages.put(i, storage);
296: }
297: return storage;
298: }
299:
300: boolean isSessionCommitted(int sessionId, int logId, int pos) {
301: Integer key = ObjectUtils.getInteger(sessionId);
302: SessionState state = (SessionState) sessions.get(key);
303: if (state == null) {
304: return true;
305: }
306: return state.isCommitted(logId, pos);
307: }
308:
309: void setLastCommitForSession(int sessionId, int logId, int pos) {
310: SessionState state = getOrAddSessionState(sessionId);
311: state.lastCommitLog = logId;
312: state.lastCommitPos = pos;
313: state.inDoubtTransaction = null;
314: }
315:
316: SessionState getOrAddSessionState(int sessionId) {
317: Integer key = ObjectUtils.getInteger(sessionId);
318: SessionState state = (SessionState) sessions.get(key);
319: if (state == null) {
320: state = new SessionState();
321: sessions.put(key, state);
322: state.sessionId = sessionId;
323: }
324: return state;
325: }
326:
327: void setPreparedCommitForSession(LogFile log, int sessionId,
328: int pos, String transaction, int blocks) {
329: SessionState state = getOrAddSessionState(sessionId);
330: // this is potentially a commit, so
331: // don't roll back the action before it (currently)
332: setLastCommitForSession(sessionId, log.getId(), pos);
333: state.inDoubtTransaction = new InDoubtTransaction(log,
334: sessionId, pos, transaction, blocks);
335: }
336:
337: public ObjectArray getInDoubtTransactions() {
338: return inDoubtTransactions;
339: }
340:
341: void removeSession(int sessionId) {
342: sessions.remove(ObjectUtils.getInteger(sessionId));
343: }
344:
345: public void prepareCommit(Session session, String transaction)
346: throws SQLException {
347: if (database == null || readOnly) {
348: return;
349: }
350: synchronized (database) {
351: if (closed) {
352: return;
353: }
354: currentLog.prepareCommit(session, transaction);
355: }
356: }
357:
358: public void commit(Session session) throws SQLException {
359: if (database == null || readOnly) {
360: return;
361: }
362: synchronized (database) {
363: if (closed) {
364: return;
365: }
366: currentLog.commit(session);
367: session.setAllCommitted();
368: }
369: }
370:
371: public void flush() throws SQLException {
372: if (database == null || readOnly) {
373: return;
374: }
375: synchronized (database) {
376: if (closed) {
377: return;
378: }
379: currentLog.flush();
380: }
381: }
382:
383: /**
384: * Add a truncate entry.
385: *
386: * @param session the session
387: * @param file the disk file
388: * @param storageId the storage id
389: * @param recordId the id of the first record
390: * @param blockCount the number of blocks
391: */
392: public void addTruncate(Session session, DiskFile file,
393: int storageId, int recordId, int blockCount)
394: throws SQLException {
395: if (database == null) {
396: return;
397: }
398: synchronized (database) {
399: if (disabled || closed) {
400: return;
401: }
402: database.checkWritingAllowed();
403: if (!file.isDataFile()) {
404: storageId = -storageId;
405: }
406: currentLog.addTruncate(session, storageId, recordId,
407: blockCount);
408: if (currentLog.getFileSize() > maxLogSize) {
409: checkpoint();
410: }
411: }
412: }
413:
414: public void add(Session session, DiskFile file, Record record)
415: throws SQLException {
416: if (database == null) {
417: return;
418: }
419: synchronized (database) {
420: if (disabled || closed) {
421: return;
422: }
423: database.checkWritingAllowed();
424: int storageId = record.getStorageId();
425: if (!file.isDataFile()) {
426: storageId = -storageId;
427: }
428: int log = currentLog.getId();
429: int pos = currentLog.getPos();
430: session.addLogPos(log, pos);
431: record.setLastLog(log, pos);
432: currentLog.add(session, storageId, record);
433: if (currentLog.getFileSize() > maxLogSize) {
434: checkpoint();
435: }
436: }
437: }
438:
439: public void checkpoint() throws SQLException {
440: if (readOnly || database == null) {
441: return;
442: }
443: synchronized (database) {
444: if (closed || disabled) {
445: return;
446: }
447: flushAndCloseUnused();
448: currentLog = new LogFile(this , currentLog.getId() + 1,
449: fileNamePrefix);
450: activeLogs.add(currentLog);
451: writeSummary();
452: currentLog.flush();
453: }
454: }
455:
456: public ObjectArray getActiveLogFiles() {
457: synchronized (database) {
458: ObjectArray list = new ObjectArray();
459: list.addAll(activeLogs);
460: return list;
461: }
462: }
463:
464: private void writeSummary() throws SQLException {
465: byte[] summary;
466: DiskFile file;
467: file = database.getDataFile();
468: if (file == null) {
469: return;
470: }
471: summary = file.getSummary();
472: if (summary != null) {
473: currentLog.addSummary(true, summary);
474: }
475: if (database.getLogIndexChanges()
476: || database.getIndexSummaryValid()) {
477: file = database.getIndexFile();
478: summary = file.getSummary();
479: if (summary != null) {
480: currentLog.addSummary(false, summary);
481: }
482: } else {
483: // invalidate the index summary
484: currentLog.addSummary(false, null);
485: }
486: }
487:
488: Database getDatabase() {
489: return database;
490: }
491:
492: DataPage getRowBuffer() {
493: return rowBuff;
494: }
495:
496: public void setFlushOnEachCommit(boolean b) {
497: flushOnEachCommit = b;
498: }
499:
500: boolean getFlushOnEachCommit() {
501: return flushOnEachCommit;
502: }
503:
504: public void sync() throws SQLException {
505: if (database == null || readOnly) {
506: return;
507: }
508: synchronized (database) {
509: if (currentLog != null) {
510: currentLog.flush();
511: currentLog.sync();
512: }
513: }
514: }
515:
516: public void setDisabled(boolean disabled) {
517: this .disabled = disabled;
518: }
519:
520: void addRedoLog(Storage storage, int recordId, int blockCount,
521: DataPage rec) throws SQLException {
522: DiskFile file = storage.getDiskFile();
523: file.addRedoLog(storage, recordId, blockCount, rec);
524: }
525:
526: public void invalidateIndexSummary() throws SQLException {
527: currentLog.addSummary(false, null);
528: }
529:
530: public synchronized void updateKeepFiles(int incrementDecrement) {
531: keepFiles += incrementDecrement;
532: }
533:
534: String getAccessMode() {
535: return accessMode;
536: }
537:
538: }
|