InMemoryLockManager.java

/*
** Module   : InMemoryLockManager.java
** Abstract : In-memory, record-level lock manager; synchronizes concurrent
**            access to records within a single process
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 ECF 20051010   @23076 Created initial version. An in-memory lock
**                           manager implementation. Manages pessimistic,
**                           row-level locking in support of the Progress
**                           locking semantic. Locks are limited in scope
**                           to the current server process.
** 002 ECF 20051104   @23241 Added queryLock method. Queries lock type
**                           held by the current context on a specific
**                           record.
** 003 ECF 20051110   @23381 Removed database from lock and queryLock
**                           signatures. This reflects a change in the
**                           LockManager API. Added setDatabase method to
**                           allow database name to be preserved for
**                           logging. It was unnecessary to track database
**                           name with each lock, since a LockManager
**                           instance is now database-specific.
** 004 ECF 20051207   @23755 Use more specific exception type if lock is
**                           unavailable. PersistenceException replaced
**                           with LockUnavailableException.
** 005 ECF 20060117   @23961 Report lock conflict to user. A message is
**                           sent via the LogicalTerminal before the
**                           current thread waits.
** 006 ECF 20060120   @24041 Replaced ThreadLocal with ContextLocal.
**                           Implemented cleanup routine to release all
**                           locks held by an expired context.
** 007 ECF 20060223   @24718 Clear conflict message. Added call to
**                           LogicalTerminal.hideMessage if session was
**                           blocked waiting for lock and is freed.
** 008 ECF 20060610   @27104 Fixed offline mode. NPEs had been introduced
**                           when this class was integrated with the P2J
**                           server environment.
** 009 ECF 20060728   @28267 Minor changes. Prevent setting user wait
**                           message more than once. Improved javadoc.
** 010 ECF 20060918   @29666 Minor logging improvement during context
**                           cleanup.
** 011 ECF 20070412   @32980 Retrofitted LockManager interface changes.
**                           Extracted LockRecord to top level class
**                           RecordIdentifier. Also improved logging
**                           during context cleanup.
** 012 ECF 20070911   @35288 Replaced database name string with Database
**                           information object. Required for non-local
**                           database connections.
** 013 ECF 20071213   @36376 Added lock contention debug logging. When
**                           LockManager logger is set to FINEST, each
**                           lock acquisition includes stack trace data.
**                           Upon contention for a record lock, these data
**                           are logged, so we can see who is requesting a
**                           lock who is holding it, and where in the code
**                           the lock was requested and acquired.
** 014 ECF 20071214   @36379 Throw EndConditionException when user aborts
**                           lock request. Previously we were throwing
**                           StopConditionException, which was never
**                           caught by business logic. Note: this appears
**                           to be a more correct approach, but further
**                           testing is required.
** 015 ECF 20071220   @36577 Fixed lock leak detection false positive. We
**                           were not removing an entry from the map of
**                           locally locked records in certain release
**                           cases. Also refactored context locals into an
**                           inner class (Context) to minimize context
**                           local variable lookups. Integrated generics.
** 016 ECF 20080221   @37140 Fixed NPE in contextCleanup().
** 017 ECF 20080506   @38218 Fixed core algorithm, which was not properly
**                           allowing up/downgrade transitions between
**                           SHARE and EXCLUSIVE lock types. Also added
**                           isHeadless(). Used to determine whether a UI
**                           exists to enable a message to be sent to the
**                           user in lock contention situations. Improved
**                           built-in test harness to test up/downgrades.
** 017 ECF 20080506   @38219 The current implementation of UI detection is
**                           flawed, in that it is server-based, rather
**                           than client-based. A problem will occur when
**                           a non-standard client (which does not support
**                           a UI) is attached to a standard server.
** 018 ECF 20080806   @39323 Added support for 64-bit primary keys. Only
**                           applies to test harness in main(). Core code
**                           is agnostic to data type of record ID.
** 019 ECF 20081106   @40353 Added logging of locking above a configurable
**                           time threshold. If a lock is held for more
**                           than the threshold (set in the directory), a
**                           warning and stack trace are logged.
** 020 ECF 20081113   @40452 Fixed lock leak. Newly created locks were not
**                           being tracked by the local context, so they
**                           could not be cleaned up in the event of an
**                           abrupt end of the session. Improved logging.
** 021 ECF 20081204   @40802 Changed warning threshold logging. We now use
**                           the same approach as the Persistence class
**                           uses.
** 022 ECF 20090128   @41241 Fixed ConcurrentModificationException during
**                           cleanup. This exception could be raised as
**                           leaked locks were being released.
** 023 ECF 20090801   @43498 Added support for lock administration. Added
**                           inner class which implements LockAdministrator,
**                           and new getLockAdministrator() method to main
**                           class. Modified internal data structures to
**                           report lock information at a moment in time.
** 024 CA  20091119   @44430 If a record is locked by another subject, now the
**                           status message contains the subject name too. 
**                           Also, the status line which contains the record
**                           locked message is no longer trimmed to 63 chars.
**                           Instead of session IDs, now it uses a session 
**                           token to describe a locker (so subject name will
**                           be provided too).
** 025 CA  20091119   @44437 The record lock info was modified to receive
**                           session tokens instead of session IDs.
** 026 CA  20091203   @44474 Rolled back H014 (@36379) - correct behavior is
**                           to throw a StopConditionException when user 
**                           aborts lock request.
** 027 VMN 20130830          Added lockTable method.
** 028 ECF 20131028          Added support for lock metadata. Moved to lock sub-package.
** 029 ECF 20150111          Replaced Apache commons logging with J2SE logging.
** 030 ECF 20150130          Ensure table lock info is not collected by lock admin.
** 031 ECF 20150305          Fixed implementation of automatic SHARE locking of tables during
**                           record locking; we were leaking table locks, which were only being
**                           removed at context cleanup.
** 032 ECF 20160120          Fixed synchronization within lockTable method.
** 033 ECF 20160819          Added APIs to permit wait timeouts to be specified. Fixed lock
**                           notification algorithm to correctly determine old lock types.
** 034 SBI 20170225          Moved administration profile type, access control list type and
**                           record lock type to their own modules respectively.
** 035 ECF 20200906          Use parameterized record identifiers.
** 036 IAS 20200906          Fixed type parameters.
**     CA  20201003          Use an identity HashSet where possible.
**     ECF 20210228          Minor format cleanup.
**     TJD 20220504          Upgrade do Java 11 minor changes
**     SVL 20230113          Improved performance by replacing some "for-each" loops with indexed "for" loops.
** 037 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 038 GBB 20230825          SecurityManager session methods calls updated.
** 039 OM  20240909          Improved Database API. Concurrent multitenancy functionality implementation.
** 040 CA  20241030          99% of the time a record will be locked in a single session; in such cases, keep
**                           that single LockStatus reference and move to a set only when another session 
**                           establishes a lock.
** 041 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
** 042 ES  20250407          Handle StopConditionException throw ErrorManager.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist.lock;

import java.io.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;

/**
 * An implementation of the Progress row-level, pessimistic locking semantic.
 * Database level locking is not performed by this class;  that is left to
 * clients of this class if they so choose.  This class is used to grant or
 * deny permission to a particular lock requester for a given record in a
 * given database table at a particular point in time.  Depending upon the
 * parameters of a request to obtain a lock, that request may return
 * normally, indicating the requested lock has been acquired, or it may
 * result in an <code>ErrorConditionException</code> being thrown.  A
 * successful lock acquisition does not necessarily return immediately;
 * rather, the current thread may be blocked until such time as the lock
 * becomes available.
 * <p>
 * It is important to note that this level of lock management occurs outside
 * of the database server.  There is a fundamental assumption that <i>all
 * attempts to lock database records pessimistically are routed through this
 * layer <b>before</b> client code attempts to obtain (or release) any
 * database-level pessimistic locks.</i>  If this fundamental assumption is
 * violated, the Progress locking semantic cannot be guaranteed.  While this
 * is a significantly limiting factor for concurrent, external access to the
 * database, it is necessary for the P2J environment to accurately mimic an
 * application's pre-conversion behavior, since Progress locking behavior is
 * different than that which is provided by SQL semantics.
 * <p>
 * A further assumption of this implementation is that changes to the lock
 * status of a record for a particular context are only made within that
 * context, and in fact, within that context, it is further assumed that
 * changes are only made on a single thread.  Otherwise, work continuing on
 * a thread with an assumption of a certain lock status could be corrupted if
 * a separate thread makes a change to that lock status.
 * <p>
 * Because this implementation manages all locks in memory and does not
 * persist lock state, an abnormal or abrupt end of the server process will
 * result in the loss of the current state of all record locks.
 * <p>
 * <b>Implementation Notes:</b><br>
 * The locking algorithm implemented by this class is naturally biased toward
 * share locks over exclusive locks. In a share-lock intensive environment,
 * exclusive locks may be starved out, as multiple threads may acquire and
 * release share locks. As long as any one thread holds a share lock, an
 * exclusive lock cannot be acquired. There is no fairness algorithm built
 * into this implementation to prevent such a scenario. When a lock is
 * released (or downgraded) in a contention situation, all blocked threads are
 * notified of the lock status change. The first thread to be run after a lock
 * status change is given the first opportunity to acquire its lock. It is up
 * to the JVM implementation (which typically will defer to the operating
 * system thread scheduler) as to the order in which waiting threads are
 * notified and run.
 * @param <T>
 *        table reference type 
 */
public class InMemoryLockManager<T extends Serializable>
implements LockManager<T>
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(LockManager.class.getName());
   
   /** Is warn level logging enabled? */
   private static final boolean warn = LOG.isLoggable(Level.WARNING);
   
   /** Is debug level logging enabled? */
   private static final boolean debug = LOG.isLoggable(Level.FINE);
   
   /** Is trace level logging enabled? */
   private static final boolean trace = LOG.isLoggable(Level.FINEST);
   
   /** Is offline mode? */
   private final boolean offline = (SecurityManager.getInstance() == null);
   
   /** Master lock table of lock status by {@code RecordIdentifier} */
   private final Map<RecordIdentifier<T>, LockStatus<T>> lockTable = new HashMap<>();
   
   /** Map of reference counts for records in the process of being locked */
   private final Map<RecordIdentifier<T>, Integer> inPlay = new HashMap<>();
   
   /** Lock administrator */
   private final Admin admin = new Admin();
   
   /** Current session context */
   private final ContextLocal<Context<T>> context = new ContextLocal<Context<T>>()
   {
      protected Context<T> initialValue() { return (new Context<>(offline)); }
      protected void cleanup(Context<T> ctx) { contextCleanup(ctx); }
   };
   
   /** Database whose record locks are to be managed */
   private Database database = null;
   
   /** Optional lock listener */
   private LockListener listener = null;
   
   /** Next lock event ID, guaranteed to be sequential and ascending */
   private long nextEventID = 0L;
   
   /**
    * Threshold in milliseconds above which a lock hold time is reported;
    * must be non-negative to enable reporting.
    */
   private long warnThreshold = -1L;
   
   /**
    * Constructor.
    */
   public InMemoryLockManager()
   {
   }
   
   /**
    * Helper method to format stack trace data into a string.
    * 
    * @param   buf
    *          Buffer into which to format data.
    * @param   throwable
    *          Object which contains stack trace element array.
    */
   private static void printStackTrace(StringBuilder buf, Throwable throwable)
   {
      for (StackTraceElement elem : throwable.getStackTrace())
      {
         buf.append("\n         ");
         buf.append(elem);
      }
   }
   
   /**
    * Write a log entry to reflect a lock transition from one lock state to another.
    *
    * @param   lockType
    *          The new type of lock;  <code>LockType.NONE</code> if lock was released.
    * @param   database
    *          Database.
    * @param   ident
    *          Record descriptor.
    */
   private static <T extends Serializable> void logTransition(LockType lockType,
                                                              Database database,
                                                              RecordIdentifier<T> ident)
   {
      logTransition(lockType, null, database, ident, 0L);
   }
   
   /**
    * Write a log entry to reflect a lock transition from one lock state to
    * another.  If an old lock type is provided, it is logged as well.  If a
    * positive, elapsed time is provided, that time is logged, along with a
    * stack trace.
    *
    * @param   lockType
    *          The new type of lock;  <code>LockType.NONE</code> if lock was
    *          released.
    * @param   oldLockType
    *          The lock type before the transition.
    * @param   database
    *          Database.
    * @param   ident
    *          Record descriptor.
    * @param   elapsed
    *          For a downgrade or release transition, the amount of time which
    *          has elapsed, in milliseconds, since <code>oldLockType</code>
    *          was acquired.
    */
   private static <T extends Serializable> void logTransition(LockType lockType,
                                                              LockType oldLockType,
                                                              Database database,
                                                              RecordIdentifier<T> ident,
                                                              long elapsed)
   {
      StringBuilder buf = new StringBuilder("[");
      buf.append(Utils.describeContext());
      buf.append("] --> ");
      buf.append(database);
      buf.append(".");
      buf.append(ident);
      if (lockType != LockType.NONE)
      {
         buf.append(" LOCK ").append(lockType);
      }
      else
      {
         buf.append(" UNLOCK");
      }
      
      if (oldLockType != null)
      {
         buf.append(" (from ");
         buf.append(oldLockType);
         buf.append(")");
      }
      
      if (elapsed > 0L)
      {
         buf.append(" after ");
         buf.append(elapsed);
         buf.append(" milliseconds");
         
         LOG.log(Level.WARNING, buf.toString(), new Throwable());
      }
      else
      {
         LOG.log(Level.FINEST, buf.toString());
      }
   }
   
   /**
    * Set the name of the physical database with which this lock manager is
    * associated.
    *
    * @param   database
    *          Database whose record locks are to be managed.
    */
   public void setDatabase(Database database)
   {
      this.database = database;
      
      // retrieve report threshold value from directory
      if (!offline)
      {
         String dbID = database.getId();
         String spec = "database/%s/p2j/warn_threshold/enable";
         String path = String.format(spec, dbID);
         boolean warn = Utils.getDirectoryNodeBoolean(null, path, false, false);
         
         if (warn)
         {
            String dbType = (database.isDirty() ? "dirty" : "primary");
            spec = "database/%s/p2j/warn_threshold/lock_manager/%s";
            path = String.format(spec, dbID, dbType);
            warnThreshold = Utils.getDirectoryNodeInt(null, path, -1, false);
            
            if (LOG.isLoggable(Level.INFO))
            {
               StringBuilder buf = new StringBuilder("Warn threshold for ");
               buf.append(database);
               buf.append(":  ");
               buf.append(warnThreshold);
               
               LOG.log(Level.INFO, buf.toString());
            }
         }
      }
   }
   
   /**
    * Register a lock listener, which will receive notifications of changes in lock status for any
    * record managed by this object.
    * 
    * @param   listener
    *          Listener to be registered or <code>null</code> to deregister the current listener.
    */
   public void setLockListener(LockListener listener)
   {
      this.listener = listener;
   }
   
   /**
    * Retrieve the {@link LockAdministrator} object associated with this
    * <code>LockManager</code> instance.
    * <p>
    * To ensure a caller has sufficient rights to access administrative lock
    * functions, the implementation of this method confirms with the
    * administrative server before returning any lock information.
    * 
    * @return  <code>LockAdministrator</code> instance, or <code>null</code>
    *          if the caller does not have administrative rights.
    */
   public LockAdministrator getLockAdministrator()
   {
      if (AdminServerImpl.getSecurityAdmin().adminAccess())
      {
         return admin;
      }
      
      return null;
   }
   
   /**
    * Lock or release a single database record in the current context. The record is uniquely
    * identified by a table name and record ID (i.e., primary key).  Whether the record is locked
    * or released depends upon the lock type requested: {@code LockType.NONE} is used to release
    * a record; any other lock type is used to lock a record.  Specifying {@code LockType.NONE}
    * for a record not locked in the current context is logically a no-op, though some overhead
    * is required for the check, and the caller may block waiting on the lock table monitor.
    * <p>
    * Before obtaining any record lock, {@code lockTable(LockType.SHARE, table, true)} is
    * invoked to acquire a share lock on the table. After releasing any record lock, {@code
    * lockTable(LockType.NONE, table, true)} is invoked to release that lock.
    * <p>
    * Any normal return from this method indicates the requested lock was acquired (or released)
    * successfully. Any error will result in an exceptional return. In the event a
    * record cannot be locked due to a conflicting lock held by another context, the current
    * thread will block until the lock is acquired, unless the requested lock is a "no-wait"
    * variant, in which case an exception is thrown.
    * <p>
    * This method also operates in a check-only mode (if {@code update} is set to {@code false})
    * where it goes through the motions of obtaining the specified lock type, but does not
    * actually change the lock status. This is at best an unreliable check, since another
    * context can change the lock status as soon as the lock status monitor is released, rendering
    * the check stale. Nevertheless, it exists to mimic the effect of the lock type option to the
    * Progress can-find function.
    * 
    * @param   lockType
    *          The type of lock requested; {@code LockType.NONE} to release a lock.
    * @param   ident
    *          ID which uniquely identifies the record being queried.
    * @param   update
    *          {@code true} if the lock state for the current record should be updated; else
    *          {@code false}.  This is set to {@code false} if the caller just wants to know
    *          whether a lock is available, without actually changing its status.
    *
    * @throws  LockUnavailableException
    *          if a "no-wait" lock cannot be acquired immediately.
    */
   public void lock(LockType lockType, RecordIdentifier<T> ident, boolean update)
   throws LockUnavailableException
   {
      lock(lockType, ident, update, 0L);
   }
   
   /**
    * Lock or release a single database record in the current context. The record is uniquely
    * identified by a table name and record ID (i.e., primary key). Whether the record is locked
    * or released depends upon the lock type requested: {@code LockType.NONE} is used to release
    * a record; any other lock type is used to lock a record. Specifying {@code LockType.NONE}
    * for a record not locked in the current context is logically a no-op, though some overhead
    * is required for the check, and the caller may block waiting on the lock table monitor.
    * <p>
    * Before obtaining any record lock, {@code lockTable(LockType.SHARE, table, true)} is
    * invoked to acquire a share lock on the table. After releasing any record lock, {@code
    * lockTable(LockType.NONE, table, true)} is invoked to release that lock.
    * <p>
    * Any normal return from this method indicates the requested lock was acquired (or released)
    * successfully. Any error or timeout will result in an exceptional return. In the event a
    * record cannot be locked due to a conflicting lock held by another context, the current
    * thread will block until the lock is acquired, unless either (a) the requested lock is a
    * "no-wait" variant; or (b) a positive timeout value was provided and at least that number
    * of milliseconds has elapsed without acquiring the lock. In either case, an exception is
    * thrown as described below.
    * <p>
    * This method also operates in a check-only mode (if {@code update} is set to {@code false})
    * where it goes through the motions of obtaining the specified lock type, but does not
    * actually change the lock status. This is at best an unreliable check, since another
    * context can change the lock status as soon as the lock status monitor is released, rendering
    * the check stale. Nevertheless, it exists to mimic the effect of the lock type option to the
    * Progress can-find function.
    * 
    * @param   lockType
    *          The type of lock requested; {@code LockType.NONE} to release a lock.
    * @param   ident
    *          ID which uniquely identifies the record being queried.
    * @param   update
    *          {@code true} if the lock state for the current record should be updated; else
    *          {@code false}.  This is set to {@code false} if the caller just wants to know
    *          whether a lock is available, without actually changing its status.
    * @param   timeout
    *          Number of milliseconds to wait before throwing {@link LockTimeoutException},
    *          or 0L to wait indefinitely.
    * 
    * @throws  LockUnavailableException
    *          if a "no-wait" lock cannot be acquired immediately.
    * @throws  LockTimeoutException
    *          if a non-zero timeout was provided and the requested lock has not been acquired
    *          by the time that period has elapsed.
    */
   public void lock(LockType lockType, RecordIdentifier<T> ident, boolean update, long timeout)
   throws LockUnavailableException,
          LockTimeoutException
   {
      boolean noLock = LockType.NONE.equals(lockType);
      
      // if no update to the lock state is requested and the lock type is NONE, no need to check
      // further
      if (noLock && !update)
      {
         return;
      }
      
      T table = ident.getTable();
      LockType previousTableLock = null;
      
      try
      {
         // if we are acquiring (or testing whether we could acquire) any record lock, we have to
         // get a share lock on the table first
         if (!noLock)
         {
            LockType prev = lockTable(LockType.SHARE, table, update);
            if (update)
            {
               previousTableLock = prev;
            }
         }
         
         // call lockImpl for normal record lock processing
         lockImpl(lockType, ident, update, timeout);
      }
      catch (LockUnavailableException exc)
      {
         // if record lock acquisition failed, but table lock was modified, we have to set table
         // lock back to what it was previously
         if (previousTableLock != null && !previousTableLock.isShare())
         {
            try
            {
               lockTable(previousTableLock, table, update);
            }
            catch (LockUnavailableException exc2)
            {
               // should never get here, since NO-WAIT variant will not have been requested
            }
         }
         
         throw exc;
      }
      finally
      {
         // if lock type is NONE (the record lock has been released) and this context has only 1
         // remaining lock for the current table, it represents the table lock itself; release it
         if (noLock)
         {
            // update must be true, or we wouldn't be here
            
            Context<T> local = context.get();
            Map<RecordIdentifier<T>, LockStatus<T>> statusMap = local.locked.get(table);
            if (statusMap != null && statusMap.size() == 1)
            {
               try
               {
                  lockTable(LockType.NONE, table, update);
               }
               catch (LockUnavailableException exc)
               {
                  // will not be thrown for lock release
               }
            }
         }
      }
   }
   
   /**
    * Attempt to obtain the specified lock type on a table. If the lock cannot be obtained
    * immediately, the current thread will block until the lock becomes available and is
    * obtained, unless the requested lock type is a "no-wait" lock.  In the latter case,
    * an exception is raised, indicating that the lock is not available. The current thread
    * continues once the lock is obtained.
    * <p>
    * A lock currently held can be released by specifying <code>LockType.NONE</code> for the
    * <code>lockType</code> parameter.
    * <p>
    * The normal return of this method indicates that the lock request (or lack thereof)
    * completed successfully, and that the current context now holds a lock of the requested
    * type.
    *
    * @param   lockType
    *          Type of lock to be obtained.  <code>LockType.NONE</code> is used to release an
    *          existing lock, and to continue with no lock.
    * @param   table
    *          Table identifier.
    * @param   update
    *          <code>true</code> if the lock state for the current table should be updated;
    *          else <code>false</code>.
    * 
    * @return  The lock type held by the current context on the given table before this method
    *          was invoked.
    * 
    * @throws  LockUnavailableException
    *          if a "no-wait" lock cannot be acquired immediately.
    */
   public LockType lockTable(LockType lockType, T table, boolean update)
   throws LockUnavailableException
   {
      boolean noLock = LockType.NONE.equals(lockType);
      
      // if no update to the lock state is requested and the lock type is NONE, no need to check
      // further
      if (noLock && !update)
      {
         return LockType.NONE;
      }
      
      TableIdentifier<T> ident = new TableIdentifier<>(table);
      LockStatus<T> status;
      synchronized (lockTable)
      {
         status = lockTable.get(ident);
      }
      
      // remember current lock type for this context
      LockType currentLockType = (status == null) ? LockType.NONE : status.lockType;
      
      // attempt to change lock if requested type is different than current type
      if (!currentLockType.equals(lockType))
      {
         lockImpl(lockType, ident, update, 0L);
      }
      
      // return previous lock type (so that calling code knows what to set it back to after bulk
      // delete or if record lock attempt fails)
      return currentLockType;
   }
   
   /**
    * Query the lock type currently held by the current context for the
    * specified database record.
    * <p>
    * This method will always return the <b>non-</b><code>NO_WAIT</code>
    * variants of a lock type.  That is, even if the actual lock type is
    * <code>LockType.EXCLUSIVE_NO_WAIT</code> or
    * <code>LockType.SHARE_NO_WAIT</code>, the simpler types of
    * <code>LockType.EXCLUSIVE</code> and <code>LockType.SHARE</code>,
    * respectively, are returned.  The lack of a lock returns
    * <code>LockType.NONE</code> rather than <code>null</code>.
    *
    * @param   ident
    *          ID which uniquely identifies the record being queried.
    *
    * @return  The lock type currently held by the current context, never
    *          <code>null</code>.
    */
   public LockType queryLock(RecordIdentifier<T> ident)
   {
      LockStatus<T> status = null;
      synchronized (lockTable)
      {
         // Check whether record is locked (by anyone).
         status = lockTable.get(ident);
      }
      
      // Check whether record is locked by current context.  If so, return
      // lock type.  No need to synchronize on status here, because we are
      // only querying the status of the current context, which cannot be
      // changed by a concurrently running context.
      if (status != null)
      {
         Context<T> local = context.get();
         if (status.isLockedBy(local.locker))
         {
            return status.getLockType().toWaitVariant();
         }
      }
      
      // Record is not locked by current context.
      return LockType.NONE;
   }
   
   /**
    * Release all locks represented by the specified set of lock status
    * objects.  This routine is called during context cleanup.  Generally,
    * if exiting the context cleanly, <code>locks</code> will be empty.
    * However, if the context is aborted abnormally, due to a dropped client,
    * for instance, the set may contain some entries which must be cleaned
    * up to prevent ghost record locks.
    *
    * @param   local
    *          Current context's state.
    */
   private void contextCleanup(Context<T> local)
   {
      boolean clean = local.locked.isEmpty();
      
      if (clean && !warn)
      {
         // nothing to clean up and nothing to report
         return;
      }
      
      String prefix = null;
      
      if (debug || (warn && !clean))
      {
         StringBuilder buf = new StringBuilder("[");
         buf.append(Utils.describeContext());
         buf.append("] --> ");
         buf.append(database);
         buf.append(":  ");
         prefix = buf.toString();
      }
      
      if (clean)
      {
         if (debug)
         {
            StringBuilder buf = new StringBuilder(prefix);
            buf.append("no record lock leak detected");
            LOG.log(Level.FINE, buf.toString());
         }
         
         return;
      }
      
      // locks were orphaned in this context, so we must release them
      int leakCount = 0;
      ArrayList<LockStatus<T>> statusList = new ArrayList<>();
      for (Map<RecordIdentifier<T>, LockStatus<T>> map : local.locked.values())
      {
         leakCount += map.size();
         statusList.addAll(map.values());
      }
      
      if (warn)
      {
         StringBuilder buf = new StringBuilder(prefix);
         buf.append("cleaning up ");
         buf.append(leakCount);
         buf.append(" leaked record lock(s) for exiting context (");
         buf.append(local.locked);
         buf.append(")");
         LOG.log(Level.WARNING, buf.toString());
      }
      
      if (debug)
      {
         StringBuilder buf = new StringBuilder(prefix);
         buf.append("releasing all record locks held by context...");
         LOG.log(Level.FINE, buf.toString());
      }
      
      // release locks held within this context
      for (int i = 0; i < statusList.size(); i++)
      {
         LockStatus<T> ls = statusList.get(i);
         release(ls, local);
      }
      
      if (debug)
      {
         StringBuilder buf = new StringBuilder(prefix);
         buf.append("...context record lock release complete");
         LOG.log(Level.FINE, buf.toString());
      }
   }
   
   /**
    * Release the current context's lock, if any, on the record represented
    * by <code>status</code>.  If after doing so, no lockers remain in
    * <code>status</code>, and no updates are pending, <code>status</code> is
    * removed from the lock table.  If no lock is held by the current
    * context, this method is effectively a no-op.
    *
    * @param   status
    *          Object which represents the lock status of the target database
    *          record.
    * @param   local
    *          Current session context.
    */
   private void release(LockStatus<T> status, Context<T> local)
   {
      SessionToken locker = local.locker;
      
      synchronized (status)
      {
         if (status.isLockedBy(locker))
         {
            RecordIdentifier<T> ident = status.getRecordIdentifier();
            LockType oldType = status.getLockType();
            
            // Remove current context from the set of lockers for this record.
            status.removeLocker(locker);
            
            // Remove the lock status object from the lock table, if it is not
            // "in-play" (being modified by other threads).
            if (status.getLockerCount() == 0)
            {
               synchronized (lockTable)
               {
                  if (!inPlay.containsKey(ident))
                  {
                     lockTable.remove(ident);
                  }
               }
            }
            
            // Remove the corresponding entry from the map of locally locked records.
            T table = ident.getTable();
            Map<RecordIdentifier<T>, LockStatus<T>> statusMap = local.locked.get(table);
            statusMap.remove(ident);
            if (statusMap.isEmpty())
            {
               local.locked.remove(table);
            }
            
            notifyLockChange(locker, ident, oldType, LockType.NONE, local.osUserName);
            
            if (trace)
            {
               logTransition(LockType.NONE, database, ident);
            }
         }
      }
   }
   
   /**
    * Indicate if we are operating in headless mode, meaning no UI is
    * available to give feedback to users.
    * 
    * @return  <code>true</code> if headless, else <code>false</code>.
    */
   protected boolean isHeadless()
   {
      return offline;
   }
   
   /**
    * Attempt to acquire a share lock on the record represented by
    * <code>status</code>.  If the record is available for a share lock, the
    * method returns immediately.  Otherwise, the outcome depends upon the
    * value of the "no-wait" designation of the requested lock type:  if
    * <code>true</code>, an exception is thrown;  if <code>false</code>, the
    * method blocks until the record becomes available and the share lock is
    * acquired.
    * <p>
    * A share lock can only be acquired if the record is currently unlocked,
    * is share locked by any context, or is exclusively locked by the current
    * context.  If another context holds an exclusive lock on the record, it
    * cannot be share locked by this context until that lock is released.
    *
    * @param   status
    *          Object which represents the lock status of the target database
    *          record.
    * @param   locker
    *          the current locking context.
    * @param   noWait
    *          If <code>true</code>, the current thread will not block in the
    *          event the record cannot be locked immediately;  if
    *          <code>false</code>, the method will block to wait for a
    *          conflicting lock to be released.
    * @param   update
    *          <code>true</code> if the lock state for the current record
    *          should be updated;  else <code>false</code>.
    * @param   timeout
    *          Number of milliseconds to wait before throwing {@link LockTimeoutException},
    *          or 0L to wait indefinitely.
    * 
    * @throws  LockUnavailableException
    *          if a "no-wait" lock cannot be acquired immediately.
    * @throws  LockTimeoutException
    *          if a non-zero timeout was provided and the requested lock has not been acquired
    *          by the time that period has elapsed.
    */
   private void acquireShare(LockStatus<T> status,
                             SessionToken locker,
                             boolean noWait,
                             boolean update,
                             long timeout)
   throws LockUnavailableException
   {
      // Can acquire share lock if:
      // 1) the record is unlocked;  or
      // 2) other users have a share lock;  or
      // 3) we are the only holder of a share or exclusive lock.
      LockType lockType = status.getLockType();
      boolean exclusive = lockType.isExclusive();
      boolean onlyLocker = status.isOnlyLocker(locker);
      if (!exclusive || onlyLocker)
      {
         if (update)
         {
            boolean typeChanged = status.setLockType(LockType.SHARE);
            boolean lockerAdded = status.addLocker(locker);
            if (listener != null && (typeChanged || lockerAdded))
            {
               LockType oldType;
               if (onlyLocker)
               {
                  if (exclusive)
                  {
                     // downgraded lock from exclusive to share
                     oldType = LockType.EXCLUSIVE;
                  }
                  else
                  {
                     // already had share lock (alone), still do
                     oldType = LockType.SHARE;
                  }
               }
               else if (lockerAdded)
               {
                  // acquired share lock from no lock
                  oldType = LockType.NONE;
               }
               else
               {
                  // already had share lock (with others), still do
                  oldType = LockType.SHARE;
               }
               
               // only notify of lock change if we didn't already hold share lock
               if (oldType != LockType.SHARE)
               {
                  RecordIdentifier<T> ident = status.getRecordIdentifier();
                  notifyLockChange(locker, ident, oldType, LockType.SHARE, context.get().osUserName);
               }
            }
            
            // If downgrading from exclusive to share, notify other threads
            // which may be waiting to acquire share locks.
            if (exclusive)
            {
               status.notifyAll();
            }
         }
         
         return;
      }
      
      // Handle no-wait condition.  If we got here, the record could not
      // be locked immediately.
      if (noWait)
      {
         RecordIdentifier<T> ident = status.getRecordIdentifier();
         String msg = "Could not acquire share lock:  " + ident;
         
         // Debug logging to report lock contention.
         if (debug)
         {
            status.logContention(LockType.SHARE_NO_WAIT);
         }
         
         throw new LockUnavailableException(msg, LockType.SHARE_NO_WAIT, ident);
      }
      
      // Block waiting on an exclusive lock;  obtain share lock when record becomes available.
      lockWhenAvailable(LockType.SHARE, status, locker, update, timeout);
   }
   
   /**
    * Attempt to acquire an exclusive lock on the record represented by
    * <code>status</code>.  If the record is available for such a lock, the
    * method returns immediately.  Otherwise, the outcome depends upon the
    * value of the <code>noWait</code> parameter:  if <code>true</code>, an
    * exception is thrown;  if <code>false</code>, the method blocks until
    * the record becomes available and the exclusive lock is acquired.
    * <p>
    * An exclusive lock can only be acquired if the record is currently
    * unlocked, or is share locked by the current context.  If another
    * context holds either an exclusive lock or a share lock on the record,
    * it cannot be exclusively locked by this context until all such locks
    * are released.
    *
    * @param   status
    *          Object which represents the lock status of the target database
    *          record.
    * @param   locker
    *          the current locking context.
    * @param   noWait
    *          If <code>true</code>, the current thread will not block in the
    *          event the record cannot be locked immediately;  if
    *          <code>false</code>, the method will block to wait for a
    *          conflicting lock to be released.
    * @param   update
    *          <code>true</code> if the lock state for the current record
    *          should be updated;  else <code>false</code>.
    * @param   timeout
    *          Number of milliseconds to wait before throwing {@link LockTimeoutException},
    *          or 0L to wait indefinitely.
    * 
    * @throws  LockUnavailableException
    *          if a "no-wait" lock cannot be acquired immediately.
    * @throws  LockTimeoutException
    *          if a non-zero timeout was provided and the requested lock has not been acquired
    *          by the time that period has elapsed.
    */
   private void acquireExclusive(LockStatus<T> status,
                                 SessionToken locker,
                                 boolean noWait,
                                 boolean update,
                                 long timeout)
   throws LockUnavailableException
   {
      // Can only acquire exclusive lock immediately if we are the only holder of a share or
      // exclusive lock, or if there is no lock at all.
      LockType lockType = status.getLockType();
      if (status.isOnlyLocker(locker) || LockType.NONE.equals(lockType))
      {
         if (update)
         {
            boolean typeChanged = status.setLockType(LockType.EXCLUSIVE);
            boolean lockerAdded = status.addLocker(locker);
            if (listener != null && (typeChanged || lockerAdded))
            {
               LockType oldType;
               if (lockerAdded)
               {
                  // acquired exclusive lock from no lock
                  oldType = LockType.NONE;
               }
               else
               {
                  // had share lock (alone), upgraded to exclusive
                  oldType = LockType.SHARE;
               }
               
               RecordIdentifier<T> ident = status.getRecordIdentifier();
               notifyLockChange(locker, ident, oldType, LockType.EXCLUSIVE, context.get().osUserName);
            }
         }
         
         return;
      }
      
      // Handle no-wait condition.  If we got here, the record could not be locked immediately.
      if (noWait)
      {
         RecordIdentifier<T> ident = status.getRecordIdentifier();
         String msg = "Could not acquire exclusive lock:  " + ident;
         
         // Debug logging to report lock contention.
         if (debug)
         {
            status.logContention(LockType.EXCLUSIVE_NO_WAIT);
         }
         
         throw new LockUnavailableException(msg, LockType.EXCLUSIVE_NO_WAIT, ident);
      }
      
      // Block waiting on an exclusive or share lock;  obtain exclusive
      // lock when record becomes available.
      lockWhenAvailable(LockType.EXCLUSIVE, status, locker, update, timeout);
   }
   
   /**
    * Worker method to handle single database record or table as whole in the current context.
    *
    * The record is uniquely identified by a table name and record ID (i.e., primary key).
    *
    * Whether the lock object (record or table as whole) is locked or released depends upon the
    * lock type requested: <code>LockType.NONE</code> is used to release a object;  any other
    * lock type is used to lock a object.  Specifying <code>LockType.NONE</code> for a object
    * not locked in the current context is logically a no-op, though some overhead is required
    * for the check, and the caller may block waiting on the lock table monitor.
    * <p>
    * Any normal return from this method indicates the requested lock was acquired (or released)
    * successfully. Any error will result in an exceptional return.  In the event a object
    * cannot be locked, due to a conflicting lock held by another context, the current thread
    * will block until the lock is acquired, unless the requested lock is a "no-wait" variant,
    * in which case an exception is thrown.
    * <p>
    * This method also operates in a check-only mode (if <code>update</code> is set to
    * <code>false</code>) where it goes through the motions of obtaining the specified lock
    * type, but does not actually change the lock status.  This is at best an unreliable check,
    * since another context can change the lock status as soon as the lock status monitor is
    * released, rendering the check stale.  Nevertheless, it exists to mimic the effect of the
    * lock type option to the Progress can-find function.
    *
    * @param   lockType
    *          The type of lock requested;  <code>LockType.NONE</code> to
    *          release a lock.
    * @param   ident
    *          ID which uniquely identifies the record or table being queried.
    * @param   update
    *          <code>true</code> if the lock state for the current record or table should be
    *          updated;  else <code>false</code>.  This is set to <code>false</code> if the
    *          caller just wants to know whether a lock is available, without actually changing
    *          its status.
    * @param   timeout
    *          Number of milliseconds to wait before throwing {@link LockTimeoutException},
    *          or 0L to wait indefinitely.
    * 
    * @throws  LockUnavailableException
    *          if a "no-wait" lock cannot be acquired immediately.
    * @throws  LockTimeoutException
    *          if a non-zero timeout was provided and the requested lock has not been acquired
    *          by the time that period has elapsed.
    */
   private void lockImpl(LockType lockType, RecordIdentifier<T> ident, boolean update, long timeout)
   throws LockUnavailableException,
          LockTimeoutException
   {
      boolean noLock = LockType.NONE.equals(lockType);
      
      // if no update to the lock state is requested and the lock type is NONE, no need to check
      // further
      if (noLock && !update)
      {
         return;
      }
      
      Context<T> local = context.get();
      SessionToken locker = local.locker;
      T table = ident.getTable();
      LockStatus<T> status;
      
      synchronized (lockTable)
      {
         // check whether record is already locked
         status = lockTable.get(ident);
         
         if (!noLock)
         {
            // check whether this is the uncontended, "fast" case where no other context holds
            // the lock
            if (status == null)
            {
               // if an update was requested, acquire the lock at the specified level; otherwise,
               // return immediately.
               if (update)
               {
                  status = (debug
                            ? new TraceLockStatus<>(database, ident, lockType, locker, warnThreshold)
                            : new LockStatus<>(database, ident, lockType, locker, warnThreshold));
                  
                  // add status to map of locks held by this context
                  Map<RecordIdentifier<T>, LockStatus<T>> statusMap = local.locked.get(table);
                  if (statusMap == null)
                  {
                     statusMap = new HashMap<>();
                     local.locked.put(table, statusMap);
                  }
                  statusMap.put(ident, status);
                  
                  // add status to main lock table
                  lockTable.put(ident, status);
                  
                  notifyLockChange(locker, ident, LockType.NONE, lockType, local.osUserName);
                  
                  if (trace)
                  {
                     logTransition(lockType, database, ident);
                  }
               }
               
               return;
            }
            
            // another context already holds a lock; mark the target record as "in-play" so that
            // it will not be removed from the lock table while this thread is using it
            Integer count = inPlay.get(ident);
            if (count == null)
            {
               // establish reference count
               count = 1;
            }
            else
            {
               // increment reference count
               count++;
            }
            
            inPlay.put(ident, count);
         }
      }
      
      // release a lock (or a no-op if record is not currently locked by the current context)
      if (noLock)
      {
         if (status != null)
         {
            release(status, local);
         }
         
         return;
      }
      
      // If we got this far, the lock is already held by another context (or
      // by the current context).  Attempt to acquire it, which may or may not
      // lead to contention, depending on the current and requested lock
      // types, and who holds them.
      try
      {
         synchronized (status)
         {
            boolean noWait = lockType.isNoWait();
            if (lockType.isShare())
            {
               acquireShare(status, locker, noWait, update, timeout);
            }
            else if (lockType.isExclusive())
            {
               acquireExclusive(status, locker, noWait, update, timeout);
            }
            
            if (update)
            {
               // add status to map of locks held by this context
               Map<RecordIdentifier<T>, LockStatus<T>> statusMap = local.locked.get(table);
               if (statusMap == null)
               {
                  statusMap = new HashMap<>();
                  local.locked.put(table, statusMap);
               }
               statusMap.put(ident, status);
               
               // log the change
               if (trace)
               {
                  logTransition(lockType, database, ident);
               }
            }
         }
      }
      finally
      {
         synchronized (lockTable)
         {
            // update "in-play" status to indicate this thread is finished with this record
            int count = inPlay.get(ident).intValue();
            if (count == 1)
            {
               // remove reference count for this lock record altogether
               inPlay.remove(ident);
            }
            else
            {
               // decrement reference count
               inPlay.put(ident, Integer.valueOf(--count));
            }
         }
      }
   }
   
   /**
    * Attempt to lock the record represented by {@code status} with the given lock type as
    * soon as the record is available, blocking the current thread for {@code timeout}
    * milliseconds, or optionally indefinitely. Multiple threads may end up blocked in this
    * method waiting on a particular record. When that record becomes available, all threads are
    * notified, such that they become runnable.
    * <p>
    * There is no fairness guarantee, FIFO or otherwise, as to which thread will execute first;
    * this is up to the platform-specific thread scheduler. The first of the released threads to
    * execute sets the new lock type of the {@code status} object, which may prevent other
    * contexts from locking (if set to {@code LockType.EXCLUSIVE}) or it may not (if set to
    * {@code LockType.SHARE}). Threads which were unable to acquire a lock upon release will
    * re-enter a wait state, blocking until another chance occurs.
    * <p>
    * This method must only be invoked when the current thread holds the monitor for the {@code
    * status} object.
    * 
    * @param   requested
    *          The type of lock being acquired.  The only valid types are
    *          {@code LockType.EXCLUSIVE} and {@code LockType.SHARE}.
    * @param   status
    *          Object which represents the lock status of the target database record.
    * @param   locker
    *          The current locking context.
    * @param   update
    *          {@code true} if the lock state for the current record should be updated; else
    *          {@code false}.
    * @param   timeout
    *          Number of milliseconds to wait before throwing {@link LockTimeoutException},
    *          or 0L to wait indefinitely.
    * 
    * @throws  LockTimeoutException
    *          if a non-zero timeout was provided and the requested lock has not been acquired
    *          by the time that period has elapsed.
    * @throws  StopConditionException
    *          if the current thread is interrupted while blocked.
    */
   private void lockWhenAvailable(LockType requested,
                                  LockStatus<T> status,
                                  SessionToken locker,
                                  boolean update,
                                  long timeout)
   throws LockTimeoutException
   {
      boolean clearMsg = false;
      
      try
      {
         long waitUntil = timeout > 0L ? System.nanoTime() + (timeout * 1000000) : 0L;
         long start = (warnThreshold > -1L ? System.currentTimeMillis() : -1L);
         LockType current;
         
         // Loop indefinitely;  we will break only when the lock is acquired
         // or if we are interrupted.
         while (true)
         {
            // Debug logging to report lock contention.
            if (debug)
            {
               status.logContention(requested);
            }
            
            // Report the conflict to the user.
            if (!isHeadless() && !clearMsg)
            {
               Iterator<SessionToken> iter = status.lockers();
               
               T table = status.getRecordIdentifier().getTable();
               StringBuilder buf = new StringBuilder(table.toString());
               buf.append(" in use by ");
               
               for (int i = 0; iter.hasNext(); i++)
               {
                  if (i > 0)
                  {
                     buf.append(", ");
                  }
                  
                  SessionToken next = iter.next();
                  String subject = next.getSubject();
                  
                  buf.append(subject);
               }
               buf.append(". Wait or press CTRL-C to stop. (121)");
               LogicalTerminal.statusDefault(buf.toString(), true);
               clearMsg = true;
            }
            
            // We are here because another context holds either a share or an
            // exclusive lock.  Wait for such lock to be released or downgraded...
            status.wait(timeout);
            
            // Determine current lock type.  In a lock transition, it should only be NONE or
            // SHARE at this point, since to get here, we had to have released or downgraded
            // the lock (unless we timed out or had a spurious wake-up).
            current = status.getLockType();
            LockType oldType = current;
            
            // if current lock is exclusive, we timed out or spuriously woke up
            if (!current.isExclusive())
            {
               // Set the lock type if we're the first one in...
               if (LockType.NONE.equals(current) || status.isOnlyLocker(locker))
               {
                  if (!update)
                  {
                     // We only wanted to know *if* we could get the lock, so we're done.
                     break;
                  }
                  
                  // Set the current lock type to the requested lock type.
                  status.setLockType(requested);
                  current = requested;
               }
               
               // ...then acquire the lock, if possible.  This test presumes that
               // the current lock type will only be EXCLUSIVE if the previous
               // block has set it to such on behalf of the requesting context.
               if (current.equals(requested) &&
                   (LockType.SHARE.equals(current) ||
                    status.getLockerCount() == 0   ||
                    status.isOnlyLocker(locker)))
               {
                  if (update && (status.addLocker(locker) || oldType != requested))
                  {
                     notifyLockChange(locker,
                                      status.getRecordIdentifier(),
                                      oldType,
                                      requested,
                                      context.get().osUserName);
                  }
                  
                  // Got it;  we're done waiting.
                  break;
               }
            }
            
            // check whether timeout (if any) has elapsed; if so, throw LockTimeoutException
            if (waitUntil > 0L && System.nanoTime() >= waitUntil)
            {
               throw new LockTimeoutException(requested, status.getRecordIdentifier());
            }
            
            // otherwise, continue waiting (includes possible spurious wake-up)...
         }
         
         if (start > -1L)
         {
            // Report how long we had to wait to acquire the lock, if it was
            // beyond the report threshold.
            long elapsed = System.currentTimeMillis() - start;
            if (elapsed > warnThreshold)
            {
               RecordIdentifier<T> ident = status.getRecordIdentifier();
               logTransition(requested, current, database, ident, elapsed);
            }
         }
      }
      catch (InterruptedException exc)
      {
         ErrorManager.handleStopException(new LockConflictConditionException(exc));
      }
      finally
      {
         if (clearMsg)
         {
            LogicalTerminal.statusDefault();
         }
      }
   }
   
   /**
    * Notify a listener, if one is set, of a lock change event. The listener should return as
    * quickly as possible to minimize any performance impact. If no listener is set, this method
    * returns immediately.
    *
    * @param   locker
    *          The current locking context.
    * @param   ident
    *          Identifier of record whose lock status has changed.
    * @param   oldType
    *          Lock type before the change.
    * @param   newType
    *          Lock type after the change.
    */
   private void notifyLockChange(SessionToken locker,
                                 RecordIdentifier<T> ident,
                                 LockType oldType,
                                 LockType newType,
                                 String osUserName)
   {
      if (listener == null)
      {
         return;
      }
      
      // cast to RecordIdentifier<String> is ok; the only listener for this event expects this type
      @SuppressWarnings("unchecked")
      RecordLockEvent evt = new RecordLockEvent(nextEventID++,
                                                locker,
                                                (RecordIdentifier<String>) ident,
                                                oldType,
                                                newType);
      evt.setOsUserName(osUserName);
      listener.lockChanged(evt);
   }
   
   /**
    * Context local lock manager state.  Stores locker's session ID and tracks
    * the record locks held by the current context.
    */
   private static class Context<T extends Serializable>
   {
      /** Session token of the current context */
      private final SessionToken locker;
      
      /** IDs of all records currently locked by this context, organized by table */
      private final Map<T, Map<RecordIdentifier<T>, LockStatus<T>>> locked;
      
      /** The OS name of the user who generated this lock event. */
      private final String osUserName;
      
      /**
       * Constructor.
       * 
       * @param   offline
       *          <code>true</code> for offline mode;  <code>false</code> if
       *          running in the context of a P2J server process.
       */
      Context(boolean offline)
      {
         if (offline)
         {
            locker = new SessionToken(-1, Thread.currentThread().getName());
         }
         else
         {
            locker = SecurityManager.getInstance().sessionSm.getSessionToken();
         }
         
         ClientParameters cp = StandardServer.getClientParameters();
         osUserName = (cp == null) ? null : cp.osUserName;
         
         locked = new HashMap<>();
      }
   }
   
   /**
    * Helper class which indicates the status of a locked record.  This
    * includes:
    * <ul>
    *   <li>the type of lock;
    *   <li>the set of locking context IDs (may be multiple for a share lock,
    *       must be singular for an exclusive lock);
    *   <li>a <code>RecordIdentifier</code> object which uniquely identifies
    *       the locked record.
    * </ul>
    * If a lock request must block waiting for a lock to be released, the
    * current thread synchronizes on instances of this class.  These objects
    * are stored in the lock manager's master lock table, keyed by
    * <code>RecordIdentifier</code> objects.
    */
   private static class LockStatus<T extends Serializable>
   {
      /** Database */
      private final Database database;
      
      /** Unique identifier of record associated with this status object */
      private final RecordIdentifier<T> ident;
      
      /** This field is used as long as a single session locks this record, to avoid a set instantiation. */
      private SessionToken singleLocker = null;
      
      /** The set of IDs of the lockers of the record */
      private Set<SessionToken> lockers = null;
      
      /** Threshold in milliseconds above which a lock hold time is reported */
      private final long warnThreshold;
      
      /** The type of lock being held on the record */
      private LockType lockType = LockType.NONE;
      
      /** Timestamp at which lock was first obtained, for admin purposes */
      private long lockAdminTime = -1L;
      
      /** Timestamp at which lock was first obtained, for warning purposes */
      private long lockWarnTime = -1L;
      
      /**
       * Constructor.
       *
       * @param   ident
       *          Unique identifier of record associated with this status
       *          object.
       * @param   lockType
       *          Type of lock initially held on the record.
       *
       * @throws  NullPointerException
       *          if <code>locker</code> is <code>null</code>.
       */
      LockStatus(Database database, RecordIdentifier<T> ident, LockType lockType, long warnThreshold)
      {
         this.database = database;
         this.ident = ident;
         this.warnThreshold = warnThreshold;
         setLockType(lockType);
      }
      
      /**
       * Constructor.
       *
       * @param   ident
       *          Unique identifier of record associated with this status
       *          object.
       * @param   lockType
       *          Type of lock initially held on the record.
       * @param   locker
       *          the initial locking context.
       *
       * @throws  NullPointerException
       *          if <code>locker</code> is <code>null</code>.
       */
      LockStatus(Database database,
                 RecordIdentifier<T> ident,
                 LockType lockType,
                 SessionToken locker,
                 long warnThreshold)
      {
         this(database, ident, lockType, warnThreshold);
         
         if (locker == null)
         {
            throw new NullPointerException("Locker ID must be non-null");
         }
         
         addLocker(locker);
      }
      
      /**
       * Compose a string representation of this object for assistance in
       * debugging and testing.
       *
       * @return  String representation of this object's internal state.
       */
      public String toString()
      {
         StringBuilder buf = new StringBuilder(lockType.toString());
         
         buf.append(" [");
         Iterator<SessionToken> iter = lockers();
         for (int i = 0; iter.hasNext(); i++)
         {
            SessionToken next = iter.next();
            if (i > 0)
            {
               buf.append(' ');
            }
            buf.append(next);
         }
         buf.append("]");
         
         return buf.toString();
      }
      
      /**
       * Remove a locking context from the set of contexts which currently
       * hold locks on the backing record.  If this was the last locking
       * context in the set, reset the lock type to <code>LockType.NONE</code>
       * and notify any threads which are blocked on this object to resume
       * processing.
       *
       * @param   locker
       *          Unique identifier of locking context to be removed.
       */
      protected void removeLocker(SessionToken locker)
      {
         boolean setToNone = false;
         if (lockers == null)
         {
            if (singleLocker == locker)
            {
               singleLocker = null;
               setToNone = true;
            }
            else
            {
               LOG.log(Level.SEVERE, "Inconsistent locking? Trying to remove locker which was not registered.");
            }
         }
         else
         {
            lockers.remove(locker);
            if (lockers.isEmpty())
            {
               setToNone = true;
               lockers = null;
            }
         }
         
         if (setToNone)
         {
            // Reset lock type to none.
            setLockType(LockType.NONE);
         }
         
         // Notify all waiters to resume processing.
         notifyAll();
      }
      
      /**
       * Compose a string representation of this object for assistance in
       * debugging and testing.
       *
       * @return  String representation of this object's internal state.
       */
      protected String toStringVerbose()
      {
         return toString();
      }
      
      /**
       * Store this object's information within a {@link
       * com.goldencode.p2j.admin.RecordLockInfo RecordLockInfo} object for administrative purposes.
       * 
       * @return  Lock information for this lock.
       */
      RecordLockInfo getLockInfo()
      {
         RecordLockType type = null;
         if (lockType.isExclusive())
         {
            type = RecordLockType.EXCLUSIVE;
         }
         else if (lockType.isShare())
         {
            type = RecordLockType.SHARE;
         }
         else
         {
            type = RecordLockType.NONE;
         }
         
         SessionToken[] tokens = lockers != null 
                                    ? lockers.toArray(new SessionToken[0])
                                    : singleLocker == null 
                                        ? new SessionToken[0] 
                                        : new SessionToken[] { singleLocker } ;
         
         return new RecordLockInfo(type,
                                   lockAdminTime,
                                   database.getName(),
                                   ident.getTable().toString(),
                                   ident.getRecordID(),
                                   tokens,
                                   !database.isTenant());
      }
      
      /**
       * Log debugging information about a lock acquisition request which
       * causes contention with one or more existing lock holders.
       * 
       * @param   requested
       *          Lock type which was requested.
       */
      void logContention(LockType requested)
      {
         StringBuilder buf = new StringBuilder("[");
         buf.append(Utils.describeContext());
         buf.append("] --> ");
         buf.append(database);
         buf.append(".");
         buf.append(ident);
         buf.append(" LOCK CONTENTION;  requested ");
         buf.append(requested);
         buf.append(" at location:");
         printStackTrace(buf, new Throwable());
         buf.append(System.lineSeparator());
         buf.append("current status:  ");
         buf.append(toStringVerbose());
         String msg = buf.toString();
         LOG.log(Level.FINE, msg);
      }
      
      /**
       * Retrieve the object which uniquely identifies the record represented
       * by this lock status object.
       *
       * @return  Lock record object.
       */
      RecordIdentifier<T> getRecordIdentifier()
      {
         return ident;
      }
      
      /**
       * Get an iterator on the lockers currently holding this lock.
       * 
       * @return  Iterator of <code>SessionToken</code>s.
       */
      Iterator<SessionToken> lockers()
      {
         if (lockers != null)
         {
            return lockers.iterator();
         }
         
         return singleLocker == null ? Collections.emptyIterator() : Arrays.asList(singleLocker).iterator();
      }
      
      /**
       * Add a locking context to the set of contexts which currently hold
       * locks on the backing record.  This may be multiple for share locks,
       * and must be a single context for exclusive locks (though this
       * restriction is not enforced here).
       *
       * @param   locker
       *          Unique identifier of locking context to be added.
       * 
       * @return  <code>true</code> if <code>locker</code> was added;  <code>false</code> if
       *          <code>locker</code> already was in the set of lockers.
       */
      boolean addLocker(SessionToken locker)
      {
         if (lockers == null)
         {
            if (singleLocker == null)
            {
               singleLocker = locker;
               return true;
            }
            else if (singleLocker == locker)
            {
               return false;
            }
            else
            {
               lockers = Collections.newSetFromMap(new IdentityHashMap<>());
               lockers.add(singleLocker);
               lockers.add(locker);
               singleLocker = null;
               return true;
            }
         }
         else
         {
            return lockers.add(locker);
         }
      }
      
      /**
       * Retrieve the current lock type applied to the backing record.
       *
       * @return  Lock type.
       */
      LockType getLockType()
      {
         return lockType;
      }
      
      /**
       * Set the current lock type applied to the backing record.
       *
       * @param   lockType
       *          Lock type.
       * 
       * @return  <code>true</code> to indicate lock type was changed; <code>false</code> to
       *          indicate <code>lockType</code> was the same as the existing lock type.
       */
      boolean setLockType(LockType lockType)
      {
         if (LockType.NONE.equals(this.lockType) && !LockType.NONE.equals(lockType))
         {
            lockAdminTime = System.currentTimeMillis();
         }
         
         if (warn && warnThreshold >= 0L)
         {
            recordOrReportTime(this.lockType, lockType);
         }
         
         LockType newType = lockType.toWaitVariant();
         boolean different = (!this.lockType.equals(newType));
         
         this.lockType = newType;
         
         return different;
      }
      
      /**
       * Indicate whether the specified locking context is among those which
       * currently lock the backing record.
       *
       * @param   locker
       *          Unique context identifier to check.
       *
       * @return  <code>true</code> if <code>locker</code> is among the
       *          records current lockers, else <code>false</code>.
       */
      boolean isLockedBy(SessionToken locker)
      {
         return singleLocker == locker || (lockers != null && lockers.contains(locker));
      }
      
      /**
       * Determine whether the specified locker is the only context which
       * currently holds a lock on the backing record.
       *
       * @param   locker
       *          Unique identifier of the locking context to be checked.
       * 
       * @return  <code>true</code> if the given locker is the only entity
       *          holding a lock on the backing record, else
       *          <code>false</code>.
       */
      boolean isOnlyLocker(SessionToken locker)
      {
         return singleLocker == locker || (lockers != null && lockers.size() == 1 && lockers.contains(locker));
      }
      
      /**
       * Determine the number of contexts which currently hold a lock on the
       * backing record.
       *
       * @return  0 for an unlocked record;  1 for an exclusive lock;  1 or
       *          more for a share lock.
       */
      int getLockerCount()
      {
         return lockers == null ? singleLocker == null ? 0 : 1 : lockers.size();
      }
      
      /**
       * Given an old lock type and a new lock type, either record the current
       * time, or check whether the elapsed time since the last recorded time
       * exceeds the reporting threshold, and write a warning to the log.  If
       * the old and new lock types are the same, do nothing.
       * <p>
       * If the new lock type is more restrictive than the old, we record the
       * current time.  If the new lock type is less restrictive than the old,
       * we check the threshold and possibly report the warning.  The idea is
       * to report when a lock is potentially blocking other access for longer
       * than expected.
       * 
       * @param   oldType
       *          Old lock type.
       * @param   newType
       *          New lock type.
       */
      private void recordOrReportTime(LockType oldType, LockType newType)
      {
         int comp = oldType.compareTo(newType);
         if (comp < 0)
         {
            // New type is more restrictive.
            lockWarnTime = lockAdminTime;
         }
         else if (comp > 0)
         {
            // New type is less restrictive.
            long elapsed = System.currentTimeMillis() - lockWarnTime;
            if (elapsed > warnThreshold)
            {
               logTransition(newType, oldType, database, ident, elapsed);
            }
         }
      }
   }
   
   /**
    * An extension of <code>LockStatus</code> used for trace-level debug
    * logging.  In addition to the information tracked by the superclass, this
    * class maps stack trace information for the point of lock acquisition to
    * each lock holder.  This information is used to debug lock contention
    * issues.  It is only used when logging for the <code>LockManager</code>
    * is set to FINEST.
    */
   private class TraceLockStatus<R extends Serializable>
   extends LockStatus<R>
   {
      /** Map of locker tokens to Throwables containing stack data */
      private final Map<SessionToken, Throwable> traces = new LinkedHashMap<>();
      
      /**
       * Constructor.
       *
       * @param   ident
       *          Unique identifier of record associated with this status
       *          object.
       * @param   lockType
       *          Type of lock initially held on the record.
       * @param   locker
       *          the initial locking context.
       *
       * @throws  NullPointerException
       *          if <code>locker</code> is <code>null</code>.
       */
      TraceLockStatus(Database database,
                      RecordIdentifier<R> ident,
                      LockType lockType,
                      SessionToken locker,
                      long warnThreshold)
      {
         super(database, ident, lockType, warnThreshold);
         
         if (locker == null)
         {
            throw new NullPointerException("Locker ID must be non-null");
         }
         
         addLocker(locker);
      }
      
      /**
       * Compose a string representation of this object for assistance in debugging and testing.
       *
       * @return  String representation of this object's internal state.
       */
      protected String toStringVerbose()
      {
         StringBuilder buf = new StringBuilder(toString());
         
         Iterator<Map.Entry<SessionToken, Throwable>> iter = traces.entrySet().iterator();
         while (iter.hasNext())
         {
            Map.Entry<SessionToken, Throwable> next = iter.next();
            SessionToken locker = next.getKey();
            Throwable throwable = next.getValue();
            buf.append("\nLocker ");
            buf.append(locker);
            buf.append(" acquisition location:");
            printStackTrace(buf, throwable);
         }
         
         return buf.toString();
      }
      
      /**
       * Remove a locking context from the set of contexts which currently
       * hold locks on the backing record.  If this was the last locking
       * context in the set, reset the lock type to <code>LockType.NONE</code>
       * and notify any threads which are blocked on this object to resume
       * processing.
       *
       * @param   locker
       *          Unique identifier of locking context to be removed.
       */
      protected void removeLocker(SessionToken locker)
      {
         super.removeLocker(locker);
         
         traces.remove(locker);
      }
      
      /**
       * Add a locking context to the set of contexts which currently hold
       * locks on the backing record.  This may be multiple for share locks,
       * and must be a single context for exclusive locks (though this
       * restriction is not enforced here).
       *
       * @param   locker
       *          Unique identifier of locking context to be added.
       * 
       * @return  <code>true</code> if <code>locker</code> was added;  <code>false</code> if
       *          <code>locker</code> already was in the set of lockers.
       */
      boolean addLocker(SessionToken locker)
      {
         boolean added = super.addLocker(locker);
         
         Throwable throwable = new Throwable();
         throwable.fillInStackTrace();
         traces.put(locker, throwable);
         
         return added;
      }
   }
   
   /**
    * Print command line usage instructions to the error console for the
    * built-in test harness.
    */
   private static void printUsage()
   {
      String[] staticText =
      {
         "Use:  InMemoryLockManager <numThreads> <iterations> <idPoolSize> <maxWorkTime> <noWait>",   
         "",   
         "where:",   
         "",   
         "   <numThreads>    Number of concurrent test threads to launch",   
         "   <iterations>    Number of iterations performed by each thread",   
         "   <idPoolSize>    Size of pool from which record IDs are randomly chosen",   
         "   <maxWorkTime>   Maximum milliseconds to pause to simulate work time",   
         "   <noWait>        'true' (omit quotes) to never block when locking a record",   
         "                   'false' (omit quotes) to allow blocking",   
         "   <reportID>      (optional) Limit reporting to this record ID only",   
         "                   defaults to reporting on all record IDs"
      };
      LOG.info(StringHelper.arrayToString(staticText));
   }
   
   /**
    * Command line test harness for this class.  Launches a user-specified
    * number of concurrent threads, each of which performs a user-specified
    * number of iterations.  Within each iteration, a record is locked with
    * a randomly selected lock type (may be NONE, which indicates no lock).
    * Upon acquiring the lock, the current thread sleeps for up to 3 seconds,
    * to simulate work time, after which the lock is released.  While the
    * table names are fixed, the ID of the record to be locked is selected
    * randomly from a pool size specified by the user.  Smaller ID pools
    * create more locking contention;  larger pools create less.
    * <p>
    * Status messages indicating lock requests, acquisitions, and releases
    * are emitted to the console, as are error messages (if there is locking
    * contention and the user has specified that threads must not block).
    *
    * @param   args
    *          The following command line arguments are expected:
    *          <ol>
    *            <li><b>numThreads:</b>  Number of concurrent test threads to
    *                launch
    *            <li><b>iterations:</b>  Number of iterations performed by
    *                each thread
    *            <li><b>idPoolSize:</b>  Size of pool from which record IDs
    *                are randomly chosen
    *            <li><b>maxWorkTime:</b>  Maximum number of milliseconds to
    *                pause after lock is acquired to simulate work time
    *            <li><b>noWait:</b>  <code>true</code> to never block when
    *                locking a record; <code>false</code> to allow blocking
    *            <li><b>reportID:</b> (optional) Limit reporting to the given
    *                record ID;  if not provided, activity on all IDs is
    *                reported.
    *          </ol>
    */
   public static void main(String[] args)
   {
      try
      {
         int numThreads = Integer.parseInt(args[0]);
         final int iterations = Integer.parseInt(args[1]);
         final int idPoolSize = Integer.parseInt(args[2]);
         final int maxWorkTime = Integer.parseInt(args[3]);
         final boolean noWait = (Boolean.valueOf(args[4])).booleanValue();
         final Long rptID = (args.length > 5 ? Long.parseLong(args[5]) : null);
         if (rptID != null && (rptID < 1 || rptID > idPoolSize))
         {
            throw new IllegalArgumentException("reportID is outside the specified pool");
         }
         
         final LockManager<String> lm = new InMemoryLockManager<>();
         lm.setDatabase(new Database("db"));
         final long[] longest = new long[1];
         final Set<Serializable> upgradeIDs = new HashSet<Serializable>();
         
         Runnable r = new Runnable()
         {
            public void run()
            {
               for (int i = 0; i < iterations; i++)
               {
                  String tName = Thread.currentThread().getName();
                  StringBuilder buf = new StringBuilder();
                  try
                  {
                     // Pseudo-random lock type.
                     LockType lockType = LockType.NONE;
                     double val = Math.random();
                     if (val >= 0.85)
                     {
                        lockType = (noWait ? LockType.EXCLUSIVE_NO_WAIT : LockType.EXCLUSIVE);
                     }
                     else if (val >= 0.25)
                     {
                        lockType = (noWait ? LockType.SHARE_NO_WAIT : LockType.SHARE);
                     }
                     
                     // Pseudo-random record ID.
                     val = Math.random() * idPoolSize + 1;
                     Long recID = Long.valueOf((long) val);
                     
                     // Pseudo-random work time millisecond pause.
                     int workTime = (int) (Math.random() * maxWorkTime + 1);
                     
                     // Request a lock.
                     RecordIdentifier<String> ident = new RecordIdentifier<>("table", recID);
                     if (rptID == null || recID.equals(rptID))
                     {
                        buf.append(tName);
                        buf.append(" --> REQUEST LOCK ");
                        buf.append(lockType);
                        buf.append(" on ");
                        buf.append(ident);
                        synchronized (longest)
                        {
                           System.out.println(buf);
                        }
                     }
                     
                     long start = System.currentTimeMillis();
                     lm.lock(lockType, ident, true);
                     long elapsed = System.currentTimeMillis() - start;
                     
                     // Lock has been acquired.
                     if (rptID == null || recID.equals(rptID))
                     {
                        buf.setLength(0);
                        buf.append(Thread.currentThread().getName());
                        buf.append(" --> ACQUIRE LOCK ");
                        buf.append(lockType);
                        buf.append(" on ");
                        buf.append(ident);
                        buf.append(" (in ");
                        buf.append(elapsed);
                        synchronized (longest)
                        {
                           if (elapsed >= longest[0])
                           {
                              longest[0] = elapsed;
                              buf.append('*');
                           }
                        }
                        buf.append(" millis)");
                        
                        synchronized (longest)
                        {
                           System.out.println(buf);
                        }
                     }
                     
                     // Simulate work, then downgrade and/or release lock.
                     while (!LockType.NONE.equals(lockType))
                     {
                        // Sleep to simulate work time.
                        start = System.currentTimeMillis();
                        try
                        {
                           Thread.sleep(workTime);
                        }
                        catch (InterruptedException exc)
                        {
                           throw new EndConditionException(exc);
                        }
                        
                        elapsed = System.currentTimeMillis() - start;
                        start = System.currentTimeMillis();
                        
                        // Upgrade, downgrade or release lock.
                        if (!LockType.NONE.equals(lockType))
                        {
                           // Release request (default).
                           LockType requested = LockType.NONE;
                           
                           if (lockType.isExclusive())
                           {
                              if (Math.random() > 0.5)
                              {
                                 // Downgrade request.
                                 requested = LockType.SHARE;
                              }
                           }
                           else if (lockType.isShare())
                           {
                              if (Math.random() > 0.5)
                              {
                                 synchronized (longest)
                                 {
                                    if (!upgradeIDs.contains(recID))
                                    {
                                       // Upgrade request.
                                       upgradeIDs.add(recID);
                                       requested = LockType.EXCLUSIVE;
                                       if (rptID == null ||
                                           recID.equals(rptID))
                                       {
                                          buf.setLength(0);
                                          buf.append(tName);
                                          buf.append(" --> REQUEST UPGRADE ");
                                          buf.append(lockType);
                                          buf.append(" on ");
                                          buf.append(ident);
                                          buf.append(" (after holding for ");
                                          buf.append(elapsed);
                                          buf.append(" millis)");
                                          System.out.println(buf);
                                       }
                                    }
                                 }
                              }
                           }
                           
                           lm.lock(requested, ident, true);
                           
                           if (requested.isExclusive())
                           {
                              synchronized (longest)
                              {
                                 upgradeIDs.remove(recID);
                              }
                           }
                           
                           if (rptID == null || recID.equals(rptID))
                           {
                              buf.setLength(0);
                              buf.append(tName);
                              if (requested.isExclusive())
                              {
                                 elapsed = System.currentTimeMillis() - start;
                                 buf.append(" --> UPGRADE LOCK ");
                                 buf.append(lockType);
                                 buf.append(" on ");
                                 buf.append(ident);
                                 buf.append(" (in ");
                                 buf.append(elapsed);
                                 synchronized (longest)
                                 {
                                    if (elapsed >= longest[0])
                                    {
                                       longest[0] = elapsed;
                                       buf.append('*');
                                    }
                                 }
                                 buf.append(" millis)");
                              }
                              else
                              {
                                 if (requested.isShare())
                                 {
                                    buf.append(" --> DOWNGRADE LOCK ");
                                 }
                                 else
                                 {
                                    buf.append(" --> RELEASE LOCK ");
                                 }
                                 buf.append(lockType);
                                 buf.append(" on ");
                                 buf.append(ident);
                                 buf.append(" (after holding for ");
                                 buf.append(elapsed);
                                 buf.append(" millis)");
                              }
                              synchronized (longest)
                              {
                                 System.out.println(buf);
                              }
                           }
                           
                           lockType = requested;
                        }
                     }
                  }
                  catch (Exception exc)
                  {
                     buf.setLength(0);
                     buf.append(tName);
                     buf.append(" --> ");
                     buf.append(exc.getMessage());
                     synchronized (longest)
                     {
                        LOG.severe(buf.toString());
                     }
                  }
               }
            }
         };
         
         for (int i = 0; i < numThreads; i++)
         {
            (new Thread(r, "T" + i)).start();
         }
      }
      catch (Exception exc)
      {
         printUsage();
         LOG.severe("", exc);
      }
   }
   
   /**
    * Implementation of <code>LockAdministrator</code>.
    */
   private class Admin
   implements LockAdministrator
   {
      /**
       * Gather record lock information for the physical database associated
       * with the enclosing lock manager.
       * 
       * @return  A list of {@link com.goldencode.p2j.admin.RecordLockInfo
       *          RecordLockInfo} objects.  If no locks currently are being
       *          held, the list will be empty, but it will not be
       *          <code>null</code>.
       */
      public List<RecordLockInfo> collectRecordLockInfo()
      {
         List<RecordLockInfo> list = null;
         
         synchronized (lockTable)
         {
            if (!lockTable.isEmpty())
            {
               list = new ArrayList<>(lockTable.size());
            }
            
            for (LockStatus<T> status : lockTable.values())
            {
               // only want active record locks, not released locks or table locks
               if (LockType.NONE.equals(status.getLockType()) ||
                   status.getRecordIdentifier() instanceof TableIdentifier)
               {
                  continue;
               }
               
               RecordLockInfo info = status.getLockInfo();
               list.add(info);
            }
         }
         
         if (list == null)
         {
            list = Collections.emptyList();
         }
         
         return list;
      }
   }
}