# 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);
                }
             }
          }
       }
    }