Top Description Inners Fields Constructors Methods
org.hsqldb.persist

public Class LockFile

extends Object
Class Inheritance
Imports
java.io.DataInputStream, .EOFException, .File, .FileInputStream, .FileNotFoundException, .IOException, .RandomAccessFile, org.hsqldb.DatabaseManager, .HsqlException, org.hsqldb.error.Error, .ErrorCode, org.hsqldb.lib.FileUtil, .HsqlTimer, .StringConverter, org.hsqldb.types.HsqlDateTime

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:
  1. 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.
  2. 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.
  3. 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.
  4. The generic rules used to acquire a cooperative lock condition are as follows:
    1. If a lock condition is already held by this object, do nothing and signify that the lock attempt was successful, else...
    2. 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.
    3. For each poll:
      1. 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...
      2. 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).
      3. 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 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.

      4. Test the file's length, fast failing the current poll if the length cannot be determined or it is not the expected value.
      5. Open a stream to read the file's MAGIC and heartbeat timestamp values, fast failing the current poll if the stream cannot be opened.
      6. Test the file's MAGIC value, failing the current poll if the value cannot be read or it is not the expected value.
      7. 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).
    4. 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...

    5. 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.
  5. The generic rules used to release a cooperative lock condition are:
    1. If a lock condition is not currently held, do nothing and signify success, else...
    2. 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, doOptionalLockActions() and 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 newLockFile().

In particular, if 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 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 POLL_RETRIES_PROPERTY and 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 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, 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:

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)
Since
1.7.2
Version
2.5.1

Nested and Inner Type Summary

Modifier and TypeClass and Description
public abstract static class
LockFile.BaseException

Base exception class for lock condition specific exceptions.

public static class
LockFile.FileCanonicalizationException

Thrown when canonicalization of a LockFile object's target file path fails.

public static class
LockFile.FileSecurityException

Thrown when access to a LockFile object's target file raises a Java SecurityException.

private class
LockFile.HeartbeatRunner

For internal use only.

public static class
LockFile.LockHeldExternallyException

Thrown when an externally held lock condition prevents lock acquisition.

public static class
LockFile.UnexpectedEndOfFileException

Thrown when access to a LockFile object's target file raises an unexpected EOFException.

public static class
LockFile.UnexpectedFileIOException

Thrown when access to a LockFile object's target file raises an unexpected IOException other than EOFException.

public static class
LockFile.UnexpectedFileNotFoundException

Thrown when access to a LockFile object's target file raises an unexpected FileNotFoundException.

public static class
LockFile.WrongLengthException

Thrown when it is detected that a LockFile object's target file does not have the expected length.

public static class
LockFile.WrongMagicException

Thrown when it is detected that a LockFile object's target file does not start with the expected MAGIC value.

Field Summary

Modifier and TypeField and Description
private String
cpath

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.

protected File
file

Canonical reference to this object's lock file.

public static final long
HEARTBEAT_INTERVAL

Arbitrary period, in milliseconds, at which heartbeat timestamps are written to this object's lock file.

public static final long
protected volatile boolean
locked

Indicates presence or absence of the cooperative lock condition.

protected static final byte[]
MAGIC

Value written at the beginning of an HSQLDB lock file to distinguish it from other file types.

public static final boolean
NIO_FILELOCK_AVAILABLE

Statically computed indication of java.nio.channels.FileLock runtime availability.

public static final Class<?>
NIO_LOCKFILE_CLASS

Statically computed reference to the NIOLockFile class.

public static final String
POLL_INTERVAL_PROPERTY

System property that can be used to override the default number of milliseconds between each heartbeat poll retry.

public static final int
POLL_RETRIES_DEFAULT

Number of retries used by default in pollHeartbeat.

public static final String
POLL_RETRIES_PROPERTY

System property that can be used to override the default number of heartbeat poll retries.

protected volatile RandomAccessFile
raf

A RandomAccessFile constructed from this object's canonical file reference.

protected static final HsqlTimer
timer

The timed scheduler with which to register this object's heartbeat task.

private volatile Object
timerTask

Opaque reference to this object's heartbeat task.

public static final boolean
USE_NIO_FILELOCK_DEFAULT

Whether java.nio file locking is attempted by default.

public static final String
USE_NIO_FILELOCK_PROPERTY

System property that can be used to control whether nio file locking is attempted.

public static final int
USED_REGION

Size, in bytes, of the region at the beginning of a lock file that is actually used to record lock information.

Constructor Summary

AccessConstructor and Description
protected
LockFile()

To allow subclassing without exposing a public constructor.

Method Summary

Modifier and TypeMethod and Description
private void
checkHeartbeat(boolean
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
withCreateNewFile
)

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.

private void
checkMagic(final DataInputStream
the stream to check
dis
)

Checks whether the given DataInputStream contains the MAGIC value.

private void
closeRAF()

Closes this object's RandomAccessFile.

protected boolean

Returns:

true if optional lock actions are performed and they succeed, else false
doOptionalLockActions
()

Provides any optional locking actions for the tryLock() template method.

protected boolean

Returns:

true if optional release actions are performed and they succeed, else false
doOptionalReleaseActions
()

Provides any optional release actions for the tryRelease() template method.

public final boolean

Returns:

true if this object is equal to the obj argument; false otherwise.
equals
(final Object
the reference object with which to compare.
obj
)

Overrides java.lang.Object.equals.

Tests whether some other object is "equal to" this one.

public final String

Returns:

the canonical path of this object's lock file.
getCanonicalPath
()

Retrieves the canonical path of this object's lock file, as a String object.

public long

Returns:

the interval, in milliseconds, that pollHeartbeat waits between failed invocations of checkHeartbeat
getPollHeartbeatInterval
()

Retrieves the interval, in milliseconds, that pollHeartbeat waits between failed invocations of checkHeartbeat.

public int

Returns:

the number of times checkHeartbeat may fail before pollHeartbeat fails as a consequence.
getPollHeartbeatRetries
()

Retrieves the number of times checkHeartbeat may fail before pollHeartbeat fails as a consequence.

public final int

Returns:

a hash code value for this object.
hashCode
()

Overrides java.lang.Object.hashCode.

Retrieves the hash code value for this object.

public final boolean

Returns:

true if this object has successfully obtained and is still holding (has not yet released) a lock condition, else false
isLocked
()

Retrieves whether this object has successfully obtained and is still holding (has not yet released) a cooperative lock condition on its lock file.

public static boolean

Returns:

true if there is currently something preventing the acquisition of a cooperative lock condition using the specified path, else false
isLocked
(final String
the path to test
path
)

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.

public boolean

Returns:

true if this object holds a valid lock condition on its lock file; else false
isValid
()

Retrieves whether this object holds a valid lock condition on its lock file.

public static LockFile

Returns:

a LockFile instance initialized with a File object whose path is the one specified by the given path argument.
newLockFile
(final String
the path of the File object with which the retrieved LockFile object is to be initialized
path
)

Retrieves a LockFile instance, initialized with a File object whose path is the canonical form of the one specified by the given path argument.

public static LockFile

Returns:

a new LockFile object holding a cooperative lock condition upon the file with the given path, appended with the extension '.lck'
newLockFileLock
(final String
of the lock file, to which will be appended '.lck'
path
)

org.hsqldb.persist.Logger#acquireLock(java.lang.String) delegate.

private static LockFile

Returns:

a new NIOLockFile, or null if not available under the current runtime environment
newNIOLockFile
()

Retrieves a new NIOLockFile, or null if not available under the current runtime environment.

private void
openRAF()

Opens (constructs) this object's RandomAccessFile.

private void
pollHeartbeat()

Polls the underlying lock file to determine if a lock condition exists.

private long

Returns:

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.
readHeartbeat
()

Retrieves the last written hearbeat timestamp from this object's lock file.

private void
setPath(String
the abstract path representing the file this object is to use as its lock file
path
)

Initializes this object with a File object whose path has the canonical form of the given path argument.

private void
startHeartbeat()

Schedules the lock heartbeat task.

private void
stopHeartbeat()

Cancels the lock heartbeat task.

public String

Returns:

a String representation of this object.
toString
()

Overrides java.lang.Object.toString.

Retrieves a String representation of this object.

protected String

Returns:

an implementation-specific tail value for the toString() method
toStringImpl
()

Retrieves an implementation-specific tail value for the toString() method.

public final boolean

Returns:

true if and only if a lock condition is obtained successfully; false otherwise. In general, an exception will always be thrown if a lock condition cannot be obtained for any reason
tryLock
()

Attempts to obtain a cooperative lock condition upon this object's lock file.

public final boolean

Returns:

true if this object does not currently hold a lock condition or the lock is released completely (including successful file deletion), else false.
tryRelease
()

Attempts to release any cooperative lock condition this object may hold upon its lock file.

private void
writeHeartbeat()

Writes the current hearbeat timestamp value to this object's lock file.

private void
writeMagic()

Writes the MAGIC value to this object's lock file that distinguishes it as an HSQLDB lock file.

Inherited from java.lang.Object:
clonefinalizegetClassnotifynotifyAllwaitwaitwait