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