org.hsqldb.persist.LockFile Maven / Gradle / Ivy
Show all versions of sqltool Show documentation
/* Copyright (c) 2001-2020, 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.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.FileUtil;
import org.hsqldb.lib.HsqlTimer;
import org.hsqldb.lib.StringConverter;
/**
* Base cooperative file locking implementation and LockFile
* factory.
*
*
*
* Provides a facility for cooperative file locking across process boundaries
* and isolated in-process class loader contexts.
*
* 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
* * 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.
*
* *
* 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.
*
*
*
* Here is the way this class presently operates:
*
*
* - 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.
*
*
- 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.
*
*
- 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.
*
*
- The generic rules used to acquire a cooperative lock condition are
* as follows:
*
*
* - If a lock condition is already held by this object, do nothing and
* signify that the lock attempt was successful, else...
*
*
- 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.
*
*
- For each poll:
*
*
*
* - 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 exception is thrown in response
* to the attempt, else...
*
*
- Test if the underlying file exists, fast failing the current
* poll if it is impossible to determine (i.e. if a security
* exception is thrown).
*
*
- If the file does not exist, exit the polling loop immediately
* indicating success.
*
* 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).
*
* If the underlying platform employs a kernel-enforced mandatory
* file locking blanket policy for open files (e.g. Windows
* tm), then this is likely a non-issue. And if
* this case makes possible a race condition with another
* LockFile object (because the test for existence and
* subsequent file creation is not atomic relative to all other
* file system actions), it is still very unlikely that
* so unfortunate a timing will occur as to allow simultaneous
* lock conditions to be established. Finally, if some
* non-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.
*
*
- Test the file's length, fast failing the current poll if the
* length cannot be determined or it is not the expected
* value.
*
*
- Open a stream to read the file's MAGIC and heartbeat
* timestamp values, fast failing the current poll if the stream
* cannot be opened.
*
*
- Test the file's MAGIC value, failing the current poll
* if the value cannot be read or it is not the expected
* value.
*
*
- 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).
*
* - 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:
*
*
* - The file had the wrong length or MAGIC value (was
* not an HSQLDB lock file).
*
*
- 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.
*
*
- An incompatible OS-enforced security restriction was in
* effect.
*
*
- An incompatible Java-enforced security restriction was
* in effect.
*
*
- The target file system media was effectively inaccessible.
*
- A cooperative lock condition was held by some other
* LockFile.
*
*
- A kernel-enforced mandatory or advisory file lock was held.
*
*
* In this case, signify failure indicating the last encountered
* reason, else...
*
*
- 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.
*
* - The generic rules used to release a cooperative lock condition are:
*
* - If a lock condition is not currently held, do nothing and signify
* success, else...
*
*
- A lock condition is currently held by this object, so try to
* release it.
*
* 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 might have failed.
*
*
*
*
*
* 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()}.
*
* In particular, if {@link #USE_NIO_FILELOCK_PROPERTY} is true and the required
* classes are available at static initialization, then newLockFile()
* produces org.hsqldb.persist.NIOLockFile instances.
*
* When NIOLockFile instances are produced, then it is possible that
* true kernel-enforced advisory or mandatory 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, newLockFile() produces vanilla LockFile
* instances, which exhibit just the elementary cooperative locking behavior on
* platforms that do not, by default, implement kernel-enforced mandatory
* locking for open files.
*
* At this point, it must be noted that not every target platform upon which
* Java can run actually provides true kernel-enforced mandatory (or even
* advisory) file locking. Indeed, even when a target platform 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 mandatory locking on systems that support it
* but employ advisory locking by default.
*
* 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
* distinguish 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).
*
* It is for the preceding reasons that, as of HSQLDB 1.8.0.3,
* 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
* FileLock and failed, then the enclosing cooperative lock attempt
* failed also, despite the fact that a vanilla locking approach could
* succeed.
*
* 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.
*
* 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.
*
* 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}.
*
* 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.
*
* 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
* 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 -D<property-name>=true directive). Be aware that
* the system property value is read only once, in the static initializer block
* for this class.
*
* 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.
*
* Second, to the limit of the author's present understanding, the
* implementation details of this class represent a good compromise under varying
* and generally uncontrollable JVM, OS and hardware platform
* limitations/capabilities, as well as under usability considerations and
* external security or operating constraints that may need to be imposed.
*
* Alternate approaches that have been considered and rejected for now
* include:
*
*
* - Socket-based locks (with/without broadcast protocol)
*
- Pure NIO locking
*
- Simple lock file (no heartbeat or polling)
*
- JNI and native configuration alternatives
*
*
* Of course, discussions involving and patches implementing improvements
* or better alternatives are always welcome.
*
* As a final note and sign post for developers starting to work with
* Java NIO:
*
* A separate NIOLockFile descendant exists specifically
* because it was determined through experimentation that
* 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
* 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 LockFile
* instances, other in-process objects or other processes to successfully obtain
* a lock condition on the lock file, despite the fact that the
* 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 FileLock.tryLock()
* method was reflectively invoked.
*
* To solve this, the original LockFile class was split in two and
* instead of reflective method invocation, subclass instantiation is now
* performed at the level of the newLockFile() factory method.
* Similarly, the HSQLDB ANT build script now detects the presence or absence
* of JDK 1.4+ features such as java.nio and only attempts to build and deploy
* NIOLockFile to the hsqldb.jar if such features are reported
* present.
*
* The nio lock file was removed in version 2.0 and reference removed in 2.5.1.
*
* @author Campbell Burnet (campbell-burnet@users dot sourceforge.net)
* @version 2.5.1
* @since 1.7.2
*/
public class LockFile {
/**
* Arbitrary period, in milliseconds, at which heartbeat timestamps are
* written to this object's lock file.
*
* 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.
*
* 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.
*
* 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'}.
*
* 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.
*
* 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 java.nio file locking is attempted by default.
* Unsupported from version 2.0.
* */
public static final boolean USE_NIO_FILELOCK_DEFAULT = false;
/**
* System property that can be used to control whether nio file locking is
* attempted. Unsupported from version 2.0.
*/
public static final String USE_NIO_FILELOCK_PROPERTY =
"hsqldb.lockfile.nio.filelock";
/**
* Statically computed indication of java.nio.channels.FileLock
* runtime availability.
*
* Design Note:
*
* Computed in a static initializer block. Will be false if
* 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 NIOLockFile class.
*
* Design Note:
*
* Computed in a static initializer block. Will be null if
* 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 referencing a subclass whose static
// field initialization 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.
*
* Design Note:
*
* Should really be final, but finality makes reflective construction
* and adherence to desirable LockFile factory method event
* sequence more complicated.
*/
protected File file;
/**
* Cached value of the lock file's canonical path
*
* Design Note:
*
* Should really be final, but finality makes reflective construction
* and adherence to desirable LockFile factory method event
* sequence much more complicated.
*/
private String cpath;
/**
* A RandomAccessFile constructed from this object's canonical file
* reference.
*
* This 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 heartbeat task. */
private volatile Object timerTask;
/**
* Retrieves a new NIOLockFile, or null if not available
* under the current runtime environment.
*
* Returns null from version 2.0.
*
* @return a new NIOLockFile, or null if not available
* under the current runtime environment
*/
private static LockFile newNIOLockFile() {
/*
if (NIO_FILELOCK_AVAILABLE && NIO_LOCKFILE_CLASS != null) {
try {
return (LockFile) NIO_LOCKFILE_CLASS.getDeclaredConstructor().newInstance();
} catch (Exception e) {
// e.printStackTrace()
}
}
*/
return null;
}
/**
* To allow subclassing without exposing a public constructor.
*/
protected LockFile() {}
/**
* Retrieves a LockFile instance, initialized with a File
* object whose path is the canonical form of the one specified by the
* given path argument.
*
* The resulting 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 LockFile instance initialized with a File
* object whose path is the one specified by the given path
* argument.
* @param path the path of the File object with which the retrieved
* 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 {@link
* java.lang.SecurityManager#checkRead} method denies read
* access to the file; or if its {@link
* java.lang.SecurityManager#checkRead(java.lang.String)}
* method does not permit verification of the existence of all
* necessary parent directories; or if the {@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}
* method does not permit all necessary parent directories to be
* created
*/
public static 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.
*
* Retrieves a new LockFile object holding a cooperative lock
* condition upon the file with the given path, appended with the
* extension '.lck'.
*
* @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 LockFile object holding a cooperative lock
* condition upon the file with the given path, appended with the
* extension '.lck'
*/
public static LockFile newLockFileLock(final String path)
throws HsqlException {
LockFile lockFile = null;
try {
lockFile = LockFile.newLockFile(path + ".lck");
} catch (LockFile.BaseException e) {
throw Error.error(ErrorCode.LOCK_FILE_ACQUISITION_FAILURE,
e.getMessage());
}
boolean locked = false;
try {
locked = lockFile.tryLock();
} catch (LockFile.BaseException e) {
throw Error.error(ErrorCode.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 Error.error(ErrorCode.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 LockFile instance) or stale.
*
* The check conforms to the following rules:
*
*
* - If the parameter withCreateNewFile is true, {@link
* java.io.File#createNewFile()} is available and its invocation
* upon this object's 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 race to establish
* a lock file).
*
*
- Test again if the file exists, returning immediately if it does not
* (there's no file and hence no heartbeat to check).
*
* An immediate return can occur here only under pre-JDK 1.2 runtimes;
* or when the underlying platform does not correctly support
* File.createNewFile(); or when the underlying file is deleted
* within a very short time after i.), above (typically on the order of
* microseconds).
*
* If the underlying platform employs a kernel-enforced mandatory file
* locking blanket policy for open files (e.g. Windowstm
* ), then this is likely a non-issue. And if this case makes
* possible a race condition with another LockFile object
* (because the test for existence yields false and subsequent file
* creation is not atomic relative to all other file system actions), it
* is still very unlikely that so unfortunate a timing will
* occur as to allow simultaneous lock conditions to be established.
* Finally, if some non-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.
*
*
- If a Java security exception is thrown while testing for existence,
* it is rethrown as a FileSecurityException.
*
*
- Read the file's length.
*
*
- If a Java security exception is thrown reading length, it is rethrown
* as a FileSecurityException (it is possible somebody
* concurrently refreshed the system Policy in the interim).
*
*
- If the file does not have the expected length, a
* WrongLengthException is thrown (we're trying to check
* something that is not an HSQLDB lock file).
*
*
- Open an input steam to read the file's MAGIC and heartbeat
* timestamp values.
*
*
- If a file not found exception is thrown above, it is rethrown as an
* UnexpectedFileNotFoundException (we've already tested for
* existence).
*
*
- If a Java security exception is thrown above, it is rethrown as a
* FileSecurityException (it is possible somebody
* concurrently refreshed the system Policy in the interim).
*
*
- Read the MAGIC value.
*
*
- If an end of file exception is thrown above, it is rethrown as an
* UnexpectedEndOfFileException (we've already tested the
* length... did someone truncate the file in the interim?).
*
*
- If an I/O exception is thrown, it is rethrown as an
* 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?)
*
*
- If the value read in does not match the expected MAGIC value,
* a WrongMagicException is thrown (we're trying to check
* something that is not an HSQLDB lock file).
*
*
- Read the heartbeat timestamp.
*
*
- If a Java security exception is thrown above, it is rethrown as a
* FileSecurityException (it is possible somebody
* concurrently refreshed the system Policy in the interim).
*
*
- If an end of file execution is thrown above, it is rethrown as an
* UnexpectedEndOfFileException (we've already tested the
* length... did someone truncate the file in the interim?).
*
*
- If an I/O exception is thrown, it is rethrown as an
* 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?)
*
*
- If the timestamp read in is less than or equal to
* {@link #HEARTBEAT_INTERVAL_PADDED} milliseconds into the past or
* future, then a LockHeldExternallyException is thrown.
*
*
- Otherwise, this method simply returns.
*
*
* @param withCreateNewFile if true, attempt to employ
* File.createNewFile() as part of the check so as to
* eliminate potential race conditions when establishing 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
* HEARTBEAT_INTERVAL_PADDED into the past (or future)
* @throws UnexpectedEndOfFileException if an EOFException is
* thrown while reading either the magic or heartbeat timestamp values
* @throws UnexpectedFileIOException if an IOException other than
* EOFException is thrown while reading either the magic or
* heartbeat timestamp values
* @throws UnexpectedFileNotFoundException if a
* 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;
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);
}
if (length != USED_REGION) {
if (length == 0) {
file.delete();
return;
}
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}.
*
* As a side-effect, the associated FileChannel object, if any,
* is closed as well.
*
* @throws UnexpectedFileIOException if an 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.
*
* Descendants are free to provide additional functionality here,
* using the following rules:
*
* PRE:
*
* This method is called only from tryLock() and it is called if
* and only if tryLock() successfully invokes
* pollHeartbeat() and openRAF() first.
*
* From this, it can be inferred that upon entry:
*
*
* - locked == false.
*
- raf is a non-null instance that can be used to get a
* FileChannel instance, if desired.
*
- the underlying file either did not exist before invoking
* openRAF() or it was a valid but stale HSQLDB lock file
* because it:
*
*
* - did exist,
*
- was readable on USED_REGION,
*
- had the expected length and MAGIC value and
*
- had a stale heartbeat timestamp value.
*
*
*
* 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 LockFile instance or as the result of a previous,
* successful tryLock() invocation upon this LockFile
* instance.
*
* Finally, it is important that this method does not rethrow any exceptions
* it encounters as unchecked exceptions to the calling context.
*
* POST:
*
* This method should return false if optional locking work is not
* performed or if it fails, else 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
* false return value to avoid calling {@link
* #doOptionalReleaseActions() doOptionalReleaseActions()} as part of the
* {@link #tryRelease() tryRelease()} method.
*
* Note:
*
* The default implementation does nothing and always returns
* false.
*
* @return true if optional lock actions are performed and they
* succeed, else false
*/
protected boolean doOptionalLockActions() {
return false;
}
/**
* Provides any optional release actions for the {@link #tryRelease()
* tryRelease()} template method.
*
* PRE:
*
* It is important that this method does not rethrow any exceptions
* it encounters as unchecked exceptions to the calling context.
*
* POST:
*
* In general, false should be returned if optional locking work
* is not performed or if it fails, else true. However, the return
* value is currently treated as purely informative.
*
* Note:
*
* The default implementation does nothing and always returns false.
*
* @return true if optional release actions are performed and they
* succeed, else false
*/
protected boolean doOptionalReleaseActions() {
return false;
}
/**
* Initializes this object with a File object whose path has the
* canonical form of the given path argument.
*
* PRE:
*
*
* - This method is called once and only once per
* Lockfile instance.
*
*
- It is always the first method called after
* LockFile construction
*
*
- The supplied path argument is never
* null.
*
*
* @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
* {@link java.lang.SecurityManager#checkRead} method denies
* read access to the file; or if its {@link
* java.lang.SecurityManager#checkRead(java.lang.String)}
* method does not permit verification of the existence of
* all necessary parent directories; or if
* its {@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}
* 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.getFileUtil().canonicalOrAbsolutePath(path);
this.file = new File(path);
try {
FileUtil.getFileUtil().makeParentDirectories(this.file);
} catch (SecurityException ex) {
throw new FileSecurityException(this, "setPath", ex);
}
try {
this.file = FileUtil.getFileUtil().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}.
*
* @throws UnexpectedFileNotFoundException if a
* FileNotFoundException is thrown in response to
* constructing the RandomAccessFile object.
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* {@link java.lang.SecurityManager#checkRead} method
* denies read access to the file; or if its {@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}
* 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);
}
}
/**
* Checks whether the given 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
* {@link java.lang.SecurityManager#checkRead} method
* denies read access to the file
* @throws UnexpectedEndOfFileException if an EOFException is
* thrown while reading the DataInputStream
* @throws UnexpectedFileIOException if an IOException other than
* EOFException is thrown while reading the
* DataInputStream
* @throws WrongMagicException if a value other than MAGIC is read
* from the 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 Long.MIN_VALUE
* (the earliest time representable as a long in Java) is
* returned immediately.
*
* @return the hearbeat timestamp read from this object's lock file,
* as a long value or, if this object's lock
* file does not exist, Long.MIN_VALUE, the earliest time
* representable as a long in Java.
* @throws FileSecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* {@link java.lang.SecurityManager#checkRead} method
* denies read access to the file
* @throws UnexpectedEndOfFileException if an EOFException is
* thrown while attempting to read the target file's 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 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
* distinguishes it as an HSQLDB lock file.
*
* @throws FileSecurityException possibly never (seek and write are native
* methods whose JavaDoc entries do not actually specify throwing
* SecurityException). However, it is conceivable that these
* native methods may, in turn, access Java methods that do
* throw SecurityException. In this case, a
* SecurityException might be thrown if a required system
* property value cannot be accessed, or if a security manager exists
* and its {@link
* java.lang.SecurityManager#checkWrite(java.io.FileDescriptor)}
* method denies write access to the file
* @throws UnexpectedEndOfFileException if an end of file exception is
* thrown while attempting to write the MAGIC value to the
* target file (typically, this cannot happen, but the case is
* included to distinguish it from the general IOException
* case).
* @throws UnexpectedFileIOException if any other I/O error occurs while
* attempting to write the 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.
*
* @throws FileSecurityException possibly never (seek and write are native
* methods whose JavaDoc entries do not actually specify throwing
* SecurityException). However, it is conceivable that these
* native methods may, in turn, access Java methods that do throw
* SecurityException. In this case, a
* SecurityException might be thrown if a required system
* property value cannot be accessed, or if a security manager exists
* and its {@link
* java.lang.SecurityManager#checkWrite(java.io.FileDescriptor)}
* method denies write access to the file
* @throws UnexpectedEndOfFileException if an end of file exception is
* thrown while attempting to write the heartbeat timestamp value to
* the target file (typically, this cannot happen, but the case is
* included to distinguish 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.
*
* An object is considered equal to a LockFile object if and
* only if it is not null, it is an instance of 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 LockFile, and the expression:
*
*
* this == other ||
* this.file == null ? other.file == null : this.file.equals(other.file);
*
*
* yields true.
*
* Note that file must be a canonical reference to correctly
* satisfy this contract.
*
* @param obj the reference object with which to compare.
* @return true if this object is equal to the obj
* argument; 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
* 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.
*
* The value is zero if file is null, else the
* hashCode of file. That is, two LockFile
* objects have the same hashCode value if they refer to the
* same lock file.
*
* Note that file must be a canonical reference to correctly
* satisfy this contract.
*
* @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.
*
* Note:
*
* Due to platform-independence restrictions 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.
*
* For instance, under JVMs with no java.nio package or under
* operating systems that do not apply mandatory file locking (especially
* 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.
*
* Because of this, the isValid() method is provided in the public
* interface in order to allow clients to detect at least a subset of such
* situations.
*
* @return true if this object has successfully obtained and is
* still holding (has not yet released) a lock condition, else
* 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 acquired using the specified path.
*
* @param path the path to test
* @return true if there is currently something preventing the
* acquisition of a cooperative lock condition using the specified
* path, else false
*/
public 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.
*
* More formally, this method retrieves true if and only if:
*
*
* isLocked() && file != null && file.exists() && raf != null
*
*
* @return true if this object holds a valid lock condition on its
* lock file; else false
* @throws SecurityException if a required system property value cannot
* be accessed, or if a Java security manager exists and its
* 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.
*
* The String is of the form:
*
*
* super.toString() +
* "[file=" + getCanonicalPath() +
* ", exists=" + file.exists() +
* ", locked=" + isLocked() +
* ", valid=" + isValid() +
* ", " + toStringImpl() +
* "]";
*
*
*
* @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 {@link
* java.lang.SecurityManager#checkRead} method denies
* read access to the lock file;
*/
public String toString() {
return new StringBuilder(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
* toString() method.
*
* The default implementation returns the empty string.
*
* @return an implementation-specific tail value for the toString()
* method
* @see #toString
*/
protected String toStringImpl() {
return "";
}
/**
* Retrieves the number of times checkHeartbeat may fail before
* pollHeartbeat fails as a consequence.
*
* The value is obtained in the following manner:
*
*
* - retries is assigned POLL_RETRIES_DEFAULT.
*
*
- retries is assigned Integer.getInteger(POLL_RETRIES_PROPERTY,
* retries) inside a try-catch block to silently ignore any security
* exception.
*
*
- If retries is less than one (1), retries is assigned one (1).
*
*
* @return the number of times checkHeartbeat may fail before
* 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 pollHeartbeat
* waits between failed invocations of checkHeartbeat.
*
* The value is obtained in the following manner:
*
*
* - interval is assigned 10 + (HEARTBEAT_INTERVAL_PADDED
* getPollHeartbeatRetries())
*
*
- interval is assigned Long.getLong(POLL_INTERVAL_PROPERTY,
* interval), inside a try-catch block, to silently ignore any security
* exception.
*
*
- If interval is less than or equal to zero (0), interval is reassigned
* 10 + (HEARTBEAT_INTERVAL_PADDED / getPollHeartbeatRetries())
*
*
* @return the interval, in milliseconds, that pollHeartbeat
* waits between failed invocations of 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.
*
* 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.
*
* The last exception thrown by checkHeartbeat is re-thrown if no
* check passes.
*
* @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
* LockFile.
* @throws UnexpectedFileNotFoundException if the target file became
* unavailable between a test for existence and an attempt to read
* the MAGIC or heartbeat timestamp value.
* @throws UnexpectedEndOfFileException if an EOFException was
* raised while trying to read the MAGIC or heartbeat
* timestamp value of the target file
* @throws UnexpectedFileIOException if an EOFException other than
* EOFException was raised while trying to read the
* 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 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.
*
* @return true if this object already holds a lock or the lock was
* obtained successfully, else 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 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
* MAGIC or heartbeat timestamp value.
* @throws UnexpectedEndOfFileException if the lock condition could not be
* obtained because EOFException was raised while trying to
* read the MAGIC or heartbeat timestamp value of the target
* file
* @throws UnexpectedFileIOException if the lock condition could not be
* obtained due to an IOException other than
* 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 MAGIC value
* @return true if and only if a lock condition is obtained;
* false otherwise. In general, an exception will
* 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.
//
// 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
// succession: 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 occurred, 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 ' 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.getFileUtil().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 performed 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.
*
*
* @return true if this object does not currently hold a
* lock condition or the lock is released completely (including
* successful file deletion), else false.
* @throws FileSecurityException if a 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 externally 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;
}
/**
* For internal use only.
*
* This Runnable class provides the implementation for the timed task
* that periodically writes out a heartbeat timestamp to the lock file.
*/
private final class HeartbeatRunner implements Runnable {
public void run() {
try {
LockFile.this.writeHeartbeat();
} catch (Throwable t) {
Error.printSystemOut(t.toString());
}
}
}
/**
* Base exception class for lock condition specific exceptions.
*
*/
public abstract static class BaseException extends Exception {
private final LockFile lockFile;
private final String inMethod;
/**
* Constructs a new LockFile.BaseException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up several 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.
*
* @return representation of lockFile and
* inMethod, as String object
*/
public String getMessage() { // override
return "lockFile: " + lockFile + " method: " + inMethod;
}
/**
* Getter for inMethod property.
*
* @return name of method in which exception originally occurred
*/
public String getInMethod() {
return this.inMethod;
}
/**
* Getter for lockFile property.
*
* @return the underlying LockFile object
*/
public LockFile getLockFile() {
return this.lockFile;
}
}
/**
* Thrown when canonicalization of a LockFile object's target
* file path fails.
*
* 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 FileCanonicalizationException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up several 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 IOException.
*
* @return Value of property reason.
*/
public IOException 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 access to a LockFile object's target file raises a
* Java SecurityException.
*
* This can occur if a required system property value cannot be accessed, or
* if a security manager exists and its {@link
* java.lang.SecurityManager#checkRead} method denies read access to a
* file; or if its {@link
* java.lang.SecurityManager#checkRead(java.lang.String)}
* method does not permit verification of the existence of all necessary
* parent directories; or if its {@link
* java.lang.SecurityManager#checkWrite(java.lang.String)}
* method does not permit all necessary parent directories to be
* created.
*
*/
public static final class FileSecurityException extends BaseException {
private final SecurityException reason;
/**
* Constructs a new 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 several 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 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
* acquisition.
*
* 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.
*
*/
public static final class LockHeldExternallyException
extends BaseException {
private final long read;
private final long heartbeat;
/**
* Constructs a new LockHeldExternallyException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up several 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 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 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.
*
* @return representation of lockFile, inMethod,
* 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 LockFile object's target file raises an
* unexpected EOFException.
*/
public static final class UnexpectedEndOfFileException
extends BaseException {
private final EOFException reason;
/**
* Constructs a new UnexpectedEndOfFileException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up several 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 EOFException.
*
* @return Value of property reason.
*/
public EOFException 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 access to a LockFile object's target file raises an
* unexpected IOException other than EOFException.
*/
public static final class UnexpectedFileIOException extends BaseException {
private final IOException reason;
/**
* Constructs a new UnexpectedFileIOException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up several 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 IOException.
*
* @return Value of property reason.
*/
public IOException 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 access to a LockFile object's target file raises an
* unexpected FileNotFoundException.
*/
public static final class UnexpectedFileNotFoundException
extends BaseException {
private final FileNotFoundException reason;
/**
* Constructs a new UnexpectedFileNotFoundException.
*
* @param lockFile the underlying LockFile object
* @param inMethod the name of the method in which the exception
* was originally thrown (may be passed up several 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 several 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;
}
/**
* Retrieves 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 several 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.byteArrayToHexString(magic)
+ "'");
return message;
}
/**
* Retrieves a copy of the actual MAGIC value read from the
* file.
*
* @return a copy of the actual MAGIC value read from the file
*/
public byte[] getMagic() {
return (magic == null) ? null
: this.magic.clone();
}
}
}