/* Copyright (c) 2001-2009, 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. */ package org.hsqldb.persist; import java.io.IOException; import java.io.File; import java.text.SimpleDateFormat; import org.hsqldb.Database; import org.hsqldb.NumberSequence; import org.hsqldb.Session; import org.hsqldb.Table; import org.hsqldb.TableBase; import org.hsqldb.lib.SimpleLog; import org.hsqldb.lib.tar.DbBackup; import org.hsqldb.Error; import org.hsqldb.ErrorCode; import org.hsqldb.lib.tar.TarMalformatException; // boucherb@users 20030510 - patch 1.7.2 - added cooperative file locking /** * The public interface of logging and cache classes.<p> * * Implements a storage manager wrapper that provides a consistent, * always available interface to storage management for the Database * class, despite the fact not all Database objects actually use file * storage.<p> * * The Logger class makes it possible to avoid testing for a * null Log Database attribute again and again, in many different places, * and generally avoids tight coupling between Database and Log, opening * the doors for multiple logs/caches in the future. In this way, the * Database class does not need to know the details of the Logging/Cache * implementation, lowering its breakability factor and promoting * long-term code flexibility. * * @author Fred Toussi (fredt@users dot sourceforge.net) * @version 1.9.0 * @since 1.7.0 */ public class Logger { public SimpleLog appLog; /** * The Log object this Logger object wraps */ private Log log; private Database database; /** * The LockFile object this Logger uses to cooperatively lock * the database files */ private LockFile lockFile; boolean needsCheckpoint; private boolean logsStatements; private boolean logStatements; private boolean syncFile = false; public Logger() { appLog = new SimpleLog(null, SimpleLog.LOG_NONE, false); } /** * Opens the specified Database object's database files and starts up * the logging process. <p> * * If the specified Database object is a new database, its database * files are first created. * * @param db the Database * @throws HsqlException if there is a problem, such as the case when * the specified files are in use by another process */ public void openLog(Database db) { needsCheckpoint = false; String path = db.getPath(); int loglevel = db.getProperties().getIntegerProperty( HsqlDatabaseProperties.hsqldb_applog, 0); this.database = db; if (loglevel != SimpleLog.LOG_NONE) { appLog = new SimpleLog(path + ".app.log", loglevel, !db.isFilesReadOnly()); } appLog.sendLine(SimpleLog.LOG_ERROR, "Database (re)opened"); logStatements = false; boolean useLock = db.getProperties().isPropertyTrue( HsqlDatabaseProperties.hsqldb_lock_file); if (useLock && !db.isFilesReadOnly()) { acquireLock(path); } log = new Log(db); log.open(); logsStatements = logStatements = !db.isFilesReadOnly(); } // fredt@users 20020130 - patch 495484 by boucherb@users /** * Shuts down the logging process using the specified mode. <p> * * @param closemode The mode in which to shut down the logging * process * <OL> * <LI> closemode -1 performs SHUTDOWN IMMEDIATELY, equivalent * to a poweroff or crash. * <LI> closemode 0 performs a normal SHUTDOWN that * checkpoints the database normally. * <LI> closemode 1 performs a shutdown compact that scripts * out the contents of any CACHED tables to the log then * deletes the existing *.data file that contains the data * for all CACHED table before the normal checkpoint process * which in turn creates a new, compact *.data file. * <LI> closemode 2 performs a SHUTDOWN SCRIPT. * </OL> * * @return true if closed with no problems or false if a problem was * encountered. */ public boolean closeLog(int closemode) { if (log == null) { return true; } try { switch (closemode) { case Database.CLOSEMODE_IMMEDIATELY : log.shutdown(); break; case Database.CLOSEMODE_NORMAL : log.close(false); break; case Database.CLOSEMODE_COMPACT : case Database.CLOSEMODE_SCRIPT : log.close(true); break; } } catch (Throwable e) { appLog.logContext(e, "error closing log"); appLog.close(); log = null; return false; } appLog.sendLine(SimpleLog.LOG_ERROR, "Database closed"); appLog.close(); log = null; return true; } /** * Determines if the logging process actually does anything. <p> * * In-memory Database objects do not need to log anything. This * method is essentially equivalent to testing whether this logger's * database is an in-memory mode database. * * @return true if this object encapsulates a non-null Log instance, * else false */ public boolean hasLog() { return log != null; } /** * Returns the Cache object or null if one doesn't exist. */ public DataFileCache getCache() { if (log == null) { return null; } else { return log.getCache(); } } /** * Returns the Cache object or null if one doesn't exist. */ public boolean hasCache() { if (log == null) { return false; } else { return log.hasCache(); } } /** * Records a Log entry representing a new connection action on the * specified Session object. * * @param session the Session object for which to record the log * entry * @throws HsqlException if there is a problem recording the Log * entry */ public synchronized void logStartSession(Session session) { if (logStatements) { writeToLog(session, session.getUser().getConnectUserSQL()); } } /** * Records a Log entry for the specified SQL statement, on behalf of * the specified Session object. * * @param session the Session object for which to record the Log * entry * @param statement the SQL statement to Log * @throws HsqlException if there is a problem recording the entry */ public synchronized void writeToLog(Session session, String statement) { if (logStatements && log != null) { log.writeStatement(session, statement); } } public synchronized void writeInsertStatement(Session session, Table table, Object[] row) { if (logStatements) { log.writeInsertStatement(session, table, row); } } public synchronized void writeDeleteStatement(Session session, Table t, Object[] row) { if (logStatements) { log.writeDeleteStatement(session, t, row); } } public synchronized void writeSequenceStatement(Session session, NumberSequence s) { if (logStatements) { log.writeSequenceStatement(session, s); } } public synchronized void writeCommitStatement(Session session) { if (logStatements) { log.writeCommitStatement(session); synchLog(); } } /** * Called after commits or after each statement when autocommit is on */ public synchronized void synchLog() { if (logStatements && syncFile) { log.synchLog(); } } public synchronized void synchLogForce() { if (logStatements) { log.synchLog(); } } /** * Checkpoints the database. <p> * * The most important effect of calling this method is to cause the * log file to be rewritten in the most efficient form to * reflect the current state of the database, i.e. only the DDL and * insert DML required to recreate the database in its present state. * Other house-keeping duties are performed w.r.t. other database * files, in order to ensure as much as possible the ACID properites * of the database. * * @throws HsqlException if there is a problem checkpointing the * database */ public synchronized void checkpoint(boolean mode) { if (logStatements) { appLog.logContext(SimpleLog.LOG_NORMAL, "start"); needsCheckpoint = false; log.checkpoint(mode); database.sessionManager.resetLoggedSchemas(); appLog.logContext(SimpleLog.LOG_NORMAL, "end"); } } /** * Sets the maximum size to which the log file can grow * before being automatically checkpointed. * * @param megas size in MB */ public synchronized void setLogSize(int megas) { if (log != null) { log.setLogSize(megas); } } /** * Sets the type of script file, currently 0 for text (default) * 1 for binary and 3 for compressed * * @param i The type */ public synchronized void setScriptType(int i) { if (log != null) { log.setScriptType(i); } } /** * Sets the log write delay mode to number of seconds. By default * executed commands written to the log are committed fully at most * 60 second after they are executed. This improves performance for * applications that execute a large number * of short running statements in a short period of time, but risks * failing to log some possibly large number of statements in the * event of a crash. A small value improves recovery. * A value of 0 will severly slow down logging when autocommit is on, * or many short transactions are committed. * * @param delay in seconds */ public synchronized void setWriteDelay(int delay) { if (log != null) { syncFile = (delay == 0); log.setWriteDelay(delay); } } public int getWriteDelay() { return log != null ? log.getWriteDelay() : 0; } public int getLogSize() { return log != null ? log.getLogSize() : 0; } public int getScriptType() { return log != null ? log.getScriptType() : 0; } public synchronized void setIncrementalBackup(boolean val) { if (log != null) { log.setIncrementalBackup(val); } } /** * Opens the TextCache object. */ public DataFileCache openTextCache(Table table, String source, boolean readOnlyData, boolean reversed) { return log.openTextCache(table, source, readOnlyData, reversed); } /** * Closes the TextCache object. */ public void closeTextCache(Table table) { log.closeTextCache(table); } public boolean needsCheckpoint() { return needsCheckpoint; } public void stopLogging() { logStatements = false; } public void restartLogging() { logStatements = logsStatements; } /** * Attempts to aquire a cooperative lock condition on the database files */ public void acquireLock(String path) { if (lockFile != null) { return; } lockFile = LockFile.newLockFileLock(path); } public void releaseLock() { try { if (lockFile != null) { lockFile.tryRelease(); } } catch (Exception e) {} lockFile = null; } public PersistentStore newStore(Session session, PersistentStoreCollection collection, TableBase table, boolean diskBased) { switch (table.getTableType()) { case TableBase.CACHED_TABLE : DataFileCache cache = getCache(); if (cache == null) { break; } return new RowStoreAVLDisk(collection, cache, (Table) table); case TableBase.MEMORY_TABLE : case TableBase.SYSTEM_TABLE : return new RowStoreAVLMemory(collection, (Table) table); case TableBase.TEXT_TABLE : return new RowStoreAVLDiskData(collection, (Table) table); case TableBase.TEMP_TABLE : diskBased = false; // fall through case TableBase.RESULT_TABLE : case TableBase.SYSTEM_SUBQUERY : case TableBase.VIEW_TABLE : case TableBase.TRANSITION_TABLE : if (session == null) { return null; } switch (table.persistenceScope) { case TableBase.SCOPE_STATEMENT : return new RowStoreAVLHybrid(session, collection, table, diskBased); case TableBase.SCOPE_TRANSACTION : return new RowStoreAVLHybrid(session, collection, table, diskBased); case TableBase.SCOPE_SESSION : return new RowStoreAVLHybrid(session, collection, table, diskBased); } } throw Error.runtimeError(ErrorCode.U_S0500, "PSCS"); } // static private SimpleDateFormat backupFileFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); static private Character runtimeFileDelim = null; public synchronized void backup(String destPath, String dbPath, boolean script, boolean blocking, boolean compressed) { /* If want to add db Id also, will need to pass either Database * instead of dbPath, or pass dbPath + Id from CommandStatement. */ if (runtimeFileDelim == null) { runtimeFileDelim = new Character(System.getProperty("file.separator").charAt(0)); } String instanceName = new File(dbPath).getName(); if (destPath == null || destPath.length() < 1) { throw Error.error(ErrorCode.X_2200F, "0-length destination path"); } char lastChar = destPath.charAt(destPath.length() - 1); boolean generateName = (lastChar == '/' || lastChar == runtimeFileDelim.charValue()); String defaultCompressionSuffix = compressed ? ".tar.gz" : ".tar"; File archiveFile = generateName ? (new File(destPath.substring(0, destPath.length() - 1), instanceName + '-' + backupFileFormat.format(new java.util.Date()) + defaultCompressionSuffix)) : (new File(destPath)); boolean nameImpliesCompress = archiveFile.getName().endsWith(".tar.gz") || archiveFile.getName().endsWith(".tgz"); if ((!nameImpliesCompress) && !archiveFile.getName().endsWith(".tar")) { throw Error.error(ErrorCode.UNSUPPORTED_FILENAME_SUFFIX, 0, new String[] { archiveFile.getName(), ".tar, .tar.gz, .tgz" }); } if (compressed != nameImpliesCompress) { throw Error.error(ErrorCode.COMPRESSION_SUFFIX_MISMATCH, 0, new Object[] { new Boolean(compressed), archiveFile.getName() }); } log.closeForBackup(); try { appLog.logContext(SimpleLog.LOG_NORMAL, "Initiating backup of instance '" + instanceName + "'"); // By default, DbBackup will throw if archiveFile (or // corresponding work file) already exist. That's just what we // want here. DbBackup backup = new DbBackup(archiveFile, dbPath); backup.setAbortUponModify(false); backup.write(); appLog.logContext(SimpleLog.LOG_NORMAL, "Successfully backed up instance '" + instanceName + "' to '" + destPath + "'"); // RENAME tempPath to destPath } catch (IllegalArgumentException iae) { throw Error.error(ErrorCode.X_HV00A, iae.getMessage()); } catch (IOException ioe) { throw Error.error(ErrorCode.FILE_IO_ERROR, ioe.getMessage()); } catch (TarMalformatException tme) { throw Error.error(ErrorCode.FILE_IO_ERROR, tme.getMessage()); } finally { log.openAfterBackup(); needsCheckpoint = false; } } }