The HSQLDB LockFile.java source code
/* Copyright (c) 2001-2008, 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.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import org.hsqldb.DatabaseManager;
import org.hsqldb.HsqlDateTime;
import org.hsqldb.HsqlException;
import org.hsqldb.Trace;
import org.hsqldb.lib.FileUtil;
import org.hsqldb.lib.HsqlTimer;
import org.hsqldb.lib.StringConverter;
/**
* Base cooperative file locking implementation and <tt>LockFile
* factory. <p>
*
* <hr>
*
* Provides a facility for cooperative file locking across process boundaries
* and isolated in-process class loader contexts. <p>
*
* The need is obvious for inter-process cases, but it is no less essential for
* in-process Java clients whose classes have been loaded by isolated class
* loaders. This is because static fields--the conventional<a href="#note1">
* <sup>* means for supporting in-process global discovery--become
* distinct and inaccessible across Java class loader context boundaries when
* the contexts do not share a common parent class loader or do not implement
* normal parent class loader delegation semantics. <p>
*
* <a name="note1">*
* The only purely in-process global discovery alternative known to the author
* is to reflect upon objects found while traversing up the Java runtime thread
* hierarchy. However, this method is often subject to Java security
* restrictions whose collective purpose is essentially dissimilar to that of
* restrictions in effect relative to cooperative file locking requirements,
* making it a generally unacceptable option in this context. <p>
*
* <hr>
*
* Here is the way this class presently operates: <p>
*
* <ol style="list-style-type: upper-latin">
* <li>A file with a commonly agreed-upon path is used to implement
* cooperative locking semantics regarding another set of files with
* commonly agreed-upon paths.<p>
*
* <li>In particular, a background thread periodically writes a timestamp
* value, which acts as a heartbeat that indicates to others whether a
* cooperative lock condition is currently held.<p>
*
* <li>In addition, a magic value is written so that it is possible to
* distinguish with a reasonably high degree of accuracy between the
* existence of a lock file and some other type of file.<p>
*
* <li>The generic rules used to aquire a cooperative lock condition are
* as follows:<p>
*
* <ol>
* <li>If a lock condition is already held by this object, do nothing and
* signify that the lock attempt was successful, else...<p>
*
* <li>Poll the underlying file, using a configured maximum number of
* retries and a configured interval between the end of a failed
* poll and the beginning of the next.<p>
*
* <li>For each poll:
*
* <ol style="list-style-type: lower-roman">
*
* <li>Attempt to atomically create the underlying file if and only
* if it does not yet exist, exit the polling loop immediately
* indicating success if the attempt succeeds, else fast fail
* the current poll if a security exeption is thrown in response
* to the attempt, else...<p>
*
* <li>Test if the underlying file exists, fast failing the current
* poll if it is impossible to determine (i.e. if a security
* exeption is thrown).<p>
*
* <li>If the file does not exist, exit the polling loop immediately
* indicating success.<p>
*
* This can occur only under pre-JDK 1.2 runtimes; or when the
* underlying platform does not correctly support {@link
* java.io.File#createNewFile()}; or when the underlying file is
* deleted within a very short time after i.), above (typically
* on the order of microseconds). <p>
*
* If the underlying platform employs a kernel-enforced mandatory
* file locking blanket policy for open files (e.g. <em>Windows
* </em>tm), then this is likely a non-issue. And if
* this case makes possible a race condition with another
* <tt>LockFile object (because the test for existence and
* subsequent file creation is not atomic relative to all other
* file system actions), it is still <em>very unlikely that
* so unfortunate a timing will occur as to allow simultaneous
* lock conditions to be established. Finally, if some
* non-<tt>LockFile entity deleted the file, then there are
* much worse things to worry about, in particular that the files
* this object is supposed to protect are in reality subject to
* arbitrary external modification and deletion.<p>
*
* <li>Test the file's length, fast failing the current poll if the
* length cannot be determined or it is not the expected
* value.<p>
*
* <li>Open a stream to read the file's MAGIC and heartbeat
* timestamp values, fast failing the current poll if the stream
* cannot be opened.<p>
*
* <li>Test the file's MAGIC value, failing the current poll
* if the value cannot be read or it is not the expected
* value.<p>
*
* <li>Test the file's heartbeat timestamp value, fast failing the
* current poll if it cannot be read or it is less than a
* commonly agreed-upon value into the past (or future, to
* overcome a caveat observed by a patch contributor).<p>
* </ol>
* <li>If the polling phase exits with a failure indication, then one or
* more of the following cases must have been true at every poll
* iteration: <p>
*
* <ul>
* <li>The file had the wrong length or MAGIC value (was
* not an HSQLDB lock file).
*
* <li>The file was deleted externally after a poll's initial
* test for existence and recreated at some point before
* the next poll's initial test for existence.
*
* <li>An incompatible OS-enforced security restriction was in
* effect.
*
* <li>An incompatible Java-enforced security restriction was
* in effect.
*
* <li>The target file system media was effectively inaccessible.
* <li>A cooperative lock condition was held by some other
* <tt>LockFile.
*
* <li>A kernel-enforced manditory or advisory file lock was held.
* </ul>
*
* In this case, signify failure indicating the last encountered
* reason, else...<p>
*
* <li>Open the file for reading and writing, write the magic value and
* an initial heartbeat timestamp, schedule a periodic heartbeat
* timestamp writer task and signify success.<p>
* </ol>
* <li>The generic rules used to release a cooperative lock condition are:
* <ol>
* <li>If a lock condition is not currently held, do nothing and signify
* success, else...<p>
*
* <li>A lock condition is currently held by this object, so try to
* release it. <p>
*
* By default, releasing the lock condition consists of closing and
* nullifying any objects that have a file descriptor open on the
* lock file, cancelling the periodic heartbeat timestamp writer
* task and deleting the lock file. If the release occurs without
* raising an exception, signify success, else signify that the
* release attempt <em>might have failed.
* </ol>
* </ol>
*
* <hr>
*
* Additionally, {@link #doOptionalLockActions() doOptionalLockActions()} and
* {@link #doOptionalReleaseActions() doOptionalReleaseActions()} are invoked
* during lock and release attempts, respectively. This enables integration of
* extended lock and release strategies based on subclassing. Subclass
* availability is automatically detected and exposed by the factory method
* {@link #newLockFile newLockFile()}.<p>
*
* In particular, if {@link #USE_NIO_FILELOCK_PROPERTY} is true and the required
* classes are available at static initialization, then <tt>newLockFile()
* produces {@link org.hsqldb.persist.NIOLockFile NIOLockFile} instances.<p>
*
* When <tt>NIOLockFile instances are produced, then it is possible that
* true kernel-enforced advisory or manditory file locking is used to protect
* the underlying lock file from inadvertent modification (and possibly even
* from deletion, including deletion by the system superuser).
*
* Otherwise, <tt>newLockFile() produces vanilla LockFile
* instances, which exhibit just the elementary cooperative locking behavior on
* platforms that do not, by default, implement kernel-enforced manditory
* locking for open files. <p>
*
* At this point, it must be noted that not every target platform upon which
* Java can run actually provides true kernel-enforced manditory (or even
* advisory) file locking. Indeed, even when a target platform <em>does
* provide such locking guarantees for local file systems, it may not be able
* to do so for network file systems, or it may only be able to do so safely
* (or at all) with certain restrictions. Further, external system configuration
* may be a prerequisite to enable manditory locking on systems that support it
* but employ advisory locking by default. <p>
*
* In recognition of these facts, the official Java NIO package specification
* explicitly states basically the same information. What is unfortunate,
* however, is that no capabilities API is yet provided as part of the package.
* What is even more unfortunate is that without something like a capabilities
* API, it is impossible for an implementation to indicate or clients to
* distiguish between simple lack of platform support and cases involving
* immature Java runtimes that do not fully or correctly implement all NIO
* features (and hence may throw exceptions at unexpected times or in places
* where the API specification indicates none can be thrown).<p>
*
* It is for the preceding reasons that, as of HSQLDB 1.8.0.3,
* <tt>FileLock's use of Java NIO has been made a purely optional feature.
* Previous to HSQLDB 1.8.0.3, if NIO was detected available, used to create a
* <tt>FileLock and failed, then the enclosing cooperative lock attempt
* failed also, despite the fact that a vanilla locking approach could
* succeed. <p>
*
* <b>Polling Configuration:
*
* Although the {@link #HEARTBEAT_INTERVAL} and default polling values may
* seem quite conservative, they are the result of ongoing research into
* generally reasonable concerns regarding normal timing and resource
* availability fluctuations experienced frequently under most, if not all
* operating systems. <p>
*
* Regardless, flexibility is almost always a good thing, so this class is
* designed to allow polling interval and retry count values to be configured
* at run-time. <p>
*
* At present, this can be done at any time by setting the system properties
* whose names are {@link #POLL_RETRIES_PROPERTY} and {@link
* #POLL_INTERVAL_PROPERTY}. <p>
*
* Some consideration has also been given to modifying the polling scheme so
* that run-time configuration of the HEARTBEAT_INTERVAL is possible. For now,
* however, this option has been rejected due to the relative complexity of
* guaranteeing acceptably safe, deterministic behaviour. On the other hand,
* if it can be guaranteed that certain site invariants hold (in particular,
* that only one version of the hsqldb jar will ever be used to open database
* instances at the site) and it is desirable or required to experiment with
* a lower interval, then it is recommended for now simply to recompile the
* jar using a different value in the static field assignment. Note that great
* care should be taken to avoid assigning too low a value, or else it may
* become possible that even very short-lived timing and resource availability
* fluctuations will cause incorrect operation of this class. <p>
*
* <b>NIO Configuration:
*
* Starting with 1.8.0.3, NIO-enhanced file lock attempts are turned off by
* default. The general reasons for this are discussed above. Anyone interested
* in the reading the detailed research notes should refer to the overview of
* {@link NIOLockFile}. If, after reviewing the notes and the capabilities of
* the intended target platform, one should still wish to enable NIO-enhanced
* file lock attempts, it can be done by setting the system property {@link
* #USE_NIO_FILELOCK_PROPERTY} true at JVM startup (for example, by using a
* command-line <tt>-D<property-name>=true directive). Be aware that
* the system property value is read only once, in the static initializer block
* for this class. <p>
*
* <b>Design Notes:
*
* First, it should be noted that no thread synchronization occurs in
* this class. Primarily, this is because the standard entry point,
* {@link #newLockFileLock(String)}, is always called from within a block
* synchronized upon an HSQLDB Database instance. If this class is to be used
* elsewhere and it could be accessed concurrently, then access should be
* synchronized on an appropriate monitor. That said, certain members of this
* class have been declared volatile to minimize possibility of inconsistent
* views under concurrent read-only access. <p>
*
* Second, to the limit of the author's present understanding, the
* implementation details of this class represent a good comprimse under varying
* and generally uncontrollable JVM, OS and hardware platform
* limitations/capabilites, as well as under usability considerations and
* external security or operating constraints that may need to be imposed.<p>
*
* Alternate approaches that have been considered and rejected for now
* include: <p>
*
* <ul>
* <li>Socket-based locks (with/without broadcast protocol)
* <li>Pure NIO locking
* <li>Simple lock file (no heartbeat or polling)
* <li>JNI and native configuration alternatives
* </ul>
*
* Of course, discussions involving and patches implementing improvements
* or better alternatives are always welcome. <p>
*
* As a final note and sign post for developers starting to work with
* Java NIO: <p>
*
* A separate <tt>NIOLockFile descendent exists specifically
* because it was determined though experimenatation that
* <tt>java.nio.channels.FileLock does not always exhibit the correct
* or desired behaviour under reflective method invocation. That is, it was
* discovered that under some operating system/JVM combinations, after calling
* <tt>FileLock.release() via a reflective method invocation, the lock is
* not released properly, deletion of the lock file is not possible even from
* the owning object (this) and it is impossible for other <tt>LockFile
* instances, other in-process objects or other processes to successfully obtain
* a lock condition on the lock file, despite the fact that the
* <tt>FileLock object reports that its lock is invalid (was released
* successfully). Frustratingly, this condition appears to persist until full
* exit of the process hosting the JVM in which the <tt>FileLock.tryLock()
* method was reflectively invoked. <p>
*
* To solve this, the original <tt>LockFile class was split in two and
* instead of reflective method invocation, subclass instantiation is now
* performed at the level of the <tt>newLockFile() factory method.
* Similarly, the HSQLDB ANT build script now detects the presence or abscence
* of JDK 1.4+ features such as java.nio and only attempts to build and deploy
* <tt>NIOLockFile to the hsqldb.jar if such features are reported
* present. <p>
*
* @author Campbell Boucher-Burnett (boucherb@users dot sourceforge.net)
* @version 1.8.1
* @since 1.7.2
*/
public class LockFile {
/**
* Arbitary period, in milliseconds, at which heartbeat timestamps are
* written to this object's lock file. <p>
*
* This value was selected to be very conservative, just in case timing
* jitters are experienced on the order introduced by brief network
* partitions, accidentally removed media and transient high load
* CPU bursts.
*/
public static final long HEARTBEAT_INTERVAL = 10000;
/**
* {@link #HEARTBEAT_INTERVAL} + 100. <p>
*
* Interval used by {@link #checkHeartbeat(boolean) checkHeartbeat} to
* test whether the timestamp in the underlying lock file is live or stale.
* Padding added in the hope of reducing potential timing jitter issues
* under the polling scheme introduced in 1.8.0.3
*/
public static final long HEARTBEAT_INTERVAL_PADDED = 10100;
/**
* Value written at the beginning of an HSQLDB lock file to distinguish it
* from other file types. <p>
*
* The value is the octet sequence: {0x48, 0x53, 0x51, 0x4c, 0x4c, 0x4f,
* 0x43, 0x4b}, which is the ASCII sequence {'H', 'S', 'Q', 'L', 'L', 'O',
* 'C', 'K'}. <p>
*
* <b>Design Note:
*
* "HSQLLOCK".getBytes() is no longer used because it is dependent on the
* underlying platform's default character set.
*/
protected static final byte[] MAGIC = {
0x48, 0x53, 0x51, 0x4c, 0x4c, 0x4f, 0x43, 0x4b
};
/**
* Size, in bytes, of the region at the beginning of a lock file that is
* actually used to record lock information. <p>
*
* Value is currently MAGIC.length + sizeof(long) = (8 + 8) = 16
*/
public static final int USED_REGION = 16;
/**
* Number of retries used by default in {@link #pollHeartbeat()
* pollHeartbeat}.
*/
public static final int POLL_RETRIES_DEFAULT = 10;
/**
* System property that can be used to override the default number of
* heartbeat poll retries.
*/
public static final String POLL_RETRIES_PROPERTY =
"hsqldb.lockfile.poll.retries";
/**
* System property that can be used to override the default number of
* milliseconds between each heartbeat poll retry.
*/
public static final String POLL_INTERVAL_PROPERTY =
"hsqldb.lockfile.poll.interval";
/** Whether <tt>java.nio file locking is attempted by default. */
public static final boolean USE_NIO_FILELOCK_DEFAULT = false;
/**
* System property that can be used to control whether nio file locking is
* attempted.
*/
public static final String USE_NIO_FILELOCK_PROPERTY =
"hsqldb.lockfile.nio.filelock";
/**
* Statically computed indication of <tt>java.nio.channels.FileLock
* runtime availability. <p>
*
* <b>Design Note:
*
* Computed in a static initializer block. Will be <tt>false if
* <tt>USE_NIO_FILELOCK_PROPERTY is false at static
* initialization, regardless of actual availability.
*/
public static final boolean NIO_FILELOCK_AVAILABLE;
/**
* Statically computed reference to the <tt>NIOLockFile class.
*
* <b>Design Note:
*
* Computed in a static initializer block. Will be <tt>null if
* <tt>USE_NIO_FILELOCK_PROPERTY is false at static
* initialization, regardless of actual availability.
*/
public static final Class NIO_LOCKFILE_CLASS;
/**
* The timed scheduler with which to register this object's
* heartbeat task.
*/
protected static final HsqlTimer timer = DatabaseManager.getTimer();
// This static initializer comes last, since it references a subclass
//
// That is, it is best practice to ensure the static fields of this class
// are all initialized before referecing a subclass whose static
// field initializtion may in turn reference static fields in this class.
static {
synchronized (LockFile.class) {
boolean use = USE_NIO_FILELOCK_DEFAULT;
try {
use = "true".equalsIgnoreCase(
System.getProperty(USE_NIO_FILELOCK_PROPERTY, use ? "true"
: "false"));
} catch (Exception e) {}
boolean avail = false;
Class clazz = null;
if (use) {
try {
Class.forName("java.nio.channels.FileLock");
clazz = Class.forName("org.hsqldb.persist.NIOLockFile");
avail = true;
} catch (Exception e) {}
}
NIO_FILELOCK_AVAILABLE = avail;
NIO_LOCKFILE_CLASS = clazz;
}
}
/**
* Canonical reference to this object's lock file. <p>
*
* <b>Design Note:
*
* Should really be final, but finality makes reflective construction
* and adherence to desirable <tt>LockFile factory method event
* sequence more complicated.
*/
protected File file;
/**
* Cached value of the lock file's canonical path
*
* <b>Design Note:
*
* Should really be final, but finality makes reflective construction
* and adherence to desirable <tt>LockFile factory method event
* sequence much more complicated.
*/
private String cpath;
/**
* A <tt>RandomAccessFile constructed from this object's canonical file
* reference. <p>
*
* This <tt>RandomAccessFile is used to periodically write out the
* heartbeat timestamp to this object's lock file.
*/
protected volatile RandomAccessFile raf;
/** Indicates presence or absence of the cooperative lock condition. */
protected volatile boolean locked;
/** Opaque reference to this object's heatbeat task. */
private volatile Object timerTask;
/**
* Retrieves a new <tt>NIOLockFile, or null if not available
* under the current runtime environment.
*
* @return a new <tt>NIOLockFile, or null if not available
* under the current runtime environment
*/
private static final LockFile newNIOLockFile() {
if (NIO_FILELOCK_AVAILABLE && NIO_LOCKFILE_CLASS != null) {
try {
return (LockFile) NIO_LOCKFILE_CLASS.newInstance();
} catch (Exception e) {
// e.printStackTrace()
}
}
return null;
}
/**
* To allow subclassing without exposing a public constructor.
*/
protected LockFile() {}
/**
* Retrieves a <tt>LockFile instance, initialized with a File
* object whose path is the canonical form of the one specified by the
* given <tt>path argument.
*
* The resulting <tt>LockFile instance does not yet hold a lock
* condition on the file with the given path, nor does it guarantee that the
* file pre-exists or is created.
*
* However, upon successful execution, it is guaranteed that all required
* parent directories have been created and that the underlying platform has
* verified the specified path is legal on the file system of the underlying
* storage partition.
*
* @return a <tt>LockFile instance initialized with a File
* object whose path is the one specified by the given <tt>path
* argument.
* @param path the path of the <tt>File object with which the retrieved
* <tt>LockFile object is to be initialized
* @throws FileCanonicalizationException if an I/O error occurs upon
* canonicalization of the given path, which is possible because
* it may be illegal on the runtime file system or because
* construction of the canonical path name may require native file
* system queries
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a security manager exists and its <tt>{@link
* java.lang.SecurityManager#checkRead}</tt> method denies read
* access to the file; or if its <tt>{@link
* java.lang.SecurityManager#checkRead(java.lang.String)}</tt>
* method does not permit verification of the existence of all
* necessary parent directories; or if the <tt>{@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}</tt>
* method does not permit all necessary parent directories to be
* created
*/
public static final LockFile newLockFile(final String path)
throws FileCanonicalizationException, FileSecurityException {
LockFile lockFile = newNIOLockFile();
if (lockFile == null) {
lockFile = new LockFile();
}
lockFile.setPath(path);
return lockFile;
}
/**
* {@link org.hsqldb.persist.Logger#acquireLock(java.lang.String)}
* delegate.<p>
*
* Retrieves a new <tt>LockFile object holding a cooperative lock
* condition upon the file with the given path, appended with the
* extension '.lck'. <p>
*
* @param path of the lock file, to which will be appended '.lck'
* @throws org.hsqldb.HsqlException if the lock condition cannot
* be obtained for any reason.
* @return a new <tt>LockFile object holding a cooperative lock
* condition upon the file with the given path, appended with the
* extension '.lck'
*/
public static final LockFile newLockFileLock(final String path)
throws HsqlException {
LockFile lockFile = null;
try {
lockFile = LockFile.newLockFile(path + ".lck");
} catch (LockFile.BaseException e) {
throw Trace.error(Trace.LOCK_FILE_ACQUISITION_FAILURE,
e.getMessage());
}
boolean locked = false;
try {
locked = lockFile.tryLock();
} catch (LockFile.BaseException e) {
throw Trace.error(Trace.LOCK_FILE_ACQUISITION_FAILURE,
e.getMessage());
}
// Paranoia mode: In theory, this case can't happen, given the way
// tryLock now works; by all current understanding of the involved API
// contracts, an exception will always be thrown instead by the code
// above.
if (!locked) {
throw Trace.error(Trace.LOCK_FILE_ACQUISITION_FAILURE,
lockFile.toString());
}
return lockFile;
}
/**
* Checks whether the underlying file is an HSQLDB lock file and, if so,
* whether its heartbeat timestamp is live (is, as far as can be known,
* presumably in use by another <tt>LockFile instance) or stale.
*
* The check conforms to the following rules: <p>
*
* <ol>
* <li>If the parameter withCreateNewFile is true, {@link
* java.io.File#createNewFile()} is available and its invocation
* upon this object's <tt>file object indicates the underlying
* file was atomically created if and only if it did not yet exist,
* then return immediately (we have won the <em>race to establish
* a lock file). <p>
*
* <li>Test again if the file exists, returning immediately if it does not
* (there's no file and hence no heartbeat to check). <p>
*
* An immediate return can occur here only under pre-JDK 1.2 runtimes;
* or when the underlying platform does not correctly support
* <tt>File.createNewFile(); or when the underlying file is deleted
* within a very short time after i.), above (typically on the order of
* microseconds). <p>
*
* If the underlying platform employs a kernel-enforced mandatory file
* locking blanket policy for open files (e.g. <em>Windowstm
* </sup>), then this is likely a non-issue. And if this case makes
* possible a race condition with another <tt>LockFile object
* (because the test for existence yeilds false and subsequent file
* creation is not atomic relative to all other file system actions), it
* is still <em>very unlikely that so unfortunate a timing will
* occur as to allow simultaneous lock conditions to be established.
* Finally, if some non-<tt>LockFile entity deleted the file, then
* there are much worse things to worry about, in particular that the
* files this object is supposed to protect are in reality subject to
* arbitrary external modification and deletion by some uncooperative
* process.<p>
*
* <li>If a Java security exception is thrown while testing for existence,
* it is rethrown as a <tt>FileSecurityException.
*
* <li>Read the file's length.
*
* <li>If a Java security exception is thrown reading length, it is rethrown
* as a <tt>FileSecurityException (it is possible somebody
* concurrently refreshed the system Policy in the interim).
*
* <li>If the file does not have the expected length, a
* <tt>WrongLengthException is thrown (we're trying to check
* something that is not an HSQLDB lock file).
*
* <li>Open an input steam to read the file's MAGIC and heartbeat
* timestamp values.
*
* <li>If a file not found exception is thrown above, it is rethrown as an
* <tt>UnexpectedFileNotFoundException (we've already tested for
* existence).
*
* <li>If a Java security exception is thrown above, it is rethrown as a
* <tt>FileSecurityException (it is possible somebody
* concurrently refreshed the system Policy in the interim).
*
* <li>Read the MAGIC value.
*
* <li>If an end of file exepction is thrown above, it is rethrown as an
* <tt>UnexpectedEndOfFileException (we've already tested the
* length... did someone truncate the file in the interim?).
*
* <li>If an I/O exception is thrown, it is rethrown as an
* <tt>UnexpectedFileIOException (we've already tested for
* existence, length and successfully opened a stream...did someone,
* for example, force unmount or physically remove the underlying device
* in the interim?)
*
* <li>If the value read in does not match the expected MAGIC value,
* a <tt>WrongMagicException is thrown (we're trying to check
* something that is not an HSQLDB lock file).
*
* <li>Read the heartbeat timestamp.
*
* <li>If a Java security exception is thrown above, it is rethrown as a
* <tt>FileSecurityException (it is possible somebody
* concurrently refreshed the system Policy in the interim).
*
* <li>If an end of file exection is thrown above, it is rethrown as an
* <tt>UnexpectedEndOfFileException (we've already tested the
* length... did someone truncate the file in the interim?).
*
* <li>If an I/O exception is thrown, it is rethrown as an
* <tt>UnexpectedFileIOException (we've already tested for
* existence, length and successfully opened a stream...did someone,
* for example, force unmount or physically remove the underlying device
* in the interim?)
*
* <li>If the timestamp read in is less than or equal to
* {@link #HEARTBEAT_INTERVAL_PADDED} milliseconds into the past or
* future, then a <tt>LockHeldExternallyException is thrown.
*
* <li>Otherwise, this method simply returns.
* </ol>
*
* @param withCreateNewFile if <tt>true, attempt to employ
* <tt>File.createNewFile() as part of the check so as to
* eliminate potential race conditions when establising a new
* lock file
* @throws FileSecurityException if the check fails due to a Java
* security permission check failure
* @throws LockHeldExternallyException if it is determined that the
* file's heartbeat timestamp is less than
* <tt>HEARTBEAT_INTERVAL_PADDED into the past (or future)
* @throws UnexpectedEndOfFileException if an <tt>EOFExceoption is
* thrown while reading either the magic or heartbeat timestamp values
* @throws UnexpectedFileIOException if an <tt>IOException other than
* <tt>EOFException is thrown while reading either the magic or
* heartbeat timestamp values
* @throws UnexpectedFileNotFoundException if a
* <tt>FileNotFoundException is thrown while attempting to open a
* stream to read the underlying file's magic and heartbeat timestamp
* values
* @throws WrongLengthException if it is determined that the length
* of the file does not equal {@link #USED_REGION}
* @throws WrongMagicException if it is determined that the file's
* content does not start with {@link #MAGIC}.
*/
private final void checkHeartbeat(boolean withCreateNewFile)
throws LockFile.FileSecurityException,
LockFile.LockHeldExternallyException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException,
LockFile.UnexpectedFileNotFoundException,
LockFile.WrongLengthException, LockFile.WrongMagicException {
long now;
long lastHeartbeat;
long length = 0;
//#ifdef JAVA2FULL
try {
if (withCreateNewFile) {
try {
if (file.createNewFile()) {
return;
}
} catch (IOException ioe) {}
}
if (!file.exists()) {
return;
}
length = file.length();
} catch (SecurityException se) {
throw new FileSecurityException(this, "checkHeartbeat", se);
}
//#else
/*
if (!file.exists()) {
if (withCreateNewFile) {
openRAF();
closeRAF();
}
return;
}
length = file.length();
*/
//#endif JAVA2
if (length != USED_REGION) {
throw new WrongLengthException(this, "checkHeartbeat", length);
}
// Compute the current wall clock time *first* to reduce possibility
// of unwanted time dilation effects introduced, for example,
// by intervening thread or process context switches under CPU
// bursts.
//
// Example:
//
// Say currentTimeMillis is actually somewhere in (-0.5 and 0.5]
// and another LockFile concurrently writes a 0-valued heartbeat
// timestamp.
//
// Then, if readHeartbeat comes first here, happens to 'win the race
// condition' (reads the previous heartbeat: -10,000) and an intervening
// switch causes greater than ~0.5 millisecond elapsed time to
// be experienced between readHeartbeat and currentTimeMillis, then
// currentTimeMillis will be computed as n (n > 0), and (now -
// lastHearbeat) will be HEARTBEAT_INTERVAL + n, instead of
// HEARTBEAT_INTERVAL.
//
// Now, let n be greater than (HEARTBEAT_INTERVAL_PADDED -
// HEARTBEAT_INTERVAL).
//
// Then the check will succeed, although it should fail.
//
// On the other hand, if currentTimeMillis is computed first, the
// worst than can happen is a false positive indication that
// the read heartbeat timestamp value was written by a live LockFile
// instance.
//
now = System.currentTimeMillis();
lastHeartbeat = readHeartbeat();
// Using padded interval to further reduce corner case effects,
// now that heartbeat polling is in effect.
//
// Basically, it is absolutely essential to fail when a lock really is
// still held elsewhere, so it is OK to fail on corner cases where
// the last written heartbeat is very close to HEARTBEAT_INTERVAL
// in the past and it is possible that timing jitters make it uncertain
// whether the lock really is still held.
if (Math.abs(now - lastHeartbeat) <= (HEARTBEAT_INTERVAL_PADDED)) {
throw new LockHeldExternallyException(this, "checkHeartbeat", now,
lastHeartbeat);
}
}
/**
* Closes this object's {@link #raf RandomAccessFile}. <p>
*
* As a side-effect, the associated <tt>FileChannel object, if any,
* is closed as well.
*
* @throws UnexpectedFileIOException if an <tt>IOException is thrown
*/
private final void closeRAF() throws LockFile.UnexpectedFileIOException {
if (raf != null) {
try {
raf.close();
} catch (IOException ex) {
throw new UnexpectedFileIOException(this, "closeRAF", ex);
} finally {
raf = null;
}
}
}
/**
* Provides any optional locking actions for the {@link #tryLock()
* tryLock()} template method. <p>
*
* Descendents are free to provide additional functionality here,
* using the following rules: <p>
*
* <b>PRE:
*
* This method is called only from <tt>tryLock() and it is called if
* and only if <tt>tryLock() successfully invokes
* <tt>pollHeartbeat() and openRAF() first.
*
* From this, it can be inferred that upon entry: <p>
*
* <ol>
* <li>locked == false.
* <li>raf is a non-null instance that can be used to get a
* <tt>FileChannel instance, if desired.
* <li>the underlying file either did not exist before invoking
* <tt>openRAF() or it was a valid but stale HSQLDB lock file
* because it:
*
* <ol style="list-style-type: lower-roman">
* <li>did exist,
* <li>was readable on USED_REGION,
* <li>had the expected length and MAGIC value and
* <li>had a stale heartbeat timestamp value.
* </ol>
* </ol>
*
* Further, it can be assumed that this object's heatbeat task is definitely
* cancelled and/or has never been scheduled at this point, so whatever
* timestamp is recorded in the lock file, if it did pre-exist, was written
* by a different <tt>LockFile instance or as the result of a previous,
* successful <tt>tryLock() invocation upon this LockFile
* instance. <p>
*
* Finally, it is important that this method does not rethrow any exceptions
* it encounters as unchecked exceptions to the calling context. <p>
*
* <b>POST:
*
* This method should return <tt>false if optional locking work is not
* performed or if it fails, else <tt>true.
*
* In general, if optional locking work fails, then any resources
* acquired in the process should be freed before this method returns.
* In this way, the surrounding implementation can take advantage of a
* <tt>false return value to avoid calling {@link
* #doOptionalReleaseActions() doOptionalReleaseActions()} as part of the
* {@link #tryRelease() tryRelease()} method. <p>
*
* <b>Note:
*
* The default implementation does nothing and always returns
* <tt>false.
*
* @return <tt>true if optional lock actions are performed and they
* succeed, else <tt>false
*/
protected boolean doOptionalLockActions() {
return false;
}
/**
* Provides any optional release actions for the {@link #tryRelease()
* tryRelease()} template method. <p>
*
* <b>PRE:
*
* It is important that this method does not rethrow any exceptions
* it encounters as unchecked exceptions to the calling context. <p>
*
* <b>POST:
*
* In general, <tt>false should be returned if optional locking work
* is not performed or if it fails, else <tt>true. However, the return
* value is currenly treated as purely informative. <p>
*
* <b>Note:
*
* The default implementation does nothing and always returns false. <p>
*
* @return <tt>true if optional release actions are performed and they
* succeed, else <tt>false
*/
protected boolean doOptionalReleaseActions() {
return false;
}
/**
* Initializes this object with a <tt>File object whose path has the
* canonical form of the given <tt>path argument.
*
* <b>PRE:
*
* <ol>
* <li>This method is called once and only once per
* <tt>Lockfile instance.
*
* <li>It is always the first method called after
* <tt>LockFile construction
*
* <li>The supplied path argument is never
* <tt>null.
* </ol>
*
* @param path the abstract path representing the file this object is to
* use as its lock file
* @throws FileCanonicalizationException if an I/O error occurs upon
* canonicalization of the given path, which is possible because
* the given path may be illegal on the runtime file system or
* because construction of the canonical pathname may require
* native file system queries
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* <tt>{@link java.lang.SecurityManager#checkRead} method denies
* read access to the file; or if its <tt>{@link
* java.lang.SecurityManager#checkRead(java.lang.String)}</tt>
* method does not permit verification of the existence of
* all necessary parent directories; or if
* its <tt>{@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}</tt>
* method does not permit all necessary parent directories to be
* created
*/
private final void setPath(String path)
throws LockFile.FileCanonicalizationException,
LockFile.FileSecurityException {
// Should at least be absolutized for reporting purposes, just in case
// a security or canonicalization exception gets thrown.
path = FileUtil.getDefaultInstance().canonicalOrAbsolutePath(path);
this.file = new File(path);
try {
FileUtil.getDefaultInstance().makeParentDirectories(this.file);
} catch (SecurityException ex) {
throw new FileSecurityException(this, "setPath", ex);
}
try {
this.file = FileUtil.getDefaultInstance().canonicalFile(path);
} catch (SecurityException ex) {
throw new FileSecurityException(this, "setPath", ex);
} catch (IOException ex) {
throw new FileCanonicalizationException(this, "setPath", ex);
}
this.cpath = this.file.getPath();
}
/**
* Opens (constructs) this object's {@link #raf RandomAccessFile}. <p>
*
* @throws UnexpectedFileNotFoundException if a
* <tt>FileNotFoundException is thrown in reponse to
* constructing the <tt>RandomAccessFile object.
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* <tt>{@link java.lang.SecurityManager#checkRead} method
* denies read access to the file; or if its <tt>{@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}</tt>
* method denies write access to the file
*/
private final void openRAF()
throws LockFile.UnexpectedFileNotFoundException,
LockFile.FileSecurityException, LockFile.UnexpectedFileIOException {
try {
raf = new RandomAccessFile(file, "rw");
} catch (SecurityException ex) {
throw new FileSecurityException(this, "openRAF", ex);
} catch (FileNotFoundException ex) {
throw new UnexpectedFileNotFoundException(this, "openRAF", ex);
} catch (IOException ex) {
throw new UnexpectedFileIOException(this, "openRAF", ex);
}
}
/**
* Checks whether the given <tt>DataInputStream contains the
* {@link #MAGIC} value.
*
* @param dis the stream to check
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* <tt>{@link java.lang.SecurityManager#checkRead} method
* denies read access to the file
* @throws UnexpectedEndOfFileException if an <tt>EOFException is
* thrown while reading the <tt>DataInputStream
* @throws UnexpectedFileIOException if an <tt>IOException other than
* <tt>EOFException is thrown while reading the
* <tt>DataInputStream
* @throws WrongMagicException if a value other than <tt>MAGIC is read
* from the <tt>DataInputStream
*/
private final void checkMagic(final DataInputStream dis)
throws LockFile.FileSecurityException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException, LockFile.WrongMagicException {
boolean success = true;
final byte[] magic = new byte[MAGIC.length];
try {
for (int i = 0; i < MAGIC.length; i++) {
magic[i] = dis.readByte();
if (MAGIC[i] != magic[i]) {
success = false;
}
}
} catch (SecurityException ex) {
throw new FileSecurityException(this, "checkMagic", ex);
} catch (EOFException ex) {
throw new UnexpectedEndOfFileException(this, "checkMagic", ex);
} catch (IOException ex) {
throw new UnexpectedFileIOException(this, "checkMagic", ex);
}
if (!success) {
throw new WrongMagicException(this, "checkMagic", magic);
}
}
/**
* Retrieves the last written hearbeat timestamp from this object's lock
* file. If this object's lock file does not exist, then <tt>Long.MIN_VALUE
* </tt> (the earliest time representable as a long in Java) is
* returned immediately. <p>
*
* @return the hearbeat timestamp read from this object's lock file,
* as a <tt>long value or, if this object's lock
* file does not exist, <tt>Long.MIN_VALUE, the earliest time
* representable as a <tt>long in Java.
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* <tt>{@link java.lang.SecurityManager#checkRead} method
* denies read access to the file
* @throws UnexpectedEndOfFileException if an <tt>EOFException is
* thrown while attempting to read the target file's <tt>MAGIC
* or heartbeat timestamp value
* @throws UnexpectedFileNotFoundException if, after successfully testing
* for existence, the target file is not found a moment later while
* attempting to read its <tt>MAGIC and heartbeat timestamp
* values
* @throws UnexpectedFileIOException if any other input stream error occurs
* @throws WrongMagicException if the lock file does not start with the
* the {@link #MAGIC} value
*/
private final long readHeartbeat()
throws LockFile.FileSecurityException,
LockFile.UnexpectedFileNotFoundException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException, LockFile.WrongMagicException {
FileInputStream fis = null;
DataInputStream dis = null;
try {
if (!file.exists()) {
return Long.MIN_VALUE;
}
fis = new FileInputStream(file);
dis = new DataInputStream(fis);
checkMagic(dis);
return dis.readLong();
} catch (SecurityException ex) {
throw new FileSecurityException(this, "readHeartbeat", ex);
} catch (FileNotFoundException ex) {
throw new UnexpectedFileNotFoundException(this, "readHeartbeat",
ex);
} catch (EOFException ex) {
throw new UnexpectedEndOfFileException(this, "readHeartbeat", ex);
} catch (IOException ex) {
throw new UnexpectedFileIOException(this, "readHeartbeat", ex);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ioe) {
// ioe.printStackTrace();
}
}
}
}
/**
* Schedules the lock heartbeat task.
*/
private final void startHeartbeat() {
if (timerTask == null || HsqlTimer.isCancelled(timerTask)) {
Runnable runner = new HeartbeatRunner();
timerTask = timer.schedulePeriodicallyAfter(0, HEARTBEAT_INTERVAL,
runner, true);
}
}
/**
* Cancels the lock heartbeat task.
*/
private final void stopHeartbeat() {
if (timerTask != null && !HsqlTimer.isCancelled(timerTask)) {
HsqlTimer.cancel(timerTask);
timerTask = null;
}
}
/**
* Writes the {@link #MAGIC} value to this object's lock file that
* distiguishes it as an HSQLDB lock file. <p>
*
* @throws FileSecurityException possibly never (seek and write are native
* methods whose JavaDoc entries do not actually specify throwing
* <tt>SecurityException). However, it is conceivable that these
* native methods may, in turn, access Java methods that do
* throw <tt>SecurityException. In this case, a
* <tt>SecurityException might be thrown if a required system
* property value cannot be accessed, or if a security manager exists
* and its <tt>{@link
* java.lang.SecurityManager#checkWrite(java.io.FileDescriptor)}</tt>
* method denies write access to the file
* @throws UnexpectedEndOfFileException if an end of file exception is
* thrown while attempting to write the <tt>MAGIC value to the
* target file (typically, this cannot happen, but the case is
* included to distiguish it from the general <tt>IOException
* case).
* @throws UnexpectedFileIOException if any other I/O error occurs while
* attepting to write the <tt>MAGIC value to the target file.
*/
private final void writeMagic()
throws LockFile.FileSecurityException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException {
try {
raf.seek(0);
raf.write(MAGIC);
} catch (SecurityException ex) {
throw new FileSecurityException(this, "writeMagic", ex);
} catch (EOFException ex) {
throw new UnexpectedEndOfFileException(this, "writeMagic", ex);
} catch (IOException ex) {
throw new UnexpectedFileIOException(this, "writeMagic", ex);
}
}
/**
* Writes the current hearbeat timestamp value to this object's lock
* file. <p>
*
* @throws FileSecurityException possibly never (seek and write are native
* methods whose JavaDoc entries do not actually specifiy throwing
* <tt>SecurityException). However, it is conceivable that these
* native methods may, in turn, access Java methods that do throw
* <tt>SecurityException. In this case, a
* <tt>SecurityException might be thrown if a required system
* property value cannot be accessed, or if a security manager exists
* and its <tt>{@link
* java.lang.SecurityManager#checkWrite(java.io.FileDescriptor)}</tt>
* method denies write access to the file
* @throws UnexpectedEndOfFileException if an end of file exception is
* thrown while attepting to write the heartbeat timestamp value to
* the target file (typically, this cannot happen, but the case is
* included to distiguish it from the general IOException case).
* @throws UnexpectedFileIOException if the current heartbeat timestamp
* value cannot be written due to an underlying I/O error
*/
private final void writeHeartbeat()
throws LockFile.FileSecurityException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException {
try {
raf.seek(MAGIC.length);
raf.writeLong(System.currentTimeMillis());
} catch (SecurityException ex) {
throw new FileSecurityException(this, "writeHeartbeat", ex);
} catch (EOFException ex) {
throw new UnexpectedEndOfFileException(this, "writeHeartbeat", ex);
} catch (IOException ex) {
throw new UnexpectedFileIOException(this, "writeHeartbeat", ex);
}
}
/**
* Tests whether some other object is "equal to" this one. <p>
*
* An object is considered equal to a <tt>LockFile object if and
* only if it is not null, it is an instance of <tt>LockFile and
* either it is the identical instance or it has the same lock file. More
* formally, is is considered equal if and only if it is not null, it is an
* instance of <tt>LockFile, and the expression:
*
* <pre>
* this == other ||
* this.file == null ? other.file == null : this.file.equals(other.file);
* </pre>
*
* yeilds true. <p>
*
* Note that <tt>file must be a canonical reference to correctly
* satisfy this contract. <p>
*
* @param obj the reference object with which to compare.
* @return <tt>true if this object is equal to the obj
* argument; <tt>false otherwise.
* @see #hashCode
*/
public final boolean equals(final Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof LockFile) {
LockFile other = (LockFile) obj;
return (this.file == null) ? other.file == null
: this.file.equals(other.file);
}
return false;
}
/**
* Retrieves the canonical path of this object's lock file, as a
* <tt>String object.
*
* @return the canonical path of this object's lock file.
*/
public final String getCanonicalPath() {
return cpath;
}
/**
* Retrieves the hash code value for this object. <p>
*
* The value is zero if <tt>file is null, else the
* <tt>hashCode of file. That is, two LockFile
* objects have the same <tt>hashCode value if they refer to the
* same lock file. <p>
*
* Note that <tt>file must be a canonical reference to correctly
* satisfy this contract. <p>
*
* @return a hash code value for this object.
* @see #equals(java.lang.Object)
*/
public final int hashCode() {
return file == null ? 0
: file.hashCode();
}
/**
* Retrieves whether this object has successfully obtained and is still
* holding (has not yet released) a cooperative lock condition on its
* lock file. <p>
*
* <b>Note:
*
* Due to platform-independence retrictions placed on a JVM, it is quite
* possible to successfully acquire a lock condition and yet for the
* condition to become invalid while still held. <p>
*
* For instance, under JVMs with no <tt>java.nio package or under
* operating systems that do not apply mandatory file locking (espcially
* mandatory locking that precludes deletion), it is quite possible for
* another process or even an uncooperative bit of code running in the same
* JVM to overwrite or delete the target lock file while this object holds
* a lock condition. <p>
*
* Because of this, the <tt>isValid() method is provided in the public
* interface in order to allow clients to detect at least a subset of such
* situations. <p>
*
* @return <tt>true if this object has successfully obtained and is
* still holding (has not yet released) a lock condition, else
* <tt>false
* @see #isValid
*/
public final boolean isLocked() {
return locked;
}
/**
* Retrieves whether there is potentially already a cooperative lock,
* operating system lock or some other situation preventing a cooperative
* lock condition from being aquired using the specified path.
*
* @param path the path to test
* @return <tt>true if there is currently something preventing the
* acquisition of a cooperative lock condition using the specified
* <tt>path, else false
*/
public final static boolean isLocked(final String path) {
boolean locked = true;
try {
LockFile lockFile = LockFile.newLockFile(path);
lockFile.checkHeartbeat(false);
locked = false;
} catch (Exception e) {}
return locked;
}
/**
* Retrieves whether this object holds a valid lock condition on its
* lock file. <p>
*
* More formally, this method retrieves true if and only if: <p>
*
* <pre>
* isLocked() && file != null && file.exists() && raf != null
* </pre>
*
* @return <tt>true if this object holds a valid lock condition on its
* lock file; else <tt>false
* @throws SecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* <tt>checkRead method denies read access to the lock file;
*/
public boolean isValid() {
return isLocked() && file != null && file.exists() && raf != null;
}
/**
* Retrieves a String representation of this object. <p>
*
* The String is of the form: <p>
*
* <pre>
* super.toString() +
* "[file=" + getCanonicalPath() +
* ", exists=" + file.exists() +
* ", locked=" + isLocked() +
* ", valid=" + isValid() +
* ", " + toStringImpl() +
* "]";
* </pre>
*
*
* @return a String representation of this object.
* @see #toStringImpl
* @throws SecurityException if a required system property value cannot
* be accessed, or if a security manager exists and its <tt>{@link
* java.lang.SecurityManager#checkRead}</tt> method denies
* read access to the lock file;
*/
public String toString() {
return new StringBuffer(super.toString()).append("[file =").append(
cpath).append(", exists=").append(file.exists()).append(
", locked=").append(isLocked()).append(", valid=").append(
isValid()).append(", ").append(toStringImpl()).append(
"]").toString();
}
/**
* Retrieves an implementation-specific tail value for the
* <tt>toString() method.
*
* The default implementation returns the empty string.
*
* @return an implementation-specific tail value for the <tt>toString()
* method
* @see #toString
*/
protected String toStringImpl() {
return "";
}
/**
* Retrieves the number of times <tt>checkHeartbeat may fail before
* <tt>pollHeartbeat fails as a consequence.
*
* The value is obtained in the following manner: <p>
*
* <ol>
* <li>retries is assigned POLL_RETRIES_DEFAULT.
*
* <li>retries is assigned Integer.getInteger(POLL_RETRIES_PROPERTY,
* retries)</tt> inside a try-catch block to silently ignore any security
* exception.
*
* <li>If retries is less than one (1), retries is assigned one (1).
* </ol>
*
* @return the number of times <tt>checkHeartbeat may fail before
* <tt>pollHeartbeat fails as a consequence.
*/
public int getPollHeartbeatRetries() {
int retries = POLL_RETRIES_DEFAULT;
try {
retries = Integer.getInteger(
HsqlDatabaseProperties.system_lockfile_poll_retries_property,
retries).intValue();
} catch (Exception e) {}
if (retries < 1) {
retries = 1;
}
return retries;
}
/**
* Retrieves the interval, in milliseconds, that <tt>pollHeartbeat
* waits between failed invocations of <tt>checkHeartbeat.
*
* The value is obtained in the following manner: <p>
*
* <ol>
* <li>interval is assigned 10 + (HEARTBEAT_INTERVAL_PADDED
* getPollHeartbeatRetries())</tt>
*
* <li>interval is assigned Long.getLong(POLL_INTERVAL_PROPERTY,
* interval)</tt>, inside a try-catch block, to silently ignore any security
* exception.
*
* <li>If interval is less than or equal to zero (0), interval is reassigned
* <tt>10 + (HEARTBEAT_INTERVAL_PADDED / getPollHeartbeatRetries())
* </ol>
*
* @return the interval, in milliseconds, that <tt>pollHeartbeat
* waits between failed invocations of <tt>checkHeartbeat
*/
public long getPollHeartbeatInterval() {
int retries = getPollHeartbeatRetries();
long interval = 10 + (HEARTBEAT_INTERVAL_PADDED / retries);
try {
interval = Long.getLong(POLL_INTERVAL_PROPERTY,
interval).longValue();
} catch (Exception e) {}
if (interval <= 0) {
interval = 10 + (HEARTBEAT_INTERVAL_PADDED / retries);
}
return interval;
}
/**
* Polls the underlying lock file to determine if a lock condition
* exists. <p>
*
* Specifically, polls {@link #checkHeartbeat(boolean) checkHeartbeat} at
* the configured interval until the check passes, the current poll interval
* wait state is interrupted or the configured number of poll retries is
* reached. <p>
*
* The last exception thrown by <tt>checkHeartbeat is re-thrown if no
* check passes. <p>
*
* @throws FileSecurityException if the Java security system denied read
* to the target file
* @throws LockHeldExternallyException if the target file's heartbeat
* timestamp indicated that a lock condition was held by another
* <tt>LockFile.
* @throws UnexpectedFileNotFoundException if the target file became
* unavailable between a test for existence and an attempt to read
* the <tt>MAGIC or heartbeat timestamp value.
* @throws UnexpectedEndOfFileException if an <tt>EOFException was
* raised while trying to read the <tt>MAGIC or heartbeat
* timestamp value of the target file
* @throws UnexpectedFileIOException if an <tt>EOFException other than
* <tt>EOFException was raised while trying to read the
* <tt>MAGIC or heartbeat timestamp value of the target file
* @throws WrongLengthException if the target file did not have the
* expected length
* @throws WrongMagicException if the target file did not begin with the
* expected <tt>MAGIC value
*/
private final void pollHeartbeat()
throws LockFile.FileSecurityException,
LockFile.LockHeldExternallyException,
LockFile.UnexpectedFileNotFoundException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException, LockFile.WrongLengthException,
LockFile.WrongMagicException {
boolean success = false;
int retries = getPollHeartbeatRetries();
long interval = getPollHeartbeatInterval();
LockFile.BaseException reason = null;
for (int i = retries; i > 0; i--) {
try {
checkHeartbeat(true); // withCreateNewFile == true
success = true;
break;
} catch (LockFile.BaseException ex) {
reason = ex;
}
// We get here if and only if success == false and reason != null,
// so its OK to 'break'
try {
Thread.sleep(interval);
} catch (InterruptedException ex) {
break;
}
}
/**
* @todo:
* Do not want to specify just BaseException in the throws clause.
* Is this really the cleanest way?
*/
if (!success) {
if (reason instanceof FileSecurityException) {
throw (FileSecurityException) reason;
} else if (reason instanceof LockHeldExternallyException) {
throw (LockHeldExternallyException) reason;
} else if (reason instanceof UnexpectedFileNotFoundException) {
throw (UnexpectedFileNotFoundException) reason;
} else if (reason instanceof UnexpectedEndOfFileException) {
throw (UnexpectedEndOfFileException) reason;
} else if (reason instanceof UnexpectedFileIOException) {
throw (UnexpectedFileIOException) reason;
} else if (reason instanceof WrongLengthException) {
throw (WrongLengthException) reason;
} else if (reason instanceof WrongMagicException) {
throw (WrongMagicException) reason;
}
}
}
/**
* Attempts to obtain a cooperative lock condition upon this object's lock
* file. <p>
*
* @return <tt>true if this object already holds a lock or the lock was
* obtained successfully, else <tt>false
* @throws FileSecurityException if the lock condition could not be
* obtained due to a Java security permission violation
* @throws LockHeldExternallyException if the lock condition could not
* be obtained because the target file's heartbeat timestamp indicated
* that a lock condition was held by another <tt>LockFile.
* @throws UnexpectedFileNotFoundException if the lock condition could not
* be obtained because the target file became unavailable between a
* successful test for existence and an attempt to read its
* <tt>MAGIC or heartbeat timestamp value.
* @throws UnexpectedEndOfFileException if the lock condition could not be
* obtained because <tt>EOFException was raised while trying to
* read the <tt>MAGIC or heartbeat timestamp value of the target
* file
* @throws UnexpectedFileIOException if the lock condition could not be
* obtained due to an <tt>IOException other than
* <tt>EOFException
* @throws WrongLengthException if the lock condition could not be obtained
* because the target file was the wrong length
* @throws WrongMagicException if the lock condition could not be obtained
* because the target file had the wrong <tt>MAGIC value
* @return <tt>true if and only if a lock condition is obtained;
* <tt>false otherwise. In general, an exception will
* <em>always be thrown if a lock condition cannot be obtained for
* any reason
*/
public final boolean tryLock()
throws LockFile.FileSecurityException,
LockFile.LockHeldExternallyException,
LockFile.UnexpectedFileNotFoundException,
LockFile.UnexpectedEndOfFileException,
LockFile.UnexpectedFileIOException, LockFile.WrongLengthException,
LockFile.WrongMagicException {
if (this.locked) {
return true;
}
try {
pollHeartbeat();
openRAF();
// Must come *after* openRAF to comply with the
// doOptionalLockActions() PRE: assertion contract.
//
// <sigh> In an ideal world, it would be possible from Java to open
// a file handle and obtain at least one associated NIO FileLock in
// one kernel-enforced atomic operation. However, we can't even
// guarantee that NIO is available.
//
// Note:
// The NIOLockFile version of this operation is 'self cleaning'...
// if it fails for some reason, then it does a 'best effort' to
// eagerly release and nullify its FileLock object before
// returning.
doOptionalLockActions();
// Inlined the following to reduce potential for timing issues
// such as initial timer thread startup induced delay of first
// pulse.
//
// In general, what we'll get is two initial pulses in rapid
// sucession: one here and one an instant later as a result of
// startHeartbeat (which is OK... no harm, and it's one-shot
// behaviour, not repeated on every writeHeartbeat)
//
// Unfortunately, we may occasionally encounter astronomic (at least
// in computer time) delays between invocation of startHeartbeat
// and the time at which effort is actually expended toward writing
// the initial MAGIC and heartbeat timestamp values.
//
// Another good reason to inline the first writeHeartbeat is to
// provide a last line of defence against inter-process as well
// as inter-thread race conditions. That is, exceptions thrown in
// HeartbeatRunner.run() do yet get propagated anywhere useful.
//
// Of course, if we are operating under a fully-featured and correct
// NIO implementation, the concerns described above are really
// non-issues... at this point, we will have (at least in theory) a
// valid OS-enforced file lock.
//
// But in an ideal world (with or without NIO), any pulse failure in
// HeartbeatRunner.run() would flag the database Logger that a
// database lock condition violation has occured, preventing further
// ad-hoc operation of the database.
//
// The problem is, if a lock condition has been violated that is
// being used by a database instance, what mechanism can be used to
// safely checkpoint, backup and/or shut down that instance? For
// all we know, the violation indicates that another instance is now
// happily writing to the other database files...
//
// A prudent course of action to take under detection of a
// cooperative lock condition violation in the heartbeatRunner task
// would be to perform a 'SCRIPT <file>' to some pre-ordained 'safe'
// backup location using a globally unique file name and then do a
// 'SHUTDOWN IMMEDIATELY' in one database-scope atomic context (e.g.
// a single JDBC statement execution).
//
// However, by the time a lock condition violation has been detected,
// the data cache file (and log/script) may already be quite
// corrupted, meaning the resulting script may be totally inaccurate
// or worse.
//
// Bottom line:
//
// Regardless of this inlining measure, if a lock violation occurs
// after startHeartbeat, it's almost certain there's much worse in
// store...
writeMagic();
writeHeartbeat();
FileUtil.getDefaultInstance().deleteOnExit(file);
this.locked = true;
startHeartbeat();
} finally {
if (!locked) {
// No harm in this...
//
// If this LockFile is an NIOLockFile instance and
// doOptionalLockActions() failed above, then a 'best
// effort' optional release was already perfomed and
// this will be a no-op.
//
// On the other hand, if doOptionalLockActions() succeeded, best
// to undo them here right away, since the core locking work
// failed.
//
// In practice, however, it is very unlikely for the core
// locking work to fail if this LockFile is an NIOLockFile
// instance and doOptionalLockActions() succeeded, except
// under JVM implementations whose NIO package is broken in
// a very specific way.
//
// Other possibilities include unfortunate timing of events
// under certain network file system or removable media
// configurations, device umounts, physical removal of storage
// media, Java security or file system security policy
// updates, etc.
doOptionalReleaseActions();
try {
closeRAF();
} catch (Exception ex) {
// It's too late to do anything useful with this exception.
//
// we've already/ failed and will let the caller know the
// reason via the exception thrown in the try block.
//
// ex.printStackTrace();
}
}
}
return this.locked;
}
/**
* Attempts to release any cooperative lock condition this object
* may hold upon its lock file. <p>
*
*
* @return <tt>true if this object does not currently hold a
* lock condition or the lock is released completely (including
* successful file deletion), else <tt>false.
* @throws FileSecurityException if a <tt>SecurityException is raised
* in the process of releasing the lock condition
* @throws UnexpectedFileIOException if an IoException is raised in the
* process of releasing the lock condition
*/
public final boolean tryRelease()
throws LockFile.FileSecurityException, LockFile.UnexpectedFileIOException {
boolean released = !locked;
if (released) {
return true;
}
stopHeartbeat();
doOptionalReleaseActions();
UnexpectedFileIOException closeRAFReason = null;
FileSecurityException securityReason = null;
try {
try {
closeRAF();
} catch (UnexpectedFileIOException ex) {
closeRAFReason = ex;
}
try {
// Hack Alert:
//
// Even without the presence of concurrent locking attempts,
// the delete or exists invocations below occasionally return
// false otherwise, perhaps due to a race condition with the
// heartbeat timestamp writer task or some nio file lock release
// timing issue?
//
// TODO:
//
// determine if this is an external constraint or if we can
// solve it instead by waiting for any in-progress
// writeHeartbeat operation to conclude.
Thread.sleep(100);
} catch (Exception ex) {
// ex.printStackTrace();
}
try {
released = file.delete();
// Perhaps excessive...
//
// Another Lockfile may recreate the file an instant after it is
// deleted above (if it it deleted successfully, that is)
// released = !file.exists();
} catch (SecurityException ex) {
securityReason = new FileSecurityException(this, "tryRelease",
ex);
}
} finally {
// Regardless of whether all release work succeeds, it is important
// to indicate that, from the perspective of this instance, a lock
// condition is no longer held.
//
// However, in a world of concurrent execution, we do not want to
// to expose this fact extenally until *after* all release work has
// been at least attempted.
this.locked = false;
}
if (closeRAFReason != null) {
throw closeRAFReason;
} else if (securityReason != null) {
throw securityReason;
}
return released;
}
/**
* Attempts to release this object's cooperative lock condition. <p>
*
* @throws Throwable if this object encounters an unhandled exception
* while trying to release the cooperative lock condition
*/
protected final void finalize() throws Throwable {
this.tryRelease();
}
/**
* For internal use only. <p>
*
* This Runnable class provides the implementation for the timed task
* that periodically writes out a heartbeat timestamp to the lock file.<p>
*/
private final class HeartbeatRunner implements Runnable {
public void run() {
try {
LockFile.this.writeHeartbeat();
} catch (Throwable t) {
Trace.printSystemOut(t.toString());
}
}
}
/**
* Base exception class for lock condition specific exceptions. <p>
*
*/
public abstract static class BaseException extends Exception {
private final LockFile lockFile;
private final String inMethod;
/**
* Constructs a new <tt>LockFile.BaseException.
*
* @param lockFile the underlying <tt>LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
*/
public BaseException(final LockFile lockFile, final String inMethod) {
super();
if (lockFile == null) {
throw new NullPointerException("lockFile");
}
if (inMethod == null) {
throw new NullPointerException("inMethod");
}
this.lockFile = lockFile;
this.inMethod = inMethod;
}
/**
* Subclass-specific override. <p>
*
* @return representation of <tt>lockFile and
* <tt>inMethod, as String object
*/
public String getMessage() { // override
return "lockFile: " + lockFile + " method: " + inMethod;
}
/**
* Getter for <tt>inMethod property.
*
* @return name of method in which exception originally occured
*/
public String getInMethod() {
return this.inMethod;
}
/**
* Getter for <tt>lockFile property.
*
* @return the underlying <tt>LockFile object
*/
public LockFile getLockFile() {
return this.lockFile;
}
}
/**
* Thrown when canonicalization of a <tt>LockFile object's target
* file path fails. <p>
*
* This is possible because the given path may be illegal on the runtime
* file system or because construction of the canonical pathname may require
* filesystem queries.
*/
public static final class FileCanonicalizationException
extends BaseException {
private final IOException reason;
/**
* Constructs a new <tt>FileCanonicalizationException.
*
* @param lockFile the underlying <tt>LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param reason the exception thrown during canonicalization
*/
public FileCanonicalizationException(final LockFile lockFile,
final String inMethod,
final IOException reason) {
super(lockFile, inMethod);
this.reason = reason;
}
/**
* Retrieves the underlying <tt>IOException.
*
* @return Value of property reason.
*/
public IOException getReason() {
return this.reason;
}
/**
* Subclass-specific override. <p>
*
* @return representation of <tt>lockFile, inMethod and
* <tt>reason, as a String object
*/
public String getMessage() { // override
return super.getMessage() + " reason: " + reason;
}
}
/**
* Thrown when access to a <tt>LockFile object's target file raises a
* Java <tt>SecurityEception.
*
* This can occur if a required system property value cannot be accessed, or
* if a security manager exists and its <tt>{@link
* java.lang.SecurityManager#checkRead}</tt> method denies read access to a
* file; or if its <tt>{@link
* java.lang.SecurityManager#checkRead(java.lang.String)}</tt>
* method does not permit verification of the existence of all necessary
* parent directories; or if its <tt>{@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}</tt>
* method does not permit all necessary parent directories to be
* created. <p>
*
*/
public static final class FileSecurityException extends BaseException {
private final SecurityException reason;
/**
* Constructs a new <tt>FileSecurityException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param reason the underlying Java security exception
*/
public FileSecurityException(final LockFile lockFile,
final String inMethod,
final SecurityException reason) {
super(lockFile, inMethod);
this.reason = reason;
}
/**
* Retrieves the underlying <tt>SecurityException.
*
* @return Value of property reason.
*/
public SecurityException getReason() {
return this.reason;
}
/**
* Subclass-specific override.
*
* @return representation of lockFile, inMethod and reason, as
* a String object
*/
public String getMessage() { // override
return super.getMessage() + " reason: " + reason;
}
}
/**
* Thrown when an externally held lock condition prevents lock
* aquisition. <p>
*
* Specifically, this exception is thrown when polling fails because the
* lock file's heartbeat timestamp value indicates that another LockFile
* object still holds the lock condition. <p>
*
*/
public static final class LockHeldExternallyException
extends BaseException {
private final long read;
private final long heartbeat;
/**
* Constructs a new <tt>LockHeldExternallyException.
*
* @param lockFile the underlying <tt>LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param read the time, in milliseconds since 1970-01-01, at which
* the heartbeat timestamp value was read from the lock file
* @param heartbeat the heartbeat timestamp value, in milliseconds
* since 1970-01-01, that was read from the lock file.
*/
public LockHeldExternallyException(final LockFile lockFile,
final String inMethod,
final long read,
final long heartbeat) {
super(lockFile, inMethod);
this.read = read;
this.heartbeat = heartbeat;
}
/**
* Getter for the <tt>heartbeat attribute.
*
* @return the heartbeat timestamp value, in milliseconds since
* 1970-01-01, that was read from the lock file.
*/
public long getHeartbeat() {
return this.heartbeat;
}
/**
* Getter for the <tt>read attribute.
*
* @return the time, in milliseconds since 1970-01-01, that
* the heartbeat timestamp value was read from the lock file.
*/
public long getRead() {
return this.read;
}
/**
* Subclass-specific override. <p>
*
* @return representation of <tt>lockFile, inMethod,
* <tt>read and heartbeat, as a String
* object
*/
public String getMessage() { // override
return super.getMessage() + " read: "
+ HsqlDateTime.getTimestampString(this.read)
+ " heartbeat - read: " + (this.heartbeat - this.read)
+ " ms.";
}
}
/**
* Thrown when access to a <tt>LockFile object's target file raises an
* unexpected <tt>EOFException.
*/
public static final class UnexpectedEndOfFileException
extends BaseException {
private final EOFException reason;
/**
* Constructs a new <tt>UnexpectedEndOfFileException.
*
* @param lockFile the underlying <tt>LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param reason the underlying exception
*/
public UnexpectedEndOfFileException(final LockFile lockFile,
final String inMethod,
final EOFException reason) {
super(lockFile, inMethod);
this.reason = reason;
}
/**
* Retrieves the underlying <tt>EOFException.
*
* @return Value of property reason.
*/
public EOFException getReason() {
return this.reason;
}
/**
* Subclass-specific override. <p>
*
* @return representation of <tt>lockFile, inMethod and
* <tt>reason, as a String object
*/
public String getMessage() { // override
return super.getMessage() + " reason: " + reason;
}
}
/**
* Thrown when access to a <tt>LockFile object's target file raises an
* unexpected <tt>IOException other than EOFException.
*/
public static final class UnexpectedFileIOException extends BaseException {
private final IOException reason;
/**
* Constructs a new <tt>UnexpectedFileIOException.
*
* @param lockFile the underlying <tt>LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param reason the underlying exception
*/
public UnexpectedFileIOException(final LockFile lockFile,
final String inMethod,
final IOException reason) {
super(lockFile, inMethod);
this.reason = reason;
}
/**
* Retrieves the underlying <tt>IOException.
*
* @return Value of property reason.
*/
public IOException getReason() {
return this.reason;
}
/**
* Subclass-specific override.
*
* @return representation of <tt>lockFile, inMethod and
* <tt>reason, as a String object
*/
public String getMessage() { // override
return super.getMessage() + " reason: " + reason;
}
}
/**
* Thrown when access to a <tt>LockFile object's target file raises an
* unexpected <tt>FileNotFoundException.
*/
public static final class UnexpectedFileNotFoundException
extends BaseException {
private final FileNotFoundException reason;
/**
* Constructs a new <tt>UnexpectedFileNotFoundException.
*
* @param lockFile the underlying <tt>LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param reason the underlying exception
*/
public UnexpectedFileNotFoundException(
final LockFile lockFile, final String inMethod,
final FileNotFoundException reason) {
super(lockFile, inMethod);
this.reason = reason;
}
/**
* Retrieves the underlying FileNotFoundException.
*
* @return Value of property reason.
*/
public FileNotFoundException getReason() {
return this.reason;
}
/**
* Subclass-specific override.
*
* @return representation of lockFile, inMethod and reason, as
* a String object
*/
public String getMessage() { // override
return super.getMessage() + " reason: " + reason;
}
}
/**
* Thrown when it is detected that a LockFile object's target file does not
* have the expected length.
*/
public static final class WrongLengthException extends BaseException {
private final long length;
/**
* Constructs a new WrongLengthException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param length the actual length reported by the file system
*/
public WrongLengthException(final LockFile lockFile,
final String inMethod, final long length) {
super(lockFile, inMethod);
this.length = length;
}
/**
* Retreives the actual length reported by the file system.
*
* @return the actual length reported by the file system
*/
public long getLength() {
return this.length;
}
/**
* Subclass-specific override.
*
* @return representation of lockFile, inMethod and length, as
* a String object
*/
public String getMessage() { // override
return super.getMessage() + " length: " + length;
}
}
/**
* Thrown when it is detected that a LockFile object's target file does not
* start with the expected MAGIC value.
*/
public static final class WrongMagicException extends BaseException {
private final byte[] magic;
/**
* Constructs a new WrongMagicException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up serveral levels)
* @param magic the actual magic value read from the file
*/
public WrongMagicException(final LockFile lockFile,
final String inMethod, final byte[] magic) {
super(lockFile, inMethod);
this.magic = magic;
}
/**
* Subclass-specific override.
*
* @return representation of inMethod, file and magic,
* as a String object
*/
public String getMessage() { // override
String message = super.getMessage() + " magic: ";
message = message + ((magic == null) ? "null"
: "'"
+ StringConverter.byteToHex(magic)
+ "'");
return message;
}
/**
* Retrieves a copy of the actual <tt>MAGIC value read from the
* file. <p>
*
* @return a copy of the actual <tt>MAGIC value read from the file
*/
public byte[] getMagic() {
return (magic == null) ? null
: (byte[]) this.magic.clone();
}
}
}
Other HSQLDB examples (source code examples)
Here is a short list of links related to this HSQLDB LockFile.java source code file: