All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hsqldb.persist.LockFile Maven / Gradle / Ivy

There is a newer version: 2.7.2
Show newest version
/* Copyright (c) 2001-2019, 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:

* *

    *
  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 {@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.

        * *

      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, {@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 though 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.

* * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net) * @version 2.5.0 * @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. */ public static final boolean USE_NIO_FILELOCK_DEFAULT = false; /** * System property that can be used to control whether nio file locking is * attempted. */ public static final String USE_NIO_FILELOCK_PROPERTY = "hsqldb.lockfile.nio.filelock"; /** * Statically computed indication of 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. * * @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:

* *

    *
  1. 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).

    * *

  2. 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.

    * *

  3. If a Java security exception is thrown while testing for existence, * it is rethrown as a FileSecurityException. * *
  4. Read the file's length. * *
  5. 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). * *
  6. 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). * *
  7. Open an input steam to read the file's MAGIC and heartbeat * timestamp values. * *
  8. If a file not found exception is thrown above, it is rethrown as an * UnexpectedFileNotFoundException (we've already tested for * existence). * *
  9. 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). * *
  10. Read the MAGIC value. * *
  11. 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?). * *
  12. 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?) * *
  13. 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). * *
  14. Read the heartbeat timestamp. * *
  15. 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). * *
  16. 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?). * *
  17. 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?) * *
  18. 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. * *
  19. 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:

* *

    *
  1. locked == false. *
  2. raf is a non-null instance that can be used to get a * FileChannel instance, if desired. *
  3. the underlying file either did not exist before invoking * openRAF() or it was a valid but stale HSQLDB lock file * because it: * *
      *
    1. did exist, *
    2. was readable on USED_REGION, *
    3. had the expected length and MAGIC value and *
    4. 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:

* *

    *
  1. This method is called once and only once per * Lockfile instance. * *
  2. It is always the first method called after * LockFile construction * *
  3. 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:

* *

    *
  1. retries is assigned POLL_RETRIES_DEFAULT. * *
  2. retries is assigned Integer.getInteger(POLL_RETRIES_PROPERTY, * retries) inside a try-catch block to silently ignore any security * exception. * *
  3. 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:

* *

    *
  1. interval is assigned 10 + (HEARTBEAT_INTERVAL_PADDED * getPollHeartbeatRetries()) * *
  2. interval is assigned Long.getLong(POLL_INTERVAL_PROPERTY, * interval), inside a try-catch block, to silently ignore any security * exception. * *
  3. 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(); } } }