/* * For work developed by the HSQL Development Group: * * Copyright (c) 2001-2010, The HSQL Development Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the HSQL Development Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * * * For work originally developed by the Hypersonic SQL Group: * * Copyright (c) 1995-2000, The Hypersonic SQL Group. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the Hypersonic SQL Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * on behalf of the Hypersonic SQL Group. */ package org.hsqldb.persist; import java.io.File; import java.io.IOException; import org.hsqldb.Database; import org.hsqldb.DatabaseURL; import org.hsqldb.HsqlException; import org.hsqldb.NumberSequence; import org.hsqldb.Session; import org.hsqldb.Table; import org.hsqldb.error.Error; import org.hsqldb.error.ErrorCode; import org.hsqldb.lib.FileAccess; import org.hsqldb.lib.FileArchiver; import org.hsqldb.lib.HashMap; import org.hsqldb.lib.Iterator; import org.hsqldb.scriptio.ScriptReaderBase; import org.hsqldb.scriptio.ScriptReaderDecode; import org.hsqldb.scriptio.ScriptReaderText; import org.hsqldb.scriptio.ScriptWriterBase; import org.hsqldb.scriptio.ScriptWriterEncode; import org.hsqldb.scriptio.ScriptWriterText; // fredt@users 20020215 - patch 1.7.0 by fredt // to move operations on the database.properties files to new // class HsqlDatabaseProperties // fredt@users 20020220 - patch 488200 by xclayl@users - throw exception // throw addded to all methods relying on file io // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP) // fredt@users 20020405 - patch 1.7.0 by fredt - no change in db location // because important information about the database is now stored in the // *.properties file, all database files should be in the same folder as the // *.properties file // tony_lai@users 20020820 - export hsqldb.log_size to .properties file // tony_lai@users 20020820 - changes to shutdown compact to save memory // fredt@users 20020910 - patch 1.7.1 by Nitin Chauhan - code improvements // fredt@users 20021208 - ongoing revamp // fredt@users 20021212 - do not rewrite the *.backup file if the *.data // file has not been updated in the current session. // boucherb@users 20030510 - patch 1.7.2 consolidated all periodic database // tasks in one timed task queue // fredt@users - 20050102 patch 1.8.0 - refactoring and clearer separation of concerns /* - if props.modified, use .backup file - .data file is ready - read .script file and set index roots - if .data file is modified, use .backup with .data file flag for increment backup - .data file is ready */ /** * This class is responsible for managing the database files. An HSQLDB database * consists of a .properties file, a .script file (contains an SQL script), * a .data file (contains data of cached tables) a .backup file * and a .log file.<p> * When using TEXT tables, a data source for each table is also present.<p> * * Notes on OpenOffice.org integration. * * A Storage API is used when HSQLDB is integrated into OpenOffice.org. All * file operations on the 4 main files are performed by OOo, which integrates * the contents of these files into its database file. The script format is * always TEXT in this case. * * Extensively rewritten and extended in successive versions of HSQLDB. * * @author Thomas Mueller (Hypersonic SQL Group) * @author Fred Toussi (fredt@users dot sourceforge.net) * @version 1.9.0 * @since Hypersonic SQL */ public class Log { private HsqlDatabaseProperties properties; private String fileName; private Database database; private FileAccess fa; ScriptWriterBase dbLogWriter; private String scriptFileName; private String logFileName; private boolean filesReadOnly; private long maxLogSize; private int writeDelay; private int scriptFormat; private DataFileCache cache; // We are using persist.Logger-instance-specific FrameworkLogger // because it is Database-instance specific. // If add any static level logging, should instantiate a standard, // context-agnostic FrameworkLogger for that purpose. Log(Database db) { database = db; fa = db.logger.getFileAccess(); fileName = db.getPath(); properties = db.getProperties(); } void initParams() { // Allows the user to set log size in the properties file. maxLogSize = database.logger.propLogSize * 1024L * 1024; scriptFormat = 0; writeDelay = database.logger.propWriteDelay; filesReadOnly = database.isFilesReadOnly(); scriptFileName = fileName + ".script"; logFileName = fileName + ".log"; } /** * When opening a database, the hsqldb.compatible_version property is * used to determine if this version of the engine is equal to or greater * than the earliest version of the engine capable of opening that * database.<p> * * @throws HsqlException */ void open() { initParams(); int state = properties.getDBModified(); switch (state) { case HsqlDatabaseProperties.FILES_MODIFIED : deleteNewAndOldFiles(); processScript(); processLog(); close(false); if (cache != null) { cache.open(filesReadOnly); } reopenAllTextCaches(); break; case HsqlDatabaseProperties.FILES_NEW : renameNewDataFile(); renameNewBackup(); renameNewScript(); deleteLog(); properties.setDBModified( HsqlDatabaseProperties.FILES_NOT_MODIFIED); // continue as non-modified files // fall through case HsqlDatabaseProperties.FILES_NOT_MODIFIED : /** * if startup is after a SHUTDOWN SCRIPT and there are CACHED * or TEXT tables, perform a checkpoint so that the .script * file no longer contains CACHED or TEXT table rows. */ processScript(); if (isAnyCacheModified()) { properties.setDBModified( HsqlDatabaseProperties.FILES_MODIFIED); close(false); if (cache != null) { cache.open(filesReadOnly); } reopenAllTextCaches(); } break; } openLog(); if (!filesReadOnly) { properties.setDBModified(HsqlDatabaseProperties.FILES_MODIFIED); } } /** * Close all the database files. If script argument is true, no .data * or .backup file will remain and the .script file will contain all the * data of the cached tables as well as memory tables. * * This is not used for filesReadOnly databases which use shutdown. */ void close(boolean script) { closeLog(); deleteNewAndOldFiles(); writeScript(script); closeAllTextCaches(script); if (cache != null) { cache.close(true); } // set this one last to save the props properties.setDBModified(HsqlDatabaseProperties.FILES_NEW); deleteLog(); if (cache != null) { if (script) { cache.deleteFile(); cache.deleteBackup(); } else { cache.backupFile(); cache.renameBackupFile(); } } renameNewScript(); properties.setDBModified(HsqlDatabaseProperties.FILES_NOT_MODIFIED); } /** * Fast counterpart to close(). Does not perform a checkpoint or a backup * of the .data file. */ void shutdown() { synchLog(); if (cache != null) { cache.close(false); } closeAllTextCaches(false); closeLog(); } /** * Deletes the leftovers from any previous unfinished operations. */ void deleteNewAndOldFiles() { fa.removeElement(fileName + ".data" + ".old"); fa.removeElement(fileName + ".data" + ".new"); fa.removeElement(fileName + ".backup" + ".new"); fa.removeElement(scriptFileName + ".new"); } void deleteBackup() { fa.removeElement(fileName + ".backup"); } void deleteData() { fa.removeElement(fileName + ".data"); } void backupData() throws IOException { if (database.logger.propIncrementBackup) { fa.removeElement(fileName + ".backup"); return; } if (fa.isStreamElement(fileName + ".data")) { FileArchiver.archive(fileName + ".data", fileName + ".backup.new", database.logger.getFileAccess(), FileArchiver.COMPRESSION_ZIP); } } void renameNewDataFile() { if (fa.isStreamElement(fileName + ".data.new")) { fa.renameElement(fileName + ".data.new", fileName + ".data"); } } void renameNewBackup() { // required for inc backup fa.removeElement(fileName + ".backup"); if (fa.isStreamElement(fileName + ".backup.new")) { fa.renameElement(fileName + ".backup.new", fileName + ".backup"); } } void renameNewScript() { if (fa.isStreamElement(scriptFileName + ".new")) { fa.renameElement(scriptFileName + ".new", scriptFileName); } } void deleteNewScript() { fa.removeElement(scriptFileName + ".new"); } void deleteNewBackup() { fa.removeElement(fileName + ".backup.new"); } void deleteLog() { fa.removeElement(logFileName); } /** * Checks all the caches and returns true if the modified flag is set for any */ boolean isAnyCacheModified() { if (cache != null && cache.isFileModified()) { return true; } return isAnyTextCacheModified(); } /** * Performs checkpoint including pre and post operations. Returns to the * same state as before the checkpoint. */ void checkpoint(boolean defrag) { if (filesReadOnly) { return; } if (cache == null) { defrag = false; } else if (forceDefrag()) { defrag = true; } if (defrag) { try { defrag(); return; } catch (Exception e) { database.logger.checkpointDisabled = true; return; } } boolean result = checkpointClose(); if (result) { checkpointReopen(); } } /** * Performs checkpoint including pre and post operations. Returns to the * same state as before the checkpoint. */ boolean checkpointClose() { if (filesReadOnly) { return true; } deleteNewAndOldFiles(); try { writeScript(false); } catch (HsqlException e) { deleteNewScript(); return false; } try { if (cache != null) { cache.close(true); cache.backupFile(); } } catch (Exception ee) { // backup failed perhaps due to lack of disk space deleteNewScript(); deleteNewBackup(); try { if (!cache.isFileOpen()) { cache.open(false); } } catch (Exception e1) {} return false; } properties.setDBModified(HsqlDatabaseProperties.FILES_NEW); closeLog(); deleteLog(); renameNewScript(); renameNewBackup(); try { properties.setDBModified( HsqlDatabaseProperties.FILES_NOT_MODIFIED); } catch (Exception e) {} return true; } boolean checkpointReopen() { if (filesReadOnly) { return true; } try { if (cache != null) { cache.open(false); } if (dbLogWriter != null) { openLog(); } properties.setDBModified(HsqlDatabaseProperties.FILES_MODIFIED); } catch (Exception e) { return false; } return true; } /** * Writes out all the rows to a new file without fragmentation. */ public void defrag() { if (cache.fileFreePosition == cache.initialFreePos) { return; } database.logger.logInfoEvent("defrag start"); try { // test /* { Session session = database.getSessionManager().getSysSession(); HsqlArrayList allTables = database.schemaManager.getAllTables(); for (int i = 0, tSize = allTables.size(); i < tSize; i++) { Table t = (Table) allTables.get(i); int count = 0; if (t.getTableType() == TableBase.CACHED_TABLE) { RowIterator it = t.rowIterator(session); for (; it.hasNext(); count++) { CachedObject row = it.getNextRow(); } System.out.println("table " + t.getName().name + " " + count); } } } */ // DataFileDefrag dfd = cache.defrag(); } catch (HsqlException e) { database.logger.logSevereEvent("defrag failure", e); throw (HsqlException) e; } catch (Throwable e) { database.logger.logSevereEvent("defrag failure", e); throw Error.error(ErrorCode.DATA_FILE_ERROR, e); } // test /* { Session session = database.getSessionManager().getSysSession(); HsqlArrayList allTables = database.schemaManager.getAllTables(); for (int i = 0, tSize = allTables.size(); i < tSize; i++) { Table t = (Table) allTables.get(i); int count = 0; if (t.getTableType() == Table.CACHED_TABLE) { RowIterator it = t.rowIterator(session); for (; it.hasNext(); count++) { CachedObject row = it.getNextRow(); } System.out.println("table " + t.getName().name + " " + count); } } } */ // database.logger.logInfoEvent("defrag end"); } /** * Returns true if lost space is above the threshold percentage */ boolean forceDefrag() { long limit = database.logger.propCacheDefragLimit * cache.getFileFreePos() / 100; long lostSize = cache.freeBlocks.getLostBlocksSize(); return limit > 0 && lostSize > limit; } /** * */ boolean hasCache() { return cache != null; } /** * Responsible for creating the cache instance. */ DataFileCache getCache() { if (cache == null) { cache = new DataFileCache(database, fileName); cache.open(filesReadOnly); } return cache; } void setLogSize(int megas) { maxLogSize = megas * 1024L * 1024; } int getScriptType() { return scriptFormat; } /** * Write delay specifies the frequency of FileDescriptor.sync() calls. */ int getWriteDelay() { return writeDelay; } void setWriteDelay(int delay) { writeDelay = delay; if (dbLogWriter != null) { dbLogWriter.forceSync(); dbLogWriter.stop(); dbLogWriter.setWriteDelay(delay); dbLogWriter.start(); } } public void setIncrementBackup(boolean val) { if (cache != null) { cache.setIncrementBackup(val); cache.fileModified = true; } } /** * Various writeXXX() methods are used for logging statements. */ void writeStatement(Session session, String s) { try { dbLogWriter.writeLogStatement(session, s); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName); } if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) { database.logger.checkpointRequired = true; } } void writeInsertStatement(Session session, Table t, Object[] row) { try { dbLogWriter.writeInsertStatement(session, t, row); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName); } if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) { database.logger.checkpointRequired = true; } } void writeDeleteStatement(Session session, Table t, Object[] row) { try { dbLogWriter.writeDeleteStatement(session, t, row); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName); } if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) { database.logger.checkpointRequired = true; } } void writeSequenceStatement(Session session, NumberSequence s) { try { dbLogWriter.writeSequenceStatement(session, s); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName); } if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) { database.logger.checkpointRequired = true; } } void writeCommitStatement(Session session) { try { dbLogWriter.writeCommitStatement(session); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName); } if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize) { database.logger.checkpointRequired = true; } } void synchLog() { if (dbLogWriter != null) { dbLogWriter.sync(); } } /** * Wrappers for openning-starting / stoping-closing the log file and * writer. */ void openLog() { if (filesReadOnly) { return; } Crypto crypto = database.logger.getCrypto(); try { if (crypto == null) { dbLogWriter = new ScriptWriterText(database, logFileName, false, false, false); } else { dbLogWriter = new ScriptWriterEncode(database, logFileName, crypto); } dbLogWriter.setWriteDelay(writeDelay); dbLogWriter.start(); } catch (Exception e) { throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName); } } synchronized void closeLog() { if (dbLogWriter != null) { dbLogWriter.close(); } } /** * Write the .script file as .script.new. */ void writeScript(boolean full) { deleteNewScript(); ScriptWriterBase scw; Crypto crypto = database.logger.getCrypto(); if (crypto == null) { scw = new ScriptWriterText(database, scriptFileName + ".new", full, true, false); } else { scw = new ScriptWriterEncode(database, scriptFileName + ".new", full, crypto); } scw.writeAll(); scw.close(); } /** * Performs all the commands in the .script file. */ private void processScript() { ScriptReaderBase scr = null; try { if (fa.isStreamElement(scriptFileName)) { Crypto crypto = database.logger.getCrypto(); if (crypto == null) { scr = new ScriptReaderText(database, scriptFileName); } else { scr = new ScriptReaderDecode(database, scriptFileName, crypto); } Session session = database.sessionManager.getSysSessionForScript(database); scr.readAll(session); scr.close(); } } catch (Throwable e) { if (scr != null) { scr.close(); if (cache != null) { cache.close(false); } closeAllTextCaches(false); } database.logger.logWarningEvent("Script processing failure", e); if (e instanceof HsqlException) { throw (HsqlException) e; } else if (e instanceof IOException) { throw Error.error(ErrorCode.FILE_IO_ERROR, e); } else if (e instanceof OutOfMemoryError) { throw Error.error(ErrorCode.OUT_OF_MEMORY); } else { throw Error.error(ErrorCode.GENERAL_ERROR, e); } } } /** * Performs all the commands in the .log file. */ private void processLog() { if (fa.isStreamElement(logFileName)) { ScriptRunner.runScript(database, logFileName); } } // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP) - text tables private HashMap textCacheList = new HashMap(); DataFileCache openTextCache(Table table, String source, boolean readOnlyData, boolean reversed) { closeTextCache(table); if (database.getType() != DatabaseURL.S_RES && !properties.isPropertyTrue( HsqlDatabaseProperties.textdb_allow_full_path)) { if (source.indexOf("..") != -1) { throw (Error.error(ErrorCode.ACCESS_IS_DENIED, source)); } String path = new File( new File( database.getPath() + ".properties").getAbsolutePath()).getParent(); if (path != null) { source = path + File.separator + source; } } TextCache c = new TextCache(table, source); c.open(readOnlyData || filesReadOnly); textCacheList.put(table.getName(), c); return c; } void closeTextCache(Table table) { TextCache c = (TextCache) textCacheList.remove(table.getName()); if (c != null) { try { c.close(true); } catch (HsqlException e) {} } } private void closeAllTextCaches(boolean script) { Iterator it = textCacheList.values().iterator(); while (it.hasNext()) { TextCache textCache = ((TextCache) it.next()); // use textCache.table to cover both cach and table readonly if (script && !textCache.table.isDataReadOnly()) { textCache.purge(); } else { textCache.close(true); } } } private void reopenAllTextCaches() { Iterator it = textCacheList.values().iterator(); while (it.hasNext()) { ((TextCache) it.next()).reopen(); } } private boolean isAnyTextCacheModified() { Iterator it = textCacheList.values().iterator(); while (it.hasNext()) { if (((TextCache) it.next()).isFileModified()) { return true; } } return false; } }