Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.transaction.file; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Iterator; import java.util.Collections; import org.apache.commons.transaction.locking.GenericLock; import org.apache.commons.transaction.locking.GenericLockManager; import org.apache.commons.transaction.locking.LockException; import org.apache.commons.transaction.locking.LockManager2; import org.apache.commons.transaction.util.FileHelper; import org.apache.commons.transaction.util.LoggerFacade; /** * A resource manager for streamable objects stored in a file system. * * It is intended for developer and "out of the box" use. * It is <em>not</em> intended to be a real alternative for * a full blown DMBS (of course it can not be compared to a RDBMS at all). * * Major features:<br> * <ul> * <li>Transactions performed with this class more or less comform to the widely accepted ACID properties * <li>Reading should be as fast as from the ordinary file system (at the cost of a bit slower commits) * </ul> * * Compared to a "real" DBMS major limitations are (in order of assumed severity):<br> * <ul> * <li>Number of simultaneously open resources is limited to the number of available file descriptors * <li>It does not scale a bit * <li>Pessimistic transaction and locking scheme * <li>Isolation level currently is restricted to <em>read committed</em> and <em>repeated read</em> (which is not that bad) * </ul> * * <em>Important</em>: If possible you should have the work and store directory located in the * same file system. If not, you might get additional problems, as there are: * <ul> * <li>On commit it might be necessay to copy files instead of rename/relink them. This may lead to time consuming, * overly blocking commit phases and higher risk of corrupted files * <li>Prepare phase might be too permissive, no check for sufficient memory on store file system is possible * </ul> * * General limitations include:<br> * <ul> * <li>Due to lack of synchronization on the transaction context level, every transaction may only be * accessed by a <em>single thread</em> throughout its full life. * This means it is forbidden for a thread that has not started a transaction * to perform any operations inside this transaction. However, threads associated * with different transactions can safely access these methods concurrently. * Reasons for the lack of synchronization are improved performance and simplicity (of the code of this class). * <li>There is no dedicated class for a transaction. Having such a class would be better practice and * make certain actions more intuitive. * <li>Resource identifiers need a reasonsable string representation obtainable by <code>toString</code>. * More specifically, they will have to resolve to a <em>valid</em> file path that does note denote a directory. * If it does, you might be able to create it, but not to read or write anything * from resp. to it. Valid string representations of a resource idenfier are * for example "file" "/root" or "hjfhdfhuhuhsdufhdsufhdsufhdfuhdfduhduhduhdu". * Invalid are for example "/" or "/root/". Invalid on some file systems are for example "c:" or "file://huhu". * <li>As there are no active processes inside this RM and it shares its threads with the application, * control over transactions is limited to points where the application calls the RM. * In particular, this disables <em>active</em> termination of transactions upon timeout. * <li>There is no notion of a connection to this file manager. This means you can not connect from hosts other than * local and you will get problems when plugging this store into a J2EE store using connectors. * <li>Methods should throw more specific exceptions * </ul> * * <p><em>Caution</em>:<br> * The <code>txId</code> passed to many methods as an identifier for the * transaction concerned will function as a key in a <code>HashMap</code>. * Thus assure that <code>equals</code> and <code>hashCode</code> are both * properly implemented and match each other.</p> * * <p><em>Caution</em>:<br> * You will have to guarantee that no other process will access neither * the store or the working dir concurrently to this <code>FileResourceManager</code>.</p> * * <p><em>Special Caution</em>:<br> * Be very careful not to have two instances of <code>FileResourceManager</code> * working in the same store and/or working dir. * * @version $Id: FileResourceManager.java 573315 2007-09-06 16:28:42Z ozeigermann $ */ public class FileResourceManager implements ResourceManager, ResourceManagerErrorCodes { // reflects the natural isolation level of this store protected static final int NATIVE_ISOLATION_LEVEL = ISOLATION_LEVEL_REPEATABLE_READ; protected static final int DEFAULT_ISOLATION_LEVEL = NATIVE_ISOLATION_LEVEL; protected static final int NO_LOCK = 0; protected static final int LOCK_ACCESS = NO_LOCK + 1; protected static final int LOCK_SHARED = NO_LOCK + 2; protected static final int LOCK_EXCLUSIVE = NO_LOCK + 3; protected static final int LOCK_COMMIT = NO_LOCK + 4; protected static final int OPERATION_MODE_STOPPED = 0; protected static final int OPERATION_MODE_STOPPING = 1; protected static final int OPERATION_MODE_STARTED = 2; protected static final int OPERATION_MODE_STARTING = 3; protected static final int OPERATION_MODE_RECOVERING = 4; protected static final String DEFAULT_PARAMETER_ENCODING = "ISO-8859-15"; protected static final int DEFAULT_TIMEOUT_MSECS = 5000; protected static final int DEFAULT_COMMIT_TIMEOUT_FACTOR = 2; protected static final String WORK_CHANGE_DIR = "change"; protected static final String WORK_DELETE_DIR = "delete"; protected static final String CONTEXT_FILE = "transaction.log"; /* * --- Static helper methods --- * * */ protected static void applyDeletes(File removeDir, File targetDir, File rootDir) throws IOException { if (removeDir.isDirectory() && targetDir.isDirectory()) { File[] files = removeDir.listFiles(); for (int i = 0; i < files.length; i++) { File removeFile = files[i]; File targetFile = new File(targetDir, removeFile.getName()); if (removeFile.isFile()) { if (targetFile.exists()) { if (!targetFile.delete()) { throw new IOException( "Could not delete file " + removeFile.getName() + " in directory targetDir"); } } // indicate, this has been done removeFile.delete(); } else { applyDeletes(removeFile, targetFile, rootDir); } } // delete empty target directories, except root dir if (!targetDir.equals(rootDir) && targetDir.list().length == 0) { targetDir.delete(); } } } /* * --- object members --- * * */ protected String workDir; protected String storeDir; protected boolean cleanUp = true; protected boolean dirty = false; protected int operationMode = OPERATION_MODE_STOPPED; protected long defaultTimeout = DEFAULT_TIMEOUT_MSECS; protected boolean debug; protected LoggerFacade logger; protected Map globalTransactions; protected List globalOpenResources; protected LockManager2 lockManager; protected ResourceIdToPathMapper idMapper = null; protected TransactionIdToPathMapper txIdMapper = null; protected int idCnt = 0; /* * --- ctor and general getter / setter methods --- * * */ /** * Creates a new resource manager operation on the specified directories. * * @param storeDir directory where main data should go after commit * @param workDir directory where transactions store temporary data * @param urlEncodePath if set to <code>true</code> encodes all paths to allow for any kind of characters * @param logger the logger to be used by this store */ public FileResourceManager(String storeDir, String workDir, boolean urlEncodePath, LoggerFacade logger) { this(storeDir, workDir, urlEncodePath, logger, false); } /** * Creates a new resource manager operation on the specified directories. * * @param storeDir directory where main data should go after commit * @param workDir directory where transactions store temporary data * @param urlEncodePath if set to <code>true</code> encodes all paths to allow for any kind of characters * @param logger the logger to be used by this store * @param debug if set to <code>true</code> logs all locking information to "transaction.log" for debugging inspection */ public FileResourceManager(String storeDir, String workDir, boolean urlEncodePath, LoggerFacade logger, boolean debug) { this(storeDir, workDir, urlEncodePath ? new URLEncodeIdMapper() : null, new NoOpTransactionIdToPathMapper(), logger, debug); } /** * Creates a new resource manager operation on the specified directories. * This constructor is reintroduced for backwards API compatibility and is used by jakarta-slide. * * @param storeDir directory where main data should go after commit * @param workDir directory where transactions store temporary data * @param idMapper mapper for resourceId to path * @param logger the logger to be used by this store * @param debug if set to <code>true</code> logs all locking information to "transaction.log" for debugging inspection */ public FileResourceManager(String storeDir, String workDir, ResourceIdToPathMapper idMapper, LoggerFacade logger, boolean debug) { this(storeDir, workDir, idMapper, new NoOpTransactionIdToPathMapper(), logger, debug); } /** * Creates a new resource manager operation on the specified directories. * * @param storeDir directory where main data should go after commit * @param workDir directory where transactions store temporary data * @param idMapper mapper for resourceId to path * @param txIdMapper mapper for transaction id to path * @param logger the logger to be used by this store * @param debug if set to <code>true</code> logs all locking information to "transaction.log" for debugging inspection */ public FileResourceManager(String storeDir, String workDir, ResourceIdToPathMapper idMapper, TransactionIdToPathMapper txIdMapper, LoggerFacade logger, boolean debug) { this.workDir = workDir; this.storeDir = storeDir; this.idMapper = idMapper; this.txIdMapper = txIdMapper; this.logger = logger; this.debug = debug; } /** * Gets the store directory. * * @return the store directory * @see #FileResourceManager(String, String, boolean, LoggerFacade) * @see #FileResourceManager(String, String, boolean, LoggerFacade, boolean) */ public String getStoreDir() { return storeDir; } /** * Gets the working directory. * * @return the work directory * @see #FileResourceManager(String, String, boolean, LoggerFacade) * @see #FileResourceManager(String, String, boolean, LoggerFacade, boolean) */ public String getWorkDir() { return workDir; } /** * Gets the logger used by this resource manager. * * @return used logger */ public LoggerFacade getLogger() { return logger; } /* * --- public methods of interface ResourceManager --- * * */ public boolean lockResource(Object resourceId, Object txId) throws ResourceManagerException { lockResource(resourceId, txId, false); // XXX will never return false as it will either throw or return true return true; } public boolean lockResource(Object resourceId, Object txId, boolean shared) throws ResourceManagerException { lockResource(resourceId, txId, shared, true, Long.MAX_VALUE, true); // XXX will never return false as it will either throw or return true return true; } public boolean lockResource(Object resourceId, Object txId, boolean shared, boolean wait, long timeoutMSecs, boolean reentrant) throws ResourceManagerException { TransactionContext context = (shared ? txInitialSaneCheck(txId) : txInitialSaneCheckForWriting(txId)); assureNotMarkedForRollback(context); fileInitialSaneCheck(txId, resourceId); // XXX allows locking of non existent resources (e.g. to prepare a create) int level = (shared ? getSharedLockLevel(context) : LOCK_EXCLUSIVE); try { lockManager.lock(txId, resourceId, level, reentrant, Math.min(timeoutMSecs, context.timeoutMSecs)); // XXX will never return false as it will either throw or return true return true; } catch (LockException e) { switch (e.getCode()) { case LockException.CODE_INTERRUPTED: throw new ResourceManagerException("Could not get lock for resource at '" + resourceId + "'", ERR_NO_LOCK, txId); case LockException.CODE_TIMED_OUT: throw new ResourceManagerException("Lock timed out for resource at '" + resourceId + "'", ERR_NO_LOCK, txId); case LockException.CODE_DEADLOCK_VICTIM: throw new ResourceManagerException("Deadlock victim resource at '" + resourceId + "'", ERR_DEAD_LOCK, txId); default: throw new ResourceManagerException("Locking exception for resource at '" + resourceId + "'", ERR_DEAD_LOCK, txId); } } } public int getDefaultIsolationLevel() { return DEFAULT_ISOLATION_LEVEL; } public int[] getSupportedIsolationLevels() throws ResourceManagerException { return new int[] { ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ }; } public boolean isIsolationLevelSupported(int level) throws ResourceManagerException { return (level == ISOLATION_LEVEL_READ_COMMITTED || level == ISOLATION_LEVEL_REPEATABLE_READ); } /** * Gets the default transaction timeout in <em>milliseconds</em>. */ public long getDefaultTransactionTimeout() { return defaultTimeout; } /** * Sets the default transaction timeout. * * @param timeout timeout in <em>milliseconds</em> */ public void setDefaultTransactionTimeout(long timeout) { defaultTimeout = timeout; } public long getTransactionTimeout(Object txId) throws ResourceManagerException { assureRMReady(); long msecs = 0; TransactionContext context = getContext(txId); if (context == null) { msecs = getDefaultTransactionTimeout(); } else { msecs = context.timeoutMSecs; } return msecs; } public void setTransactionTimeout(Object txId, long mSecs) throws ResourceManagerException { assureRMReady(); TransactionContext context = getContext(txId); if (context != null) { context.timeoutMSecs = mSecs; } else { throw new ResourceManagerException(ERR_NO_TX, txId); } } public int getIsolationLevel(Object txId) throws ResourceManagerException { assureRMReady(); TransactionContext context = getContext(txId); if (context == null) { return DEFAULT_ISOLATION_LEVEL; } else { return context.isolationLevel; } } public void setIsolationLevel(Object txId, int level) throws ResourceManagerException { assureRMReady(); TransactionContext context = getContext(txId); if (context != null) { if (level != ISOLATION_LEVEL_READ_COMMITTED || level != ISOLATION_LEVEL_REPEATABLE_READ) { context.isolationLevel = level; } else { throw new ResourceManagerException(ERR_ISOLATION_LEVEL_UNSUPPORTED, txId); } } else { throw new ResourceManagerException(ERR_NO_TX, txId); } } public synchronized void start() throws ResourceManagerSystemException { logger.logInfo("Starting RM at '" + storeDir + "' / '" + workDir + "'"); operationMode = OPERATION_MODE_STARTING; globalTransactions = Collections.synchronizedMap(new HashMap()); lockManager = new GenericLockManager(LOCK_COMMIT, logger); globalOpenResources = Collections.synchronizedList(new ArrayList()); recover(); sync(); operationMode = OPERATION_MODE_STARTED; if (dirty) { logger.logWarning("Started RM, but in dirty mode only (Recovery of pending transactions failed)"); } else { logger.logInfo("Started RM"); } } public synchronized boolean stop(int mode) throws ResourceManagerSystemException { return stop(mode, getDefaultTransactionTimeout() * DEFAULT_COMMIT_TIMEOUT_FACTOR); } public synchronized boolean stop(int mode, long timeOut) throws ResourceManagerSystemException { logger.logInfo("Stopping RM at '" + storeDir + "' / '" + workDir + "'"); operationMode = OPERATION_MODE_STOPPING; sync(); boolean success = shutdown(mode, timeOut); releaseGlobalOpenResources(); if (success) { operationMode = OPERATION_MODE_STOPPED; logger.logInfo("Stopped RM"); } else { logger.logWarning("Failed to stop RM"); } return success; } public synchronized boolean recover() throws ResourceManagerSystemException { if (operationMode != OPERATION_MODE_STARTED && operationMode != OPERATION_MODE_STARTING) { throw new ResourceManagerSystemException(ERR_SYSTEM, "Recovery is possible in started or starting resource manager only"); } int oldMode = operationMode; operationMode = OPERATION_MODE_RECOVERING; recoverContexts(); if (globalTransactions.size() > 0) { logger.logInfo("Recovering pending transactions"); } dirty = !rollBackOrForward(); operationMode = oldMode; return dirty; } public int getTransactionState(Object txId) throws ResourceManagerException { TransactionContext context = getContext(txId); if (context == null) { return STATUS_NO_TRANSACTION; } else { return context.status; } } public void startTransaction(Object txId) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine("Starting Tx " + txId); assureStarted(); // can only start a new transaction when not already stopping if (txId == null || txIdMapper.getPathForId(txId).length() == 0) { throw new ResourceManagerException(ERR_TXID_INVALID, txId); } // be sure we are the only ones who create this tx synchronized (globalTransactions) { TransactionContext context = getContext(txId); if (context != null) { throw new ResourceManagerException(ERR_DUP_TX, txId); } context = new TransactionContext(txId); context.init(); globalTransactions.put(txId, context); } } public void markTransactionForRollback(Object txId) throws ResourceManagerException { assureRMReady(); TransactionContext context = txInitialSaneCheckForWriting(txId); try { context.status = STATUS_MARKED_ROLLBACK; context.saveState(); } finally { // be very sure to free locks and resources, as application might crash or otherwise forget to roll this tx back context.finalCleanUp(); } } public int prepareTransaction(Object txId) throws ResourceManagerException { assureRMReady(); // do not allow any further writing or commit or rollback when db is corrupt if (dirty) { throw new ResourceManagerSystemException( "Database is set to dirty, this *may* mean it is corrupt. No modifications are allowed until a recovery run has been performed!", ERR_SYSTEM, txId); } if (txId == null) { throw new ResourceManagerException(ERR_TXID_INVALID, txId); } TransactionContext context = getContext(txId); if (context == null) { return PREPARE_FAILURE; } synchronized (context) { sync(); if (context.status != STATUS_ACTIVE) { context.status = STATUS_MARKED_ROLLBACK; context.saveState(); return PREPARE_FAILURE; } if (logger.isFineEnabled()) logger.logFine("Preparing Tx " + txId); int prepareStatus = PREPARE_FAILURE; context.status = STATUS_PREPARING; context.saveState(); // do all checks as early as possible context.closeResources(); if (context.readOnly) { prepareStatus = PREPARE_SUCCESS_READONLY; } else { // do all checks as early as possible try { context.upgradeLockToCommit(); } catch (ResourceManagerException rme) { // if this did not work, mark it for roll back as early as possible markTransactionForRollback(txId); throw rme; } prepareStatus = PREPARE_SUCCESS; } context.status = STATUS_PREPARED; context.saveState(); if (logger.isFineEnabled()) logger.logFine("Prepared Tx " + txId); return prepareStatus; } } public void rollbackTransaction(Object txId) throws ResourceManagerException { assureRMReady(); TransactionContext context = txInitialSaneCheckForWriting(txId); // needing synchronization in order not to interfer with shutdown thread synchronized (context) { try { if (logger.isFineEnabled()) logger.logFine("Rolling back Tx " + txId); context.status = STATUS_ROLLING_BACK; context.saveState(); context.rollback(); if (logger.isFineEnabled()) logger.logFine("All resources successfully removed for tx" + txId); context.status = STATUS_ROLLEDBACK; context.saveState(); globalTransactions.remove(txId); context.cleanUp(); if (logger.isFineEnabled()) logger.logFine("Rolled back Tx " + txId); // any system or runtime exceptions or errors thrown in rollback means we are in deep trouble, set the dirty flag } catch (Error e) { setDirty(txId, e); throw e; } catch (RuntimeException e) { setDirty(txId, e); throw e; } catch (ResourceManagerSystemException e) { setDirty(txId, e); throw e; } finally { context.finalCleanUp(); // tell shutdown thread this tx is finished context.notifyFinish(); } } } public void commitTransaction(Object txId) throws ResourceManagerException { assureRMReady(); TransactionContext context = txInitialSaneCheckForWriting(txId); assureNotMarkedForRollback(context); // needing synchronization in order not to interfer with shutdown thread synchronized (context) { try { if (logger.isFineEnabled()) logger.logFine("Committing Tx " + txId); context.status = STATUS_COMMITTING; context.saveState(); context.commit(); if (logger.isFineEnabled()) logger.logFine("All resources successfully moved for tx" + txId); context.status = STATUS_COMMITTED; context.saveState(); globalTransactions.remove(txId); context.cleanUp(); if (logger.isFineEnabled()) logger.logFine("Committed Tx " + txId); // any system or runtime exceptions or errors thrown in rollback means we are in deep trouble, set the dirty flag } catch (Error e) { setDirty(txId, e); throw e; } catch (RuntimeException e) { setDirty(txId, e); throw e; } catch (ResourceManagerSystemException e) { setDirty(txId, e); throw e; // like "could not upgrade lock" } catch (ResourceManagerException e) { logger.logWarning("Could not commit tx " + txId + ", rolling back instead", e); rollbackTransaction(txId); } finally { context.finalCleanUp(); // tell shutdown thread this tx is finished context.notifyFinish(); } } } public boolean resourceExists(Object resourceId) throws ResourceManagerException { // create temporary light weight tx Object txId; TransactionContext context; synchronized (globalTransactions) { txId = generatedUniqueTxId(); if (logger.isFinerEnabled()) logger.logFiner("Creating temporary light weight tx " + txId + " to check for exists"); context = new TransactionContext(txId); context.isLightWeight = true; // XXX higher isolation might be needed to make sure upgrade to commit lock always works context.isolationLevel = ISOLATION_LEVEL_READ_COMMITTED; // context.isolationLevel = ISOLATION_LEVEL_REPEATABLE_READ; globalTransactions.put(txId, context); } boolean exists = resourceExists(txId, resourceId); context.freeLocks(); globalTransactions.remove(txId); if (logger.isFinerEnabled()) logger.logFiner("Removing temporary light weight tx " + txId); return exists; } public boolean resourceExists(Object txId, Object resourceId) throws ResourceManagerException { lockResource(resourceId, txId, true); return (getPathForRead(txId, resourceId) != null); } public void deleteResource(Object txId, Object resourceId) throws ResourceManagerException { deleteResource(txId, resourceId, true); } public void deleteResource(Object txId, Object resourceId, boolean assureOnly) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine(txId + " deleting " + resourceId); lockResource(resourceId, txId, false); if (getPathForRead(txId, resourceId) == null) { if (assureOnly) { return; } throw new ResourceManagerException("No such resource at '" + resourceId + "'", ERR_NO_SUCH_RESOURCE, txId); } String txDeletePath = getDeletePath(txId, resourceId); String mainPath = getMainPath(resourceId); try { getContext(txId).readOnly = false; // first undo change / create when there was one undoScheduledChangeOrCreate(txId, resourceId); // if there still is a file in main store, we need to schedule // a delete additionally if (FileHelper.fileExists(mainPath)) { FileHelper.createFile(txDeletePath); } } catch (IOException e) { throw new ResourceManagerSystemException("Can not delete resource at '" + resourceId + "'", ERR_SYSTEM, txId, e); } } public void createResource(Object txId, Object resourceId) throws ResourceManagerException { createResource(txId, resourceId, true); } public void createResource(Object txId, Object resourceId, boolean assureOnly) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine(txId + " creating " + resourceId); lockResource(resourceId, txId, false); if (getPathForRead(txId, resourceId) != null) { if (assureOnly) { return; } throw new ResourceManagerException("Resource at '" + resourceId + "', already exists", ERR_RESOURCE_EXISTS, txId); } String txChangePath = getChangePath(txId, resourceId); try { getContext(txId).readOnly = false; // creation means either undoing a delete or actually scheduling a create if (!undoScheduledDelete(txId, resourceId)) { FileHelper.createFile(txChangePath); } } catch (IOException e) { throw new ResourceManagerSystemException("Can not create resource at '" + resourceId + "'", ERR_SYSTEM, txId, e); } } public void copyResource(Object txId, Object fromResourceId, Object toResourceId, boolean overwrite) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine(txId + " copying " + fromResourceId + " to " + toResourceId); lockResource(fromResourceId, txId, true); lockResource(toResourceId, txId, false); if (resourceExists(txId, toResourceId) && !overwrite) { throw new ResourceManagerException("Resource at '" + toResourceId + "' already exists", ERR_RESOURCE_EXISTS, txId); } InputStream fromResourceStream = null; OutputStream toResourceStream = null; try { fromResourceStream = readResource(txId, fromResourceId); toResourceStream = writeResource(txId, toResourceId); FileHelper.copy(fromResourceStream, toResourceStream); } catch (IOException e) { throw new ResourceManagerException(ERR_SYSTEM, txId, e); } finally { closeOpenResource(fromResourceStream); closeOpenResource(toResourceStream); } } public void moveResource(Object txId, Object fromResourceId, Object toResourceId, boolean overwrite) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine(txId + " moving " + fromResourceId + " to " + toResourceId); lockResource(fromResourceId, txId, false); lockResource(toResourceId, txId, false); copyResource(txId, fromResourceId, toResourceId, overwrite); deleteResource(txId, fromResourceId, false); } public InputStream readResource(Object resourceId) throws ResourceManagerException { // create temporary light weight tx Object txId; synchronized (globalTransactions) { txId = generatedUniqueTxId(); if (logger.isFinerEnabled()) logger.logFiner("Creating temporary light weight tx " + txId + " for reading"); TransactionContext context = new TransactionContext(txId); context.isLightWeight = true; // XXX higher isolation might be needed to make sure upgrade to commit lock always works context.isolationLevel = ISOLATION_LEVEL_READ_COMMITTED; // context.isolationLevel = ISOLATION_LEVEL_REPEATABLE_READ; globalTransactions.put(txId, context); } InputStream is = readResource(txId, resourceId); return is; } public InputStream readResource(Object txId, Object resourceId) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine(txId + " reading " + resourceId); lockResource(resourceId, txId, true); String resourcePath = getPathForRead(txId, resourceId); if (resourcePath == null) { throw new ResourceManagerException("No such resource at '" + resourceId + "'", ERR_NO_SUCH_RESOURCE, txId); } File file = new File(resourcePath); try { FileInputStream stream = new FileInputStream(file); getContext(txId).registerResource(stream); return new InputStreamWrapper(stream, txId, resourceId); } catch (FileNotFoundException e) { throw new ResourceManagerSystemException("File '" + resourcePath + "' does not exist", ERR_SYSTEM, txId); } } public OutputStream writeResource(Object txId, Object resourceId) throws ResourceManagerException { return writeResource(txId, resourceId, false); } public OutputStream writeResource(Object txId, Object resourceId, boolean append) throws ResourceManagerException { if (logger.isFineEnabled()) logger.logFine(txId + " writing " + resourceId); lockResource(resourceId, txId, false); if (append) { String mainPath = getMainPath(resourceId); String txChangePath = getChangePath(txId, resourceId); String txDeletePath = getDeletePath(txId, resourceId); boolean changeExists = FileHelper.fileExists(txChangePath); boolean deleteExists = FileHelper.fileExists(txDeletePath); boolean mainExists = FileHelper.fileExists(mainPath); if (mainExists && !changeExists && !deleteExists) { // the read and the write path for resourceId will be different! copyResource(txId, resourceId, resourceId, true); } } String resourcePath = getPathForWrite(txId, resourceId); try { FileOutputStream stream = new FileOutputStream(resourcePath, append); TransactionContext context = getContext(txId); context.registerResource(stream); context.readOnly = false; return stream; } catch (FileNotFoundException e) { throw new ResourceManagerSystemException("File '" + resourcePath + "' does not exist", ERR_SYSTEM, txId); } } /* * --- additional public methods complementing implementation of interfaces --- * * */ /** * Resets the store by deleting work <em>and</em> store directory. */ public synchronized void reset() { FileHelper.removeRec(new File(storeDir)); FileHelper.removeRec(new File(workDir)); new File(storeDir).mkdirs(); new File(workDir).mkdirs(); } /** * Synchronizes persistent data with caches. Is implemented with an empty * body, but called by other methods relying on synchronization. Subclasses * that utilize caching must implement this method reasonably. * * @throws ResourceManagerSystemException if anything fatal hapened during synchonization */ public synchronized void sync() throws ResourceManagerSystemException { } /** * Generates a transaction identifier unique to this resource manager. To do so * it requires this resource manager to be started. * * @return generated transaction identifier * @throws ResourceManagerSystemException if this resource manager has not been started, yet */ public String generatedUniqueTxId() throws ResourceManagerSystemException { assureRMReady(); String txId; synchronized (globalTransactions) { do { txId = Long.toHexString(System.currentTimeMillis()) + "-" + Integer.toHexString(idCnt++); // XXX busy loop } while (getContext(txId) != null); } return txId; } /* * --- sane checks --- * * */ protected void fileInitialSaneCheck(Object txId, Object path) throws ResourceManagerException { if (path == null || path.toString().length() == 0) { throw new ResourceManagerException(ERR_RESOURCEID_INVALID, txId); } } protected void assureStarted() throws ResourceManagerSystemException { if (operationMode != OPERATION_MODE_STARTED) { throw new ResourceManagerSystemException("Resource Manager Service not started", ERR_SYSTEM, null); } } protected void assureRMReady() throws ResourceManagerSystemException { if (operationMode != OPERATION_MODE_STARTED && operationMode != OPERATION_MODE_STOPPING) { throw new ResourceManagerSystemException("Resource Manager Service not ready", ERR_SYSTEM, null); } } protected void assureNotMarkedForRollback(TransactionContext context) throws ResourceManagerException { if (context.status == STATUS_MARKED_ROLLBACK) { throw new ResourceManagerException(ERR_MARKED_FOR_ROLLBACK, context.txId); } } protected TransactionContext txInitialSaneCheckForWriting(Object txId) throws ResourceManagerException { assureRMReady(); // do not allow any further writing or commit or rollback when db is corrupt if (dirty) { throw new ResourceManagerSystemException( "Database is set to dirty, this *may* mean it is corrupt. No modifications are allowed until a recovery run has been performed!", ERR_SYSTEM, txId); } return txInitialSaneCheck(txId); } protected TransactionContext txInitialSaneCheck(Object txId) throws ResourceManagerException { assureRMReady(); if (txId == null) { throw new ResourceManagerException(ERR_TXID_INVALID, txId); } TransactionContext context = getContext(txId); if (context == null) { throw new ResourceManagerException(ERR_NO_TX, txId); } return context; } /* * --- General Helpers --- * * */ protected TransactionContext getContext(Object txId) { return (TransactionContext) globalTransactions.get(txId); } protected String assureLeadingSlash(Object pathObject) { String path = ""; if (pathObject != null) { if (idMapper != null) { path = idMapper.getPathForId(pathObject); } else { path = pathObject.toString(); } if (path.length() > 0 && path.charAt(0) != '/' && path.charAt(0) != '\\') { path = "/" + path; } } return path; } protected String getMainPath(Object path) { StringBuffer buf = new StringBuffer(storeDir.length() + path.toString().length() + 5); buf.append(storeDir).append(assureLeadingSlash(path)); return buf.toString(); } protected String getTransactionBaseDir(Object txId) { return workDir + '/' + txIdMapper.getPathForId(txId); } protected String getChangePath(Object txId, Object path) { String txBaseDir = getTransactionBaseDir(txId); StringBuffer buf = new StringBuffer( txBaseDir.length() + path.toString().length() + WORK_CHANGE_DIR.length() + 5); buf.append(txBaseDir).append('/').append(WORK_CHANGE_DIR).append(assureLeadingSlash(path)); return buf.toString(); } protected String getDeletePath(Object txId, Object path) { String txBaseDir = getTransactionBaseDir(txId); StringBuffer buf = new StringBuffer( txBaseDir.length() + path.toString().length() + WORK_DELETE_DIR.length() + 5); buf.append(txBaseDir).append('/').append(WORK_DELETE_DIR).append(assureLeadingSlash(path)); return buf.toString(); } protected boolean undoScheduledDelete(Object txId, Object resourceId) throws ResourceManagerException { String txDeletePath = getDeletePath(txId, resourceId); File deleteFile = new File(txDeletePath); if (deleteFile.exists()) { if (!deleteFile.delete()) { throw new ResourceManagerSystemException("Failed to undo delete of '" + resourceId + "'", ERR_SYSTEM, txId); } return true; } return false; } protected boolean undoScheduledChangeOrCreate(Object txId, Object resourceId) throws ResourceManagerException { String txChangePath = getChangePath(txId, resourceId); File changeFile = new File(txChangePath); if (changeFile.exists()) { if (!changeFile.delete()) { throw new ResourceManagerSystemException("Failed to undo change / create of '" + resourceId + "'", ERR_SYSTEM, txId); } return true; } return false; } protected String getPathForWrite(Object txId, Object resourceId) throws ResourceManagerException { try { // when we want to write, be sure to write to a local copy String txChangePath = getChangePath(txId, resourceId); if (!FileHelper.fileExists(txChangePath)) { FileHelper.createFile(txChangePath); } return txChangePath; } catch (IOException e) { throw new ResourceManagerSystemException("Can not write to resource at '" + resourceId + "'", ERR_SYSTEM, txId, e); } } protected String getPathForRead(Object txId, Object resourceId) throws ResourceManagerException { String mainPath = getMainPath(resourceId); String txChangePath = getChangePath(txId, resourceId); String txDeletePath = getDeletePath(txId, resourceId); // now, this gets a bit complicated: boolean changeExists = FileHelper.fileExists(txChangePath); boolean deleteExists = FileHelper.fileExists(txDeletePath); boolean mainExists = FileHelper.fileExists(mainPath); boolean resourceIsDir = ((mainExists && new File(mainPath).isDirectory()) || (changeExists && new File(txChangePath).isDirectory())); if (resourceIsDir) { logger.logWarning("Resource at '" + resourceId + "' maps to directory"); } // first do some sane checks // this may never be, two cases are possible, both disallowing to have a delete together with a change // 1. first there was a change, than a delete -> at least delete file exists (when there is a file in main store) // 2. first there was a delete, than a change -> only change file exists if (!resourceIsDir && changeExists && deleteExists) { throw new ResourceManagerSystemException( "Inconsistent delete and change combination for resource at '" + resourceId + "'", ERR_TX_INCONSISTENT, txId); } // you should not have been allowed to delete a file that does not exist at all if (deleteExists && !mainExists) { throw new ResourceManagerSystemException("Inconsistent delete for resource at '" + resourceId + "'", ERR_TX_INCONSISTENT, txId); } if (changeExists) { return txChangePath; } else if (mainExists && !deleteExists) { return mainPath; } else { return null; } } /* * --- Locking Helpers --- * * */ protected int getSharedLockLevel(TransactionContext context) throws ResourceManagerException { if (context.isolationLevel == ISOLATION_LEVEL_READ_COMMITTED || context.isolationLevel == ISOLATION_LEVEL_READ_UNCOMMITTED) { return LOCK_ACCESS; } else if (context.isolationLevel == ISOLATION_LEVEL_REPEATABLE_READ || context.isolationLevel == ISOLATION_LEVEL_SERIALIZABLE) { return LOCK_SHARED; } else { return LOCK_ACCESS; } } /* * --- Resource Management --- * * */ protected void registerOpenResource(Object openResource) { if (logger.isFinerEnabled()) logger.logFiner("Registering open resource " + openResource); globalOpenResources.add(openResource); } protected void releaseGlobalOpenResources() { ArrayList copy; synchronized (globalOpenResources) { // XXX need to copy in order to allow removal in releaseOpenResource copy = new ArrayList(globalOpenResources); for (Iterator it = copy.iterator(); it.hasNext();) { Object stream = it.next(); closeOpenResource(stream); } } } protected void closeOpenResource(Object openResource) { if (logger.isFinerEnabled()) logger.logFiner("Releasing resource " + openResource); globalOpenResources.remove(openResource); if (openResource instanceof InputStream) { InputStream is = (InputStream) openResource; try { is.close(); } catch (IOException e) { // do not care, as it might have been closed somewhere else, before } } else if (openResource instanceof OutputStream) { OutputStream os = (OutputStream) openResource; try { os.close(); } catch (IOException e) { // do not care, as it might have been closed somewhere else, before } } } /* * --- Recovery / Shutdown Support --- * * */ protected boolean rollBackOrForward() { boolean allCool = true; synchronized (globalTransactions) { ArrayList contexts = new ArrayList(globalTransactions.values()); for (Iterator it = contexts.iterator(); it.hasNext();) { TransactionContext context = (TransactionContext) it.next(); if (context.status == STATUS_COMMITTING) { // roll forward logger.logInfo("Rolling forward " + context.txId); try { context.commit(); context.status = STATUS_COMMITTED; context.saveState(); globalTransactions.remove(context.txId); context.cleanUp(); } catch (ResourceManagerException e) { // this is not good, but what can we do now? allCool = false; logger.logSevere("Rolling forward of " + context.txId + " failed", e); } } else if (context.status == STATUS_COMMITTED) { logger.logInfo("Cleaning already commited " + context.txId); globalTransactions.remove(context.txId); try { context.cleanUp(); } catch (ResourceManagerException e) { // this is not good, but what can we do now? allCool = false; logger.logWarning("Cleaning of " + context.txId + " failed", e); } } else { // in all other cases roll back and warn when not rollback was explicitely selected for tx if (context.status != STATUS_ROLLING_BACK && context.status != STATUS_ROLLEDBACK && context.status != STATUS_MARKED_ROLLBACK) { logger.logWarning("Irregularly rolling back " + context.txId); } else { logger.logInfo("Rolling back " + context.txId); } try { context.rollback(); context.status = STATUS_ROLLEDBACK; context.saveState(); globalTransactions.remove(context.txId); context.cleanUp(); } catch (ResourceManagerException e) { logger.logWarning("Rolling back of " + context.txId + " failed", e); } } } } return allCool; } protected void recoverContexts() { File dir = new File(workDir); File[] files = dir.listFiles(); if (files == null) return; for (int i = 0; i < files.length; i++) { File file = files[i]; Object txId = txIdMapper.getIdForPath(file.getName()); // recover all transactions we do not already know if (!globalTransactions.containsKey(txId)) { logger.logInfo("Recovering " + txId); TransactionContext context; try { context = new TransactionContext(txId); context.recoverState(); globalTransactions.put(txId, context); } catch (ResourceManagerException e) { // this is not good, but the best we get, just log as warning logger.logWarning("Recovering of " + txId + " failed"); } } } } protected boolean waitForAllTxToStop(long timeoutMSecs) { long startTime = System.currentTimeMillis(); // be sure not to lock globalTransactions for too long, as we need to give // txs the chance to complete (otherwise deadlocks are very likely to occur) // instead iterate over a copy as we can be sure no new txs will be registered // after operation level has been set to stopping Collection transactionsToStop; synchronized (globalTransactions) { transactionsToStop = new ArrayList(globalTransactions.values()); } for (Iterator it = transactionsToStop.iterator(); it.hasNext();) { long remainingTimeout = startTime - System.currentTimeMillis() + timeoutMSecs; if (remainingTimeout <= 0) { return false; } TransactionContext context = (TransactionContext) it.next(); synchronized (context) { if (!context.finished) { logger.logInfo("Waiting for tx " + context.txId + " to finish for " + remainingTimeout + " milli seconds"); } while (!context.finished && remainingTimeout > 0) { try { context.wait(remainingTimeout); } catch (InterruptedException e) { return false; } remainingTimeout = startTime - System.currentTimeMillis() + timeoutMSecs; } if (context.finished) { logger.logInfo("Tx " + context.txId + " finished"); } else { logger.logWarning("Tx " + context.txId + " failed to finish in given time"); } } } return (globalTransactions.size() == 0); } protected boolean shutdown(int mode, long timeoutMSecs) { switch (mode) { case SHUTDOWN_MODE_NORMAL: return waitForAllTxToStop(timeoutMSecs); case SHUTDOWN_MODE_ROLLBACK: return rollBackOrForward(); case SHUTDOWN_MODE_KILL: return true; default: return false; } } protected void setDirty(Object txId, Throwable t) { logger.logSevere("Fatal error during critical commit/rollback of transaction " + txId + ", setting database to dirty.", t); dirty = true; } /** * Inner class to hold the complete context, i.e. all information needed, for a transaction. * */ protected class TransactionContext { protected Object txId; protected int status = STATUS_ACTIVE; protected int isolationLevel = DEFAULT_ISOLATION_LEVEL; protected long timeoutMSecs = getDefaultTransactionTimeout(); protected long startTime; protected long commitTime = -1L; protected boolean isLightWeight = false; protected boolean readOnly = true; protected boolean finished = false; // list of streams participating in this tx private List openResources = new ArrayList(); public TransactionContext(Object txId) throws ResourceManagerException { this.txId = txId; startTime = System.currentTimeMillis(); } public long getRemainingTimeout() { long now = System.currentTimeMillis(); return (startTime - now + timeoutMSecs); } public synchronized void init() throws ResourceManagerException { String baseDir = getTransactionBaseDir(txId); String changeDir = baseDir + "/" + WORK_CHANGE_DIR; String deleteDir = baseDir + "/" + WORK_DELETE_DIR; new File(changeDir).mkdirs(); new File(deleteDir).mkdirs(); saveState(); } public synchronized void rollback() throws ResourceManagerException { closeResources(); freeLocks(); } public synchronized void commit() throws ResourceManagerException { String baseDir = getTransactionBaseDir(txId); String changeDir = baseDir + "/" + WORK_CHANGE_DIR; String deleteDir = baseDir + "/" + WORK_DELETE_DIR; closeResources(); upgradeLockToCommit(); try { applyDeletes(new File(deleteDir), new File(storeDir), new File(storeDir)); FileHelper.moveRec(new File(changeDir), new File(storeDir)); } catch (IOException e) { throw new ResourceManagerSystemException("Commit failed", ERR_SYSTEM, txId, e); } freeLocks(); commitTime = System.currentTimeMillis(); } public synchronized void notifyFinish() { finished = true; notifyAll(); } public synchronized void cleanUp() throws ResourceManagerException { if (!cleanUp) return; // XXX for debugging only boolean clean = true; Exception cleanException = null; String baseDir = getTransactionBaseDir(txId); FileHelper.removeRec(new File(baseDir)); if (!clean) { throw new ResourceManagerSystemException("Clean up failed due to unreleasable lock", ERR_SYSTEM, txId, cleanException); } } public synchronized void finalCleanUp() throws ResourceManagerException { closeResources(); freeLocks(); } public synchronized void upgradeLockToCommit() throws ResourceManagerException { for (Iterator it = lockManager.getAll(txId).iterator(); it.hasNext();) { GenericLock lock = (GenericLock) it.next(); // only upgrade if we had write access if (lock.getLockLevel(txId) == LOCK_EXCLUSIVE) { try { // in case of deadlocks, make failure of non-committing tx more likely if (!lock.acquire(txId, LOCK_COMMIT, true, true, getDefaultTransactionTimeout() * DEFAULT_COMMIT_TIMEOUT_FACTOR)) { throw new ResourceManagerException("Could not upgrade to commit lock for resource at '" + lock.getResourceId().toString() + "'", ERR_NO_LOCK, txId); } } catch (InterruptedException e) { throw new ResourceManagerSystemException(ERR_SYSTEM, txId, e); } } } } public synchronized void freeLocks() { lockManager.releaseAll(txId); } public synchronized void closeResources() { synchronized (globalOpenResources) { for (Iterator it = openResources.iterator(); it.hasNext();) { Object stream = it.next(); closeOpenResource(stream); } } } public synchronized void registerResource(Object openResource) { synchronized (globalOpenResources) { registerOpenResource(openResource); openResources.add(openResource); } } public synchronized void saveState() throws ResourceManagerException { String statePath = getTransactionBaseDir(txId) + "/" + CONTEXT_FILE; File file = new File(statePath); BufferedWriter writer = null; try { OutputStream os = new FileOutputStream(file); writer = new BufferedWriter(new OutputStreamWriter(os, DEFAULT_PARAMETER_ENCODING)); writer.write(toString()); } catch (FileNotFoundException e) { String msg = "Saving status information to '" + statePath + "' failed! Could not create file"; logger.logSevere(msg, e); throw new ResourceManagerSystemException(msg, ERR_SYSTEM, txId, e); } catch (IOException e) { String msg = "Saving status information to '" + statePath + "' failed"; logger.logSevere(msg, e); throw new ResourceManagerSystemException(msg, ERR_SYSTEM, txId, e); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { } } } } public synchronized void recoverState() throws ResourceManagerException { String statePath = getTransactionBaseDir(txId) + "/" + CONTEXT_FILE; File file = new File(statePath); BufferedReader reader = null; try { InputStream is = new FileInputStream(file); reader = new BufferedReader(new InputStreamReader(is, DEFAULT_PARAMETER_ENCODING)); txId = reader.readLine(); status = Integer.parseInt(reader.readLine()); isolationLevel = Integer.parseInt(reader.readLine()); timeoutMSecs = Long.parseLong(reader.readLine()); startTime = Long.parseLong(reader.readLine()); } catch (FileNotFoundException e) { String msg = "Recovering status information from '" + statePath + "' failed! Could not find file"; logger.logSevere(msg, e); throw new ResourceManagerSystemException(msg, ERR_SYSTEM, txId); } catch (IOException e) { String msg = "Recovering status information from '" + statePath + "' failed"; logger.logSevere(msg, e); throw new ResourceManagerSystemException(msg, ERR_SYSTEM, txId, e); } catch (Throwable t) { String msg = "Recovering status information from '" + statePath + "' failed"; logger.logSevere(msg, t); throw new ResourceManagerSystemException(msg, ERR_SYSTEM, txId, t); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { } } } } public synchronized String toString() { StringBuffer buf = new StringBuffer(); buf.append(txId).append('\n'); buf.append(Integer.toString(status)).append('\n'); buf.append(Integer.toString(isolationLevel)).append('\n'); buf.append(Long.toString(timeoutMSecs)).append('\n'); buf.append(Long.toString(startTime)).append('\n'); if (debug) { buf.append("----- Lock Debug Info -----\n"); for (Iterator it = lockManager.getAll(txId).iterator(); it.hasNext();) { GenericLock lock = (GenericLock) it.next(); buf.append(lock.toString() + "\n"); } } return buf.toString(); } } private class InputStreamWrapper extends InputStream { private InputStream is; private Object txId; private Object resourceId; public InputStreamWrapper(InputStream is, Object txId, Object resourceId) { this.is = is; this.txId = txId; this.resourceId = resourceId; } public int read() throws IOException { return is.read(); } public int read(byte b[]) throws IOException { return is.read(b); } public int read(byte b[], int off, int len) throws IOException { return is.read(b, off, len); } public int available() throws IOException { return is.available(); } public void close() throws IOException { try { is.close(); } finally { TransactionContext context; synchronized (globalTransactions) { context = getContext(txId); if (context == null) { return; } } synchronized (context) { if (context.isLightWeight) { if (logger.isFinerEnabled()) logger.logFiner("Upon close of resource removing temporary light weight tx " + txId); context.freeLocks(); globalTransactions.remove(txId); } else { // release access lock in order to allow other transactions to commit if (lockManager.getLevel(txId, resourceId) == LOCK_ACCESS) { if (logger.isFinerEnabled()) { logger.logFiner("Upon close of resource releasing access lock for tx " + txId + " on resource at " + resourceId); } lockManager.release(txId, resourceId); } } } } } public void mark(int readlimit) { is.mark(readlimit); } public void reset() throws IOException { is.reset(); } public boolean markSupported() { return is.markSupported(); } } }