RecordLockContext.java

/*
** Module   : RecordLockContext.java
** Abstract : Coordinates record locks across record buffers within a session
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 ECF 20070411   @32990 Created initial version. Coordinates record
**                           locks across record buffers within a session.
** 002 ECF 20070414   @32995 Fixed ConcurrentModificationException. Cannot
**                           modify lockStates map while iterating it
**                           except via iterator. Changed collection type
**                           for buffers in LockState to LinkedList from
**                           HashSet to conserve memory.
** 003 SVL 20080512   @38259 Added support for the
**                           RecordBuffer.pinnedLockTypes map.
** 004 ECF 20080821   @39564 Disallow exclusive lock to be acquired unless
**                           a transaction is active. Normally, this will
**                           not occur in converted code. It is added as a
**                           failsafe for hand-written code.
** 005 ECF 20081114   @40454 Fixed record lock leak. Added implementations
**                           of hashCode() and equals() to LockState, to
**                           ensure duplicates are not stored in the hash
**                           sets of LockStates in the locksByBuffer map.
**                           Integrated generics.
** 006 ECF 20090202   @41250 Fixed LockState storage. We now store record
**                           identifiers by buffer instead of LockState
**                           objects by buffer. This prevents duplicate,
**                           conflicting LockState objects from corrupting
**                           state.
** 007 ECF 20090218   @41333 Fixed memory leak. Perm.locksByBuffer is now
**                           a WeakHashMap to prevent RecordBuffer objects
**                           from being pinned in memory.
** 008 SVL 20090708   @43113 Into endTransaction(), if the given record
**                           isn't loaded into any corresponding buffer,
**                           downgrade record's lock to NONE.
** 009 ECF 20090824   @43732 Fixed Perm.relinquishBufferLocks(). We were
**                           leaking RecordIdentifiers in the locksByBuffer
**                           collection, which was causing both a memory leak
**                           and a performance problem.
** 010 ECF 20131028          Moved to lock sub-package.
** 011 CA  20140406          Fixed lock downgrade for buffers surviving a persistent procedure.
** 012 ECF 20160229          Require fewer checks for active transaction when relinquishing locks.
** 013 IAS 20160331          Fixed LockState.equals()
** 014 ECF 20160328          Removed logger, which was not being used. Minor code cleanups.
** 015 ECF 20160820          Modified API to allow for lock timeouts and to report locks actually
**                           released at end of transaction and buffer scope.
** 016 ECF 20160827          Removed TM.isTransaction check from Perm.relinquishBufferLocks,
**                           which should only be called outside a transaction.
** 017 ECF 20160829          Performance enhancement: use local BufferManager to check
**                           transaction status instead of TransactionManager.
** 018 ECF 20200906          New ORM implementation.
** 019 ECF 20210504          BufferManager API name change.
**     CA  20221101          4GL allows an EXCLUSIVE-LOCK even if a full TRANSACTION is not present.
** 020 TJD 20240123          Java 17 compatibility updates
** 021 OM  20240909          Improved Database API. Simplify imports.
** 022 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
** 023 OM  20241211          This is a context specific class and the context is known from the creation time.
*/

/*
** 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.util.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.util.logging.*;

/**
 * An instance of this class coordinates record locking activity across all
 * record buffers for a particular database, within a user session.  While
 * the {@link LockManager} handles the actual synchronization and acquisition
 * of record locks across all sessions for a given database, an instance of
 * <code>RecordLockContext</code> is responsible for ensuring that all record
 * buffers which access a given record lock <i>within a session</i> cooperate
 * properly in their interactions with the lock manager.
 * <p>
 * When a record lock is required, it generally is associated with one or
 * more record buffers in a session.  There are several housekeeping
 * processes in the persistence framework which may temporarily require a
 * lock, but generally, record locks are associated with record buffers as
 * the result of some converted Progress language statement option which
 * (possibly implicitly) determines a record lock (i.e., NO-LOCK, SHARE-LOCK,
 * EXCLUSIVE-LOCK on FIND, GET, FOR, OPEN QUERY, CREATE, etc.).
 * <p>
 * In P2J, this association is made by {@link RecordBuffer} at the time it
 * creates a new record, or when a record found by a database query is stored
 * in the buffer via the {@link RecordBuffer#setRecord setRecord method}.
 * <p>
 * A record lock is associated with a single record;  there can be only one
 * type of lock granted for a particular combination of database, table, and
 * primary key identifier at a time across all sessions.  However, multiple
 * record buffers which contain the same record at the same time <i>within a
 * session</i> all share access to the lock, such that each of them are
 * granted the set of rights (or lack thereof) associated with the lock.
 * Since each session is essentially single threaded, this is safe.
 * <p>
 * Record locking across buffers within a session is asymmetric:  any buffer
 * can acquire and upgrade the communal lock, but all buffers must agree
 * before the communal lock can be downgraded or released.  More specifically,
 * each buffer maintains its own state about its particular requirement for
 * the state of the lock (its "pinned" lock type -- the lock type which will
 * potentially "pin" the state of the communal lock at a certain, minimum
 * level until released by that buffer).  If a buffer's pinned lock type is
 * <code>LockType.NONE</code>, then that buffer effectively does not pin the
 * communal lock at all, and the lock may be released if no other buffers are
 * pinning it to a higher level.  However, if a buffer's pinned lock type is
 * <code>LockType.EXCLUSIVE</code> or <code>LockType.SHARE</code>, the
 * communal lock may not be downgraded or released, respectively, until that
 * buffer changes its pinned type to a lower level.
 * <p>
 * In addition to this requirement for buffers to cooperate for lock
 * downgrade and release, a SHARE lock may not be released, nor an EXCLUSIVE
 * lock downgraded, while an application level transaction is active.  The
 * presence of a transaction trumps the pinning rules noted above.  Once the
 * transaction ends, via commit or rollback, those rules apply again.
 * <p>
 * This class is abstract, and two different subclasses, both defined here, are used to support permanent
 * database tables and temporary tables.  The factory method {@link #get(Persistence, BufferManager, boolean)}
 * is used to acquire an instance of the appropriate type.  It should only be invoked once per database, per
 * session and is only intended for use by the {@link Persistence} class.
 */
public abstract class RecordLockContext
{
   /**
    * Factory method used to retrieve an instance of a
    * <code>RecordLockContext</code> implementation which is appropriate for
    * the database whose records are being managed.  Should only be called by
    * the {@link Persistence} class.
    * <p>
    * The implementation returned for use with temporary tables essentially
    * is a do-nothing implementation, since record locking is a no-op for
    * temp tables.
    * <p>
    * The implementation returned for permanent tables adheres to the rules
    * noted in the class description above.
    * <p>
    * This dual architecture is used so that other classes which use the
    * services of this class do not have to differentiate their behavior for
    * temporary vs. permanent tables.  This permits client classes to remain
    * unaware of the implementation differences for permanent and temporary
    * tables.
    *
    * @param   persistence
    *          Persistence service object which will be used for access to
    *          the global lock manager.
    * @param   bufferManager
    *          Buffer manager which tracks buffer scopes.
    * @param   sharedDb
    *          In case of multi-tenant environment: is the buffer shared or private?
    *
    * @return  An instance of this class.
    */
   public static RecordLockContext get(Persistence persistence, BufferManager bufferManager, boolean sharedDb)
   {
      if (persistence.isTemporary())
      {
         return new RecordLockContext()
         {
         };
      }
      
      return new Perm(persistence, bufferManager, sharedDb);
   }
   
   /**
    * Default constructor accessible only from within this class.
    */
   private RecordLockContext()
   {
   }
   
   /**
    * Register a record buffer to a particular record lock, which must
    * have been created previously.  This creates a bidirectional, one to
    * many association between the buffer and the record lock.
    * <p>
    * Explicit deregistration of this association is unnecessary.  It
    * occurs implicitly when the lock is released and/or the buffer goes
    * out of scope.
    * <p>
    * This implementation does nothing.  It should be overridden for a
    * meaningful implementation.
    *
    * @param   ident
    *          Unique identifier for a particular database record.
    * @param   buffer
    *          Record buffer to be associated with the specified record
    *          lock.
    */
   public void registerBuffer(RecordIdentifier<String> ident, RecordBuffer buffer)
   {
   }
   
   /**
    * Attempt to release all locks associated with the given buffer. Release of any given lock
    * may be deferred if other buffers in the session are pinning the lock.
    * <p>
    * This implementation does nothing.  It should be overridden for a meaningful implementation.
    *
    * @param   buffer
    *          Record buffer whose locks are being relinquished.
    *
    * @return  Set of released locks, or {@code null} if no locks were released. Does not
    *          include locks which were downgraded to SHARE locks. This implementation always
    *          returns {@code null}.
    */
   public Set<Long> relinquishBufferLocks(RecordBuffer buffer)
   {
      return null;
   }
   
   /**
    * Force the unconditional release of a lock. This should be used when a lock was acquired
    * provisionally, but then a circumstance (e.g., error) required it to be released
    * immediately, rather than going through the normal transaction cycle of possible
    * downgrade before release.
    *
    * @param   ident
    *          Record identifier.
    * @param   buffer
    *          Optional buffer whose pinned state should be updated in accordance with the
    *          lock release.
    */
   public void forceRelease(RecordIdentifier<String> ident, RecordBuffer buffer)
   {
   }
   
   /**
    * Process any pending lock changes which have been deferred during the most recent
    * application level transaction.  Specifically, release or downgrade any locks which are
    * no longer being pinned by record buffers.
    * <p>
    * This implementation does nothing.  It should be overridden for a meaningful implementation.
    * 
    * @return  Set of released locks, or {@code null} if no locks were released. Does not
    *          include locks which were downgraded to SHARE locks. This implementation always
    *          returns {@code null}.
    */
   public Set<Long> transactionEnded()
   {
      return null;
   }
   
   /**
    * Request that the communal lock on a record be acquired or otherwise
    * modified to match the requested type.
    * <p>
    * This implementation does nothing.  It should be overridden for a
    * meaningful implementation.
    *
    * @param   ident
    *          Unique identifier for a particular database record.
    * @param   requestedType
    *          The lock type being requested.
    * @param   timeout
    *          Positive number of milliseconds to wait to acquire lock before
    *          raising an exception; specify 0 to wait indefinitely. Ignored if
    *          a no-wait lock variant is requested.
    * @param   buffer
    *          Record buffer on whose behalf the request is being made.
    *          May be <code>null</code> in cases where the association
    *          will be made later or if the lock is being managed for
    *          framework housekeeping purposes.
    *
    * @return  The actual lock type held as a result of the invocation.
    *
    * @throws  LockTimeoutException
    *          never.
    * @throws  LockUnavailableException
    *          never.
    */
   public LockType lock(RecordIdentifier<String> ident,
                        LockType requestedType,
                        long timeout,
                        RecordBuffer buffer)
   throws LockUnavailableException,
          LockTimeoutException
   {
      return LockType.NONE;
   }
   
   /**
    * Request that the communal lock on a record be acquired or otherwise
    * modified to match the requested type.  If a record buffer is
    * provided, its pinned lock type is adjusted to match the request.
    * <p>
    * The communal lock may or may not be modified immediately, according
    * to the rules described in this class' description.  Generally, a
    * request to upgrade or acquire the lock for the first time is honored
    * immediately.  However, a request to downgrade or release the lock
    * may be deferred.
    *
    * @param   ident
    *          Unique identifier for a particular database record.
    * @param   requestedType
    *          The lock type being requested.
    *
    * @return  The actual lock type held as a result of the invocation.
    *
    * @throws  LockUnavailableException
    *          if a no-wait lock variant is requested, and another session
    *          currently holds the lock.  This will never be thrown in
    *          cases where a downgrade or release is being requested.
    */
   public LockType lock(RecordIdentifier<String> ident, LockType requestedType)
   throws LockUnavailableException
   {
      return lock(ident, requestedType, 0L, null);
   }
   
   /**
    * Concrete implementation of <code>RecordLockContext</code> which enables
    * intra-session record lock sharing across record buffers for permanent
    * database tables.
    */
   private static class Perm
   extends RecordLockContext
   {
      /** Persistence service object through which lock manager is accessed */
      private final Persistence persistence;
      
      /** Buffer manager which tracks buffer scopes */
      private final BufferManager bufferManager;
      
      /** In case of multi-tenant environment: is the buffer shared or private? */
      private final boolean sharedDb;
      
      /** Mapping of record identifiers to lock states */
      private final Map<RecordIdentifier<String>, LockState> lockStates = new HashMap<>();
      
      /** Mapping of buffers to lock state sets */
      private final Map<RecordBuffer, Set<RecordIdentifier<String>>> locksByBuffer = new WeakHashMap<>();
      
      /**
       * Constructor accessible only from within this class.
       *
       * @param   persistence
       *          Persistence service object which will be used for access to
       *          the global lock manager.
       * @param   bufferManager
       *          Buffer manager which tracks buffer scopes.
       * @param   sharedDb
       *          In case of multi-tenant environment: is the buffer shared or private?
       */
      private Perm(Persistence persistence, BufferManager bufferManager, boolean sharedDb)
      {
         this.persistence = persistence;
         this.bufferManager = bufferManager;
         this.sharedDb = sharedDb;
      }
      
      /**
       * Register a record buffer to a particular record lock, which must
       * have been created previously.  This creates a bidirectional, one to
       * many association between the buffer and the record lock.
       * <p>
       * Explicit deregistration of this association is unnecessary.  It
       * occurs implicitly when the lock is released and/or the buffer goes
       * out of scope.
       *
       * @param   ident
       *          Unique identifier for a particular database record.
       * @param   buffer
       *          Record buffer to be associated with the specified record
       *          lock.
       */
      @Override
      public void registerBuffer(RecordIdentifier<String> ident, RecordBuffer buffer)
      {
         LockState state = getLockState(ident, false);
         if (state == null)
         {
            return;
         }
         
         if (state.registerBuffer(buffer))
         {
            Set<RecordIdentifier<String>> locks = locksByBuffer.computeIfAbsent(buffer, k -> new HashSet<>(4));
            locks.add(ident);
         }
      }
      
      /**
       * Request that the communal lock on a record be acquired or otherwise
       * modified to match the requested type.  If a record buffer is
       * provided, the pinned lock type of the specified record is adjusted to
       * match the request.
       * <p>
       * The communal lock may or may not be modified immediately, according
       * to the rules described in this class' description.  Generally, a
       * request to upgrade or acquire the lock for the first time is honored
       * immediately.  However, a request to downgrade or release the lock
       * may be deferred.
       *
       * @param   ident
       *          Unique identifier for a particular database record.
       * @param   requestedType
       *          The lock type being requested.
       * @param   timeout
       *          Positive number of milliseconds to wait to acquire lock before
       *          raising an exception; specify 0 to wait indefinitely. Ignored if
       *          a no-wait lock variant is requested.
       * @param   buffer
       *          Record buffer on whose behalf the request is being made.
       *          May be <code>null</code> in cases where the association
       *          will be made later or if the lock is being managed for
       *          framework housekeeping purposes.
       *
       * @return  The actual lock type held as a result of the invocation.
       *
       * @throws  LockUnavailableException
       *          if a no-wait lock variant is requested, and another session
       *          currently holds the lock.  This will never be thrown in
       *          cases where a downgrade or release is being requested.
       * @throws  IllegalStateException
       *          if an exclusive lock is requested outside an active transaction.
       */
      @Override
      public LockType lock(RecordIdentifier<String> ident,
                           LockType requestedType,
                           long timeout,
                           RecordBuffer buffer)
      throws LockUnavailableException
      {
         if (buffer != null)
         {
            // Set lock type buffer currently is pinning to requested type.
            buffer.setPinnedLockType(ident.getRecordID(), requestedType);
         }
         
         boolean create = (requestedType != LockType.NONE);
         LockState state = getLockState(ident, create);
         
         if (state == null)
         {
            return LockType.NONE;
         }
         
         LockType currentType = state.getCurrentType();
         int comparison = currentType.compareTo(requestedType);
         
         if (comparison < 0)
         {
            // current type is less restrictive than requested type; attempt upgrade immediately
            
            // disallow exclusive lock outside a transaction
            // 4GL allows an EXCLUSIVE-LOCK even if a full TRANSACTION is not present
            /*
            if (requestedType.isExclusive() && !bufferManager.isTransaction())
            {
               throw new IllegalStateException(
                  "Cannot acquire an exclusive lock outside a transaction (" + ident + ")");
            }
            */
            
            // perform upgrade
            persistence.lock(requestedType, ident, true, timeout, sharedDb);
            state.setCurrentType(requestedType);
         }
         else if (comparison > 0 && !bufferManager.isTransaction())
         {
            // current type is more restrictive than requested type; must follow rules to
            // downgrade or release lock, but only if outside a transaction
            state.relinquish(requestedType);
         }
         
         currentType = state.getCurrentType();
         if (currentType == LockType.NONE)
         {
            lockStates.remove(ident);
         }
         
         return currentType;
      }
      
      /**
       * Attempt to release all locks associated with the given buffer. Release of any given lock
       * may be deferred if other buffers in the session are pinning the lock.
       * <p>
       * This method only should be invoked outside of a transaction.
       *
       * @param   buffer
       *          Record buffer whose locks are being relinquished.
       * 
       * @return  Set of released locks, or {@code null} if no locks were released. Does not
       *          include locks which were downgraded to SHARE locks.
       */
      @Override
      public Set<Long> relinquishBufferLocks(RecordBuffer buffer)
      {
         Set<RecordIdentifier<String>> locks = locksByBuffer.get(buffer);
         
         if (locks == null)
         {
            return null;
         }
         
         Set<Long> released = null;
         
         try
         {
            Iterator<RecordIdentifier<String>> iter = locks.iterator();
            while (iter.hasNext())
            {
               RecordIdentifier<String> ident = iter.next();
               LockState state = getLockState(ident, false);
               if (state != null)
               {
                  state.relinquish(LockType.NONE);
                  if (state.getCurrentType() == LockType.NONE)
                  {
                     lockStates.remove(ident);
                     iter.remove();
                     
                     if (released == null)
                     {
                        released = new HashSet<>();
                     }
                     released.add(ident.getRecordID());
                  }
               }
               else
               {
                  iter.remove();
               }
            }
         }
         catch (LockUnavailableException exc)
         {
            // never thrown
         }
         finally
         {
            if (locks.isEmpty())
            {
               locksByBuffer.remove(buffer);
            }
         }
         
         return released;
      }
      
      /**
       * Force the unconditional release of a lock. This should be used when a lock was acquired
       * provisionally, but then a circumstance (e.g., error) required it to be released
       * immediately, rather than going through the normal transaction cycle of possible
       * downgrade before release.
       *
       * @param   ident
       *          Record identifier.
       * @param   buffer
       *          Optional buffer whose pinned state should be updated in accordance with the
       *          lock release.
       */
      @Override
      public void forceRelease(RecordIdentifier<String> ident, RecordBuffer buffer)
      {
         if (buffer != null)
         {
            // set lock type buffer currently is pinning to NONE.
            buffer.setPinnedLockType(ident.getRecordID(), LockType.NONE);
         }
         
         LockState state = lockStates.remove(ident);
         if (state != null)
         {
            try
            {
               state.updateLock(LockType.NONE);
            }
            catch (LockUnavailableException exc)
            {
               // not thrown on release
            }
         }
      }
      
      /**
       * Process any pending lock changes which have been deferred during the most recent
       * application level transaction.  Specifically, release or downgrade any locks which are
       * no longer being pinned by record buffers.
       * 
       * @return  Set of released locks, or {@code null} if no locks were released. Does not
       *          include locks which were downgraded to SHARE locks.
       */
      @Override
      public Set<Long> transactionEnded()
      {
         Set<Long> released = null;
         
         try
         {
            Iterator<LockState> iter = lockStates.values().iterator();
            while (iter.hasNext())
            {
               LockState state = iter.next();
               state.transactionEnded();
               if (state.getCurrentType() == LockType.NONE)
               {
                  iter.remove();
                  
                  if (released == null)
                  {
                     released = new HashSet<>();
                  }
                  released.add(state.getIdentifier().getRecordID());
               }
            }
         }
         catch (LockUnavailableException exc)
         {
            // never thrown
         }
         
         return released;
      }
      
      /**
       * Get the <code>LockState</code> object associated with the given
       * record identifier, optionally creating it if it does not yet exist.
       *
       * @param   ident
       *          Unique identifier for a particular database record.
       * @param   create
       *          If {@code true}, create the lock state object if not found; otherwise do not create it.
       * 
       * @return  <code>LockState</code> for given identifier.
       */
      private LockState getLockState(RecordIdentifier<String> ident, boolean create)
      {
         LockState state = lockStates.get(ident);
         if (state == null && create)
         {
            state = new LockState(ident);
            lockStates.put(ident, state);
         }
         
         return state;
      }
      
      /**
       * The lock state of a particular record, which includes its current
       * lock type, its unique identifier, and the set of buffers which share
       * access to the backing lock.
       */
      private final class LockState
      {
         /** Database record whose lock state is being tracked */
         private final RecordIdentifier<String> ident;
         
         /** Buffers who share this lock state */
         private final List<RecordBuffer> buffers = new LinkedList<>();
         
         /** Lock type currently assigned to the record */
         private LockType currentType;
         
         /**
          * Constructor which queries current lock type for a record from the lock manager.
          *
          * @param   ident
          *          Unique identifier for the database record whose lock
          *          type is to be queried from the lock manager.
          */
         LockState(RecordIdentifier<String> ident)
         {
            this.ident = ident;
            this.currentType = persistence.queryLock(ident, sharedDb);
         }
         
         /**
          * Override parent's implementation to be consistent with {@link #equals(Object)}.
          * 
          * @return  Hash code for this object.
          */
         @Override
         public int hashCode()
         {
            return (37 * 17 + ident.hashCode());
         }
         
         /**
          * Override parent's implementation of equality.  Two instances of
          * this class are considered equal if they represent the same
          * lockable resource, regardless of the other internal state.
          * 
          * @param   o
          *          Another instance of this class to test for equality with
          *          this instance.
          * 
          * @return  <code>true</code> if this object and the given instance
          *          have the same record identifier, else <code>false</code>.
          */
         @Override
         public boolean equals(Object o)
         {
            if (!(o instanceof LockState))
            {
               return false;
            }
            
            return ident.equals(((LockState) o).ident);
         }
         
         /**
          * Register a record buffer which shares access to the record's
          * current lock.  Deregistration occurs implicitly when this buffer
          * no longer pins the lock.
          *
          * @param   buffer
          *          Record buffer to be registered.
          *
          * @return  <code>true</code> if the buffer was added;
          *          <code>false</code> if it was previously registered.
          */
         boolean registerBuffer(RecordBuffer buffer)
         {
            if (!buffers.contains(buffer))
            {
               buffers.add(buffer);
               
               return true;
            }
            
            return false;
         }
         
         /**
          * Get record's unique identifier.
          *
          * @return  Unique identifier.
          */
         RecordIdentifier<String> getIdentifier()
         {
            return ident;
         }
         
         /**
          * Get the current lock type.
          *
          * @return  Current lock type.
          */
         LockType getCurrentType()
         {
            return currentType;
         }
         
         /**
          * Set the current lock type.  This will always set the current type
          * to a <i>non-</i>no-wait variant.
          *
          * @param   currentType
          *          Current type to be set.  Only the category is used
          *          (i.e., NONE, SHARE, EXCLUSIVE);  the no-wait-"ness" of
          *          the lock type is discarded.
          */
         void setCurrentType(LockType currentType)
         {
            this.currentType = currentType.toWaitVariant();
         }
         
         /**
          * Process any pending lock change which has been deferred during the most recent
          * application level transaction. Specifically, release or downgrade a lock according
          * to how it is pinned by all record buffers which reference it.
          * 
          * @throws  LockUnavailableException
          *          never, since we are never acquiring a more restrictive lock, only
          *          downgrading or releasing.
          */
         void transactionEnded()
         throws LockUnavailableException
         {
            if (currentType == LockType.NONE)
            {
               return;
            }
            
            int currentScope = bufferManager.getOpenBufferScopes();
            LockType maxLock = LockType.NONE;
            
            Iterator<RecordBuffer> iter = buffers.iterator();
            while (iter.hasNext())
            {
               RecordBuffer buffer = iter.next();
               
               Long id = ident.getRecordID();
               LockType pinned = buffer.getPinnedLockType(id);
               if (pinned == LockType.NONE)
               {
                  iter.remove();
                  
                  continue;
               }
               
               Record dmo = buffer.getCurrentRecord();
               if (dmo == null || !id.equals(dmo.primaryKey()))
               {
                  // the buffer no longer holds the record
                  pinned = LockType.NONE;
               }
               else
               {
                  // scope depth at which this buffer's scope was first opened
                  int activeScopeDepth = buffer.getScopeOpenDepth();
                  boolean worldScope = buffer.isWorldScope();
                  
                  if (activeScopeDepth > 0 && (worldScope || currentScope > activeScopeDepth))
                  {
                     // Buffer scope pinning this record lock is still open AND the record is
                     // currently in the buffer AND we're not yet in the block scope in which
                     // the buffer scope will end. Downgrade to (or maintain) share lock.
                     pinned = LockType.SHARE;
                  }
                  else
                  {
                     // buffer scope is closed; stop pinning the lock
                     pinned = LockType.NONE;
                  }
               }
               
               buffer.setPinnedLockType(id, pinned);
               
               if (pinned == LockType.NONE)
               {
                  iter.remove();
               }
               
               // remember the most restrictive lock type pinned by any buffer
               if (pinned.compareTo(maxLock) > 0)
               {
                  maxLock = pinned;
               }
            }
            
            // downgrade or remove lock context-wide
            updateLock(maxLock);
         }
         
         /**
          * Attempt to relinquish the lock represented by this object, releasing or downgrading
          * it if possible.  A change is only possible outside a transaction, and then only to
          * the most restrictive lock pinned by any buffer sharing the communal lock.
          * 
          * @param   requestedType
          *          NONE or SHARE only (it doesn't make sense to downgrade to EXCLUSIVE).
          * 
          * @throws  IllegalArgumentException
          *          if an EXCLUSIVE lock is requested.
          * @throws  LockUnavailableException
          *          never, since we never request a no-wait lock here.
          */
         void relinquish(LockType requestedType)
         throws LockUnavailableException
         {
            // if there is no change to be made, nothing more to do
            if (currentType == requestedType.toWaitVariant())
            {
               return;
            }
            
            if (requestedType.isExclusive())
            {
               throw new IllegalArgumentException("Cannot downgrade lock type to EXCLUSIVE");
            }
            
            // Walk through buffers and determine whether any one would pin the lock to SHARE.
            // If not, release the lock, otherwise make it SHARE.
            // NOTE: Since we are outside of a transaction, no buffer will be pinning to EXCLUSIVE.
            //       Also, requestedType will only ever be NONE or SHARE, so this algorithm is safe.
            Iterator<RecordBuffer> iter = buffers.iterator();
            while (iter.hasNext())
            {
               RecordBuffer buffer = iter.next();
               LockType pinned = buffer.getPinnedLockType(ident.getRecordID());
               
               if (pinned == LockType.NONE)
               {
                  // buffer no longer pins the lock, so remove its association.
                  iter.remove();
               }
               else
               {
                  // can't go below SHARE
                  requestedType = LockType.SHARE;
               }
            }
            
            updateLock(requestedType);
         }
         
         /**
          * Attempt to update the current lock on the record immediately.
          *
          * @param   lockType
          *          Requested lock type.  If NONE, this object will be removed from the map of lock states by
          *          identifier.
          *
          * @throws  LockUnavailableException
          *          never.
          */
         private void updateLock(LockType lockType)
         throws LockUnavailableException
         {
            if (lockType.toWaitVariant() != currentType)
            {
               persistence.lock(lockType, ident, true, sharedDb);
               setCurrentType(lockType);
            }
         }
      }
   }
}