// Copyright 2017 JanusGraph Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.janusgraph.diskstorage.locking; import java.time.Duration; import java.time.Instant; import java.util.Iterator; import java.util.Map; import org.janusgraph.diskstorage.TemporaryBackendException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import org.janusgraph.diskstorage.util.time.TimestampProvider; import org.janusgraph.diskstorage.util.time.TimestampProviders; import org.janusgraph.diskstorage.StaticBuffer; import org.janusgraph.diskstorage.keycolumnvalue.StoreTransaction; import org.janusgraph.diskstorage.locking.consistentkey.ConsistentKeyLockStatus; import org.janusgraph.diskstorage.locking.consistentkey.ConsistentKeyLocker; import org.janusgraph.diskstorage.locking.consistentkey.ConsistentKeyLockerSerializer; import org.janusgraph.diskstorage.util.KeyColumn; import org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration; import org.janusgraph.util.stats.MetricManager; /** * Abstract base class for building lockers. Implements locking between threads * using {@link LocalLockMediator} but delegates inter-process lock resolution * to its subclasses. * * @param <S> An implementation-specific type holding information about a single * lock; see {@link ConsistentKeyLockStatus} for an example * @see ConsistentKeyLocker */ public abstract class AbstractLocker<S extends LockStatus> implements Locker { /** * Uniquely identifies a process within a domain (or across all domains, * though only intra-domain uniqueness is required) */ protected final StaticBuffer rid; /** * Sole source of time. All fields of type long that represent times or * durations should be expressed in terms of * {@link TimestampProvider#getUnit()}. Furthermore, if the locking backend * allows the client to set a timestamp on writes, those timestamps should * be in the same units. * <p> * Don't call {@link System#currentTimeMillis()} or * {@link System#nanoTime()} directly. Use only this object. This object is * replaced with a mock during testing to give tests exact control over the * flow of time. */ protected final TimestampProvider times; /** * This is sort-of Cassandra/HBase specific. It concatenates * {@link KeyColumn} arguments into a single StaticBuffer containing the key * followed by the column and vice-versa. */ protected final ConsistentKeyLockerSerializer serializer; /** * Resolves lock contention by multiple threads. */ protected final LocalLockMediator<StoreTransaction> llm; /** * Stores all information about all locks this implementation has taken on * behalf of any {@link StoreTransaction}. It is parameterized in a type * specific to the concrete subclass, so that concrete implementations can * store information specific to their locking primitives. */ protected final LockerState<S> lockState; /** * The amount of time, in {@link #times}{@code .getUnit()}, that may pass * after writing a lock before it is considered to be invalid and * automatically unlocked. */ protected final Duration lockExpire; protected final Logger log; private static final String M_LOCKS = "locks"; private static final String M_WRITE = "write"; private static final String M_CHECK = "check"; private static final String M_DELETE = "delete"; private static final String M_CALLS = "calls"; private static final String M_EXCEPTIONS = "exceptions"; /** * Abstract builder for this Locker implementation. See * {@link ConsistentKeyLocker} for an example of how to subclass this * abstract builder into a concrete builder. * <p/> * If you're wondering why the bounds for the type parameter {@code B} looks so hideous, see: * <p/> * <a href="https://weblogs.java.net/blog/emcmanus/archive/2010/10/25/using-builder-pattern-subclasses">Using the builder pattern with subclasses by Eamonn McManus</a> * * @param <S> The concrete type of {@link LockStatus} * @param <B> The concrete type of the subclass extending this builder */ public static abstract class Builder<S, B extends Builder<S, B>> { protected StaticBuffer rid; protected TimestampProvider times; protected ConsistentKeyLockerSerializer serializer; protected LocalLockMediator<StoreTransaction> llm; protected LockerState<S> lockState; protected Duration lockExpire; protected Logger log; public Builder() { this.rid = null; //TODO: can we ensure that this is always set correctly? Check the AstyanaxRecipe this.times = TimestampProviders.NANO; this.serializer = new ConsistentKeyLockerSerializer(); this.llm = null; // redundant, but it preserves this constructor's overall pattern this.lockState = new LockerState<S>(); this.lockExpire = GraphDatabaseConfiguration.LOCK_EXPIRE.getDefaultValue(); this.log = LoggerFactory.getLogger(AbstractLocker.class); } /** * Concrete subclasses should just "{@code return this;}". * * @return concrete subclass instance */ protected abstract B self(); public B rid(StaticBuffer rid) { this.rid = rid; return self(); } public B times(TimestampProvider times) { this.times = times; return self(); } public B serializer(ConsistentKeyLockerSerializer serializer) { this.serializer = serializer; return self(); } public B mediator(LocalLockMediator<StoreTransaction> mediator) { this.llm = mediator; return self(); } /** * Retrieve the mediator associated with {@code name} via {@link LocalLockMediators#get(String)}. * * @param name the mediator name * @return this builder */ public B mediatorName(String name) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(times, "Timestamp provider must be set before initializing local lock mediator"); mediator(LocalLockMediators.INSTANCE.<StoreTransaction>get(name, times)); return self(); } public B logger(Logger log) { this.log = log; return self(); } public B lockExpire(Duration d) { this.lockExpire = d; return self(); } /** * This method is only intended for testing. Calling this in production * could cause lock failures. * * @param state the initial lock state for this instance * @return this builder */ public B internalState(LockerState<S> state) { this.lockState = state; return self(); } /** * Inspect and modify this builder's state after the client has called * {@code build()}, but before a return object has been instantiated. * This is useful for catching illegal values or translating placeholder * configuration values into the objects they represent. This is * intended to be called from subclasses' build() methods. */ protected void preBuild() { if (null == llm) { llm = getDefaultMediator(); } } /** * Get the default {@link LocalLockMediator} for Locker being built. * This is called when the client doesn't specify a locker. * * @return a lock mediator */ protected abstract LocalLockMediator<StoreTransaction> getDefaultMediator(); } public AbstractLocker(StaticBuffer rid, TimestampProvider times, ConsistentKeyLockerSerializer serializer, LocalLockMediator<StoreTransaction> llm, LockerState<S> lockState, Duration lockExpire, Logger log) { this.rid = rid; this.times = times; this.serializer = serializer; this.llm = llm; this.lockState = lockState; this.lockExpire = lockExpire; this.log = log; } /** * Try to take/acquire/write/claim a lock uniquely identified within this * {@code Locker} by the {@code lockID} argument on behalf of {@code tx}. * * @param lockID identifies the lock * @param tx identifies the process claiming this lock * @return a {@code LockStatus} implementation on successful lock aquisition * @throws Throwable if the lock could not be taken/acquired/written/claimed or * the attempted write encountered an error */ protected abstract S writeSingleLock(KeyColumn lockID, StoreTransaction tx) throws Throwable; /** * Try to verify that the lock identified by {@code lockID} is already held * by {@code tx}. The {@code lockStatus} argument refers to the object * returned by a previous call to * {@link #writeSingleLock(KeyColumn, StoreTransaction)}. This should be a * read-only operation: return if the lock is already held, but this method * finds that it is not held, then throw an exception instead of trying to * acquire it. * <p/> * This method is only useful with nonblocking locking implementations try * to lock and then check the outcome of the attempt in two separate stages. * For implementations that build {@code writeSingleLock(...)} on a * synchronous locking primitive, such as a blocking {@code lock()} method * or a blocking semaphore {@code p()}, this method is redundant with * {@code writeSingleLock(...)} and may unconditionally return true. * * @param lockID identifies the lock to check * @param lockStatus the result of a prior successful {@code writeSingleLock(...)} * call on this {@code lockID} and {@code tx} * @param tx identifies the process claiming this lock * @throws Throwable if the lock fails the check or if the attempted check * encountered an error */ protected abstract void checkSingleLock(KeyColumn lockID, S lockStatus, StoreTransaction tx) throws Throwable; /** * Try to unlock/release/delete the lock identified by {@code lockID} and * both held by and verified for {@code tx}. This method is only called with * arguments for which {@link #writeSingleLock(KeyColumn, StoreTransaction)} * and {@link #checkSingleLock(KeyColumn, LockStatus, StoreTransaction)} * both returned successfully (i.e. without exceptions). * * @param lockID identifies the lock to release * @param lockStatus the result of a prior successful {@code writeSingleLock(...)} * followed by a successful {@code checkSingleLock(...)} * @param tx identifies the process that wrote and checked this lock * @throws Throwable if the lock could not be released/deleted or if the attempted * delete encountered an error */ protected abstract void deleteSingleLock(KeyColumn lockID, S lockStatus, StoreTransaction tx) throws Throwable; @Override public void writeLock(KeyColumn lockID, StoreTransaction tx) throws TemporaryLockingException, PermanentLockingException { if (null != tx.getConfiguration().getGroupName()) { MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_WRITE, M_CALLS).inc(); } if (lockState.has(tx, lockID)) { log.debug("Transaction {} already wrote lock on {}", tx, lockID); return; } if (lockLocally(lockID, tx)) { boolean ok = false; try { S stat = writeSingleLock(lockID, tx); lockLocally(lockID, stat.getExpirationTimestamp(), tx); // update local lock expiration time lockState.take(tx, lockID, stat); ok = true; } catch (TemporaryBackendException tse) { throw new TemporaryLockingException(tse); } catch (AssertionError ae) { // Concession to ease testing with mocks & behavior verification ok = true; throw ae; } catch (Throwable t) { throw new PermanentLockingException(t); } finally { if (!ok) { // lockState.release(tx, lockID); // has no effect unlockLocally(lockID, tx); if (null != tx.getConfiguration().getGroupName()) { MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_WRITE, M_EXCEPTIONS).inc(); } } } } else { // Fail immediately with no retries on local contention throw new PermanentLockingException("Local lock contention"); } } @Override public void checkLocks(StoreTransaction tx) throws TemporaryLockingException, PermanentLockingException { if (null != tx.getConfiguration().getGroupName()) { MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_CHECK, M_CALLS).inc(); } Map<KeyColumn, S> m = lockState.getLocksForTx(tx); if (m.isEmpty()) { return; // no locks for this tx } // We never receive interrupts in normal operation; one can only appear // during Thread.sleep(), and in that case it probably means the entire // JanusGraph process is shutting down; for this reason, we return ASAP on an // interrupt boolean ok = false; try { for (KeyColumn kc : m.keySet()) { checkSingleLock(kc, m.get(kc), tx); } ok = true; } catch (TemporaryLockingException tle) { throw tle; } catch (PermanentLockingException ple) { throw ple; } catch (AssertionError ae) { throw ae; // Concession to ease testing with mocks & behavior verification } catch (InterruptedException e) { throw new TemporaryLockingException(e); } catch (TemporaryBackendException tse) { throw new TemporaryLockingException(tse); } catch (Throwable t) { throw new PermanentLockingException(t); } finally { if (!ok && null != tx.getConfiguration().getGroupName()) { MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_CHECK, M_CALLS).inc(); } } } @Override public void deleteLocks(StoreTransaction tx) throws TemporaryLockingException, PermanentLockingException { if (null != tx.getConfiguration().getGroupName()) { MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_DELETE, M_CALLS).inc(); } Map<KeyColumn, S> m = lockState.getLocksForTx(tx); Iterator<KeyColumn> iter = m.keySet().iterator(); while (iter.hasNext()) { KeyColumn kc = iter.next(); S ls = m.get(kc); try { deleteSingleLock(kc, ls, tx); } catch (AssertionError ae) { throw ae; // Concession to ease testing with mocks & behavior verification } catch (Throwable t) { log.error("Exception while deleting lock on " + kc, t); if (null != tx.getConfiguration().getGroupName()) { MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_DELETE, M_CALLS).inc(); } } // Regardless of whether we successfully deleted the lock from storage, take it out of the local mediator llm.unlock(kc, tx); iter.remove(); } } private boolean lockLocally(KeyColumn lockID, StoreTransaction tx) { return lockLocally(lockID, times.getTime().plus(lockExpire), tx); } private boolean lockLocally(KeyColumn lockID, Instant expire, StoreTransaction tx) { return llm.lock(lockID, tx, expire); } private void unlockLocally(KeyColumn lockID, StoreTransaction txh) { llm.unlock(lockID, txh); } }