/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.github.geophile.erdo.transaction;
import com.github.geophile.erdo.AbstractKey;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
// About synchronization: All the public methods here are synchronized. However, synchronization
// only covers the bucket's state. Transaction state can change at any time:
// active -> committed/aborted/deadlocked -> irrelevant.
class LockManagerBucket
{
@Override
public String toString()
{
return String.format("b%s", bucketNumber);
}
// lock() returns normally when the lock on key has been granted to transaction.
// A TransactionException is thrown if the transaction cannot be granted the lock.
// This can occur in the following situations:
// - The key was locked by a transaction that is now committed, but was active when the
// current transaction started.
// - The key was locked by an active transaction when this transaction requested
// the lock. When that transaction committed, it marked this transaction one as a conflict
// victim.
// - This transaction was selected as a deadlock victim.
// Whoever catches the TransactionException needs to initiate the rollback. Rollback
// will lock buckets, so initiating it here will lead to java deadlock (not transaction
// deadlock).
public synchronized void lock(AbstractKey key, Transaction transaction)
throws InterruptedException,
DeadlockException,
TransactionRolledBackException
{
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: {1} locking {2}",
new Object[]{this, transaction, key});
}
Transaction lockOwner = locks.get(key);
if (lockOwner == null) {
locks.put(key, transaction);
} else if (lockOwner == transaction) {
// transaction already has the lock
} else {
// there is a conflict
boolean firstAttempt = true;
do {
if (lockOwner.hasCommitted()) {
transaction.rollback();
transaction.throwExceptionDueToTragicEnding();
} else {
LockConflict conflict;
if (firstAttempt) {
conflict = conflicts.get(key);
if (conflict == null) {
// First conflict for key
conflict = new LockConflict(key, lockOwner);
conflicts.put(key, conflict);
}
conflict.addWaiter(transaction);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: {1} waiting for {2}: conflict: {3}",
new Object[]{this, transaction, key, conflict});
}
}
// else: conflict was set following the wait. If we stayed in the loop, then
// we didn't throw (following transaction ending tragically), and lockOwner !=
// transaction. The assertion in the else below implies that conflict != null.
// Also, if we waited once, then we don't want to do conflict.addWaiter again.
wait();
firstAttempt = false;
if (transaction.endedTragically()) {
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: {1} can''t lock {2} and will abort",
new Object[]{this, transaction, key});
}
// Remove assuming that there is usually just one waiter, (and it's
// being removed). If there are more waiters, conflict will be put back.
conflict = conflicts.remove(key);
if (conflict != null) {
conflict.removeWaiter(transaction);
if (conflict.hasWaiters()) {
conflicts.put(key, conflict);
}
}
transaction.throwExceptionDueToTragicEnding();
} else {
// transaction was waiting for the lock. So if the transaction didn't
// end tragically (deadlock or conflict with committed), then it must
// now be the owner or it must still be waiting. So someone must own
// the lock, (lockOwner != null).
lockOwner = locks.get(key);
assert
lockOwner != null
: String.format("%s notices no owner for %s", transaction, key);
conflict = conflicts.get(key);
assert
lockOwner == transaction ||
conflict != null && conflict.hasWaiter(transaction)
: String.format("transaction: %s, lockOwner: %s, key: %s",
lockOwner, transaction, key);
}
}
} while (lockOwner != transaction);
}
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: {1} locked {2}",
new Object[]{this, transaction, key});
}
LockConflict conflict = conflicts.get(key);
assert
conflict == null || !conflict.hasWaiter(transaction)
: String.format("transaction: %s, key: %s, conflict: %s", transaction, key, conflict);
}
public synchronized void abortAllWaiters(Transaction transaction)
{
boolean log = !conflicts.isEmpty();
if (log && LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: abort all waiters for {1}: {2}",
new Object[]{this, transaction, conflictsDescription()});
}
assert transaction.hasCommitted() : transaction;
// Clear out all the waiters of every conflict owned by transaction.
// But leave locks alone until the transaction becomes irrelevant, (commit timestamp
// less than the start timestamp of all active transactions).
for (Iterator<Map.Entry<AbstractKey, LockConflict>> conflictScan =
conflicts.entrySet().iterator();
conflictScan.hasNext(); ) {
LockConflict conflict = conflictScan.next().getValue();
if (conflict.lockOwner() == transaction) {
markAllWaitersAsConflictVictims(transaction, conflict);
conflictScan.remove();
}
}
if (log && LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: aborted all waiters for {1}: {2}",
new Object[]{this, transaction, conflictsDescription()});
}
}
public synchronized void releaseLocks(List<AbstractKey> keys)
{
for (AbstractKey key : keys) {
Transaction formerLockOwner = locks.remove(key);
LockConflict conflict = conflicts.remove(key);
Transaction formerConflictOwner = conflict == null ? null : conflict.lockOwner();
assert
formerConflictOwner == null || formerLockOwner == formerConflictOwner
: String.format("key: %s, formerLockOwner: %s, conflict: %s",
key, formerLockOwner, conflict);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: releaseLocks: {1}",
new Object[]{this, conflict});
}
if (conflict != null) {
Transaction newLockOwner = conflict.giveLockToFirstWaiter();
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: Removed {1} as owner of {2}, new owner is {3}",
new Object[]{this, formerConflictOwner, conflict.key(), newLockOwner});
}
if (newLockOwner != null) {
if (conflict.hasWaiters()) {
// Removal was too optimistic
conflicts.put(key, conflict);
}
locks.put(key, newLockOwner);
}
}
}
// Waiters may have aborted and transactions may have been marked irrelevant.
// Wake up all blocked threads and give them a chance to make progress.
notifyAll();
}
public synchronized void wakeUp()
{
notifyAll();
}
public synchronized void findDependencies(Map<Transaction, WaitsFor> dependencies)
{
for (LockConflict conflict : conflicts.values()) {
conflict.findDependencies(dependencies);
}
}
public LockManagerBucket(int bucketNumber)
{
this.bucketNumber = bucketNumber;
}
private void markAllWaitersAsConflictVictims(Transaction transaction, LockConflict conflict)
{
// Transaction committed. All waiters have to abort. Just mark them
// as being in conflict, but don't actually carry out the abort here.
// Abort will lock buckets, so this would lead to java deadlock (not transaction
// deadlock).
Collection<Transaction> waiters = conflict.waiters();
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO,
"{0}: Marking transactions waiting on {1} as conflict victims: {2}",
new Object[]{this, transaction, waiters});
}
for (Transaction waiter : waiters) {
waiter.rollback();
}
}
public synchronized void describeConflicts(List<String> conflictDescriptions)
{
for (LockConflict conflict : conflicts.values()) {
conflictDescriptions.add(conflict.toString());
}
}
// For use by this class
private String conflictsDescription()
{
StringBuilder buffer = new StringBuilder();
for (LockConflict conflict : conflicts.values()) {
if (buffer.length() > 0) {
buffer.append(", ");
}
buffer.append(conflict.toString());
}
return buffer.toString();
}
// Class state
private static final Logger LOG = Logger.getLogger(LockManagerBucket.class.getName());
// Object state
private final int bucketNumber;
// locks contains an entry for each key whose lock is owned by a transaction.
// conflicts contains an entry only when there are actual conflicts on the key.
// This penalizes lock and unlock operations slightly, but limits deadlock detection
// to the conflicts map, which should normally be much smaller.
private final Map<AbstractKey, Transaction> locks = new HashMap<>();
private final Map<AbstractKey, LockConflict> conflicts = new HashMap<>();
}