/* * 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.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import static java.lang.Math.abs; public class LockManager { // LockManager interface public void lock(AbstractKey key, Transaction transaction) throws InterruptedException, DeadlockException, TransactionRolledBackException { bucket(key).lock(key, transaction); } public void transactionCommitted(Transaction transaction, List<Transaction> irrelevantTransactions) { LOG.log(Level.INFO, "Aborting transactions waiting on keys locked by {0}", transaction); boolean mustVisit[] = possiblyLockedBuckets(transaction); for (int b = 0; b < nBuckets; b++) { if (mustVisit[b]) { buckets[b].abortAllWaiters(transaction); } } releaseLocksHeldByIrrelevantTransactions(irrelevantTransactions); } public void transactionAborted(Transaction transaction, List<Transaction> irrelevantTransactions) { LOG.log(Level.INFO, "Releasing locks held by {0}", transaction); assert irrelevantTransactions.contains(transaction); releaseLocksHeldByIrrelevantTransactions(irrelevantTransactions); } public void killDeadlockVictims() throws IOException, InterruptedException { Set<Transaction> deadlockVictims = deadlockDetector.victims(); if (LOG.isLoggable(Level.INFO) && !deadlockVictims.isEmpty()) { LOG.log(Level.INFO, "Deadlock victims: {0}", deadlockVictims); for (LockManagerBucket bucket : buckets) { List<String> conflicts = new ArrayList<>(); bucket.describeConflicts(conflicts); for (String conflict : conflicts) { LOG.log(Level.INFO, "deadlock: {0}", conflict); } } } for (Transaction deadlockVictim : deadlockVictims) { deadlockVictim.markDeadlockVictim(); } for (LockManagerBucket bucket : buckets) { bucket.wakeUp(); } } public LockManager() { this(DEFAULT_BUCKETS); } public LockManager(int nBuckets) { this.nBuckets = nBuckets; this.buckets = new LockManagerBucket[nBuckets]; for (int b = 0; b < nBuckets; b++) { buckets[b] = new LockManagerBucket(b); } this.deadlockDetector = new DeadlockDetector(this); } // For use by this package // This is used by DeadlockDetector. The output map is keyed by WaitsFor.waiter(). // We get an atomic of view of each bucket, but not a point-in-time view for all buckets. // That's OK because the state pertaining to deadlock persists, and inconsistencies in // other transactions don't matter. Map<Transaction, WaitsFor> dependencies() { Map<Transaction, WaitsFor> dependencies = new ConcurrentHashMap<>(); for (LockManagerBucket bucket : buckets) { bucket.findDependencies(dependencies); } return dependencies; } // For testing void waitOnConflict(boolean waitOnConflict) { this.waitOnConflict = waitOnConflict; } // For use by this class private void releaseLocksHeldByIrrelevantTransactions(List<Transaction> irrelevantTransactions) { for (Transaction irrelevantTransaction : irrelevantTransactions) { assert irrelevantTransaction.irrelevant() : irrelevantTransaction; } List<List<AbstractKey>> lockedKeysByBucket = lockedKeysByBucket(irrelevantTransactions); for (int b = 0; b < nBuckets; b++) { List<AbstractKey> keys = lockedKeysByBucket.get(b); if (keys != null) { if (LOG.isLoggable(Level.INFO) && !keys.isEmpty()) { LOG.log(Level.INFO, "release locks for bucket {0}: {1}", new Object[]{b, keys}); } buckets[b].releaseLocks(keys); } } } private List<List<AbstractKey>> lockedKeysByBucket(List<Transaction> transactions) { List<List<AbstractKey>> keysByBucket = new ArrayList<>(nBuckets); for (int bucketNumber = 0; bucketNumber < nBuckets; bucketNumber++) { keysByBucket.add(null); } for (Transaction transaction : transactions) { for (AbstractKey key : transaction.lockedForWrite()) { int bucketNumber = bucketNumber(key); LOG.log(Level.INFO, "lockedKeysByBucket {0} {1}: bucket {2}", new Object[]{transaction, key, bucketNumber}); List<AbstractKey> keys = keysByBucket.get(bucketNumber); if (keys == null) { keys = new ArrayList<>(); keysByBucket.set(bucketNumber, keys); } keys.add(key); } } return keysByBucket; } private LockManagerBucket bucket(AbstractKey key) { return buckets[bucketNumber(key)]; } private boolean[] possiblyLockedBuckets(Transaction transaction) { boolean[] possiblyLockedBuckets = new boolean[nBuckets]; Set<AbstractKey> locked = transaction.lockedForWrite(); // TODO: Restore the original code if (false) { // locked.size() < DEFAULT_BUCKETS) { for (AbstractKey key : locked) { possiblyLockedBuckets[bucketNumber(key)] = true; } } else { // If locked.size() is sufficiently high we'll probably need to visit all buckets // anyway. No point in visiting every locked key to discover that fact. Arrays.fill(possiblyLockedBuckets, true); } return possiblyLockedBuckets; } private int bucketNumber(AbstractKey key) { return abs(key.hashCode() % nBuckets); } // Class state static final int DEFAULT_BUCKETS = 100; private static final Logger LOG = Logger.getLogger(LockManager.class.getName()); // Object state private final int nBuckets; private final LockManagerBucket[] buckets; private final DeadlockDetector deadlockDetector; volatile boolean waitOnConflict = true; // Sometimes false in tests }