/* * Copyright 2012-2013 Aurelius LLC * 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 com.thinkaurelius.titan.diskstorage.locking; import com.google.common.base.Preconditions; import com.thinkaurelius.titan.diskstorage.util.time.Timepoint; import com.thinkaurelius.titan.diskstorage.util.time.TimestampProvider; import com.thinkaurelius.titan.diskstorage.locking.consistentkey.ExpectedValueCheckingTransaction; import com.thinkaurelius.titan.diskstorage.util.KeyColumn; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; /** * This class resolves lock contention between two transactions on the same JVM. * <p/> * This is not just an optimization to reduce network traffic. Locks written by * Titan to a distributed key-value store contain an identifier, the "Rid", * which is unique only to the process level. The Rid can't tell which * transaction in a process holds any given lock. This class prevents two * transactions in a single process from concurrently writing the same lock to a * distributed key-value store. * * @author Dan LaRocque <dalaro@hopcount.org> */ public class LocalLockMediator<T> { private static final Logger log = LoggerFactory .getLogger(LocalLockMediator.class); /** * Namespace for which this mediator is responsible * * @see LocalLockMediatorProvider */ private final String name; private final TimestampProvider times; private DelayQueue<ExpirableKeyColumn> expiryQueue = new DelayQueue<>(); private ExecutorService lockCleanerService = Executors.newFixedThreadPool(1, new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { Thread thread = Executors.defaultThreadFactory().newThread(runnable); thread.setDaemon(true); return thread; } }); /** * Maps a ({@code key}, {@code column}) pair to the local transaction * holding a lock on that pair. Values in this map may have already expired * according to {@link AuditRecord#expires}, in which case the lock should * be considered invalid. */ private final ConcurrentHashMap<KeyColumn, AuditRecord<T>> locks = new ConcurrentHashMap<KeyColumn, AuditRecord<T>>(); public LocalLockMediator(String name, TimestampProvider times) { this.name = name; this.times = times; Preconditions.checkNotNull(name); Preconditions.checkNotNull(times); lockCleanerService.submit(new LockCleaner()); } /** * Acquire the lock specified by {@code kc}. * <p/> * <p/> * For any particular key-column, whatever value of {@code requestor} is * passed to this method must also be passed to the associated later call to * {@link #unlock(KeyColumn, ExpectedValueCheckingTransaction)}. * <p/> * If some requestor {@code r} calls this method on a KeyColumn {@code k} * and this method returns true, then subsequent calls to this method by * {@code r} on {@code l} merely attempt to update the {@code expiresAt} * timestamp. This differs from typical lock reentrance: multiple successful * calls to this method do not require an equal number of calls to * {@code #unlock()}. One {@code #unlock()} call is enough, no matter how * many times a {@code requestor} called {@code lock} beforehand. Note that * updating the timestamp may fail, in which case the lock is considered to * have expired and the calling context should assume it no longer holds the * lock specified by {@code kc}. * <p/> * The number of nanoseconds elapsed since the UNIX Epoch is not readily * available within the JVM. When reckoning expiration times, this method * uses the approximation implemented by * {@link com.thinkaurelius.titan.diskstorage.util.NanoTime#getApproxNSSinceEpoch(false)}. * <p/> * The current implementation of this method returns true when given an * {@code expiresAt} argument in the past. Future implementations may return * false instead. * * @param kc lock identifier * @param requestor the object locking {@code kc} * @param expires instant at which this lock will automatically expire * @return true if the lock is acquired, false if it was not acquired */ public boolean lock(KeyColumn kc, T requestor, Timepoint expires) { assert null != kc; assert null != requestor; AuditRecord<T> audit = new AuditRecord<T>(requestor, expires); AuditRecord<T> inmap = locks.putIfAbsent(kc, audit); boolean success = false; if (null == inmap) { // Uncontended lock succeeded if (log.isTraceEnabled()) { log.trace("New local lock created: {} namespace={} txn={}", new Object[]{kc, name, requestor}); } success = true; } else if (inmap.equals(audit)) { // requestor has already locked kc; update expiresAt success = locks.replace(kc, inmap, audit); if (log.isTraceEnabled()) { if (success) { log.trace( "Updated local lock expiration: {} namespace={} txn={} oldexp={} newexp={}", new Object[]{kc, name, requestor, inmap.expires, audit.expires}); } else { log.trace( "Failed to update local lock expiration: {} namespace={} txn={} oldexp={} newexp={}", new Object[]{kc, name, requestor, inmap.expires, audit.expires}); } } } else if (0 > inmap.expires.compareTo(times.getTime())) { // the recorded lock has expired; replace it success = locks.replace(kc, inmap, audit); if (log.isTraceEnabled()) { log.trace( "Discarding expired lock: {} namespace={} txn={} expired={}", new Object[]{kc, name, inmap.holder, inmap.expires}); } } else { // we lost to a valid lock if (log.isTraceEnabled()) { log.trace( "Local lock failed: {} namespace={} txn={} (already owned by {})", new Object[]{kc, name, requestor, inmap}); } } if (success) { expiryQueue.add(new ExpirableKeyColumn(kc, expires)); } return success; } /** * Release the lock specified by {@code kc} and which was previously * locked by {@code requestor}, if it is possible to release it. * * @param kc lock identifier * @param requestor the object which previously locked {@code kc} */ public boolean unlock(KeyColumn kc, T requestor) { if (!locks.containsKey(kc)) { log.info("Local unlock failed: no locks found for {}", kc); return false; } AuditRecord<T> unlocker = new AuditRecord<T>(requestor, null); AuditRecord<T> holder = locks.get(kc); if (!holder.equals(unlocker)) { log.error("Local unlock of {} by {} failed: it is held by {}", new Object[]{kc, unlocker, holder}); return false; } boolean removed = locks.remove(kc, unlocker); if (removed) { expiryQueue.remove(kc); if (log.isTraceEnabled()) { log.trace("Local unlock succeeded: {} namespace={} txn={}", new Object[]{kc, name, requestor}); } } else { log.warn("Local unlock warning: lock record for {} disappeared " + "during removal; this suggests the lock either expired " + "while we were removing it, or that it was erroneously " + "unlocked multiple times.", kc); } // Even if !removed, we're finished unlocking, so return true return true; } public String toString() { return "LocalLockMediator [" + name + ", ~" + locks.size() + " current locks]"; } /** * A record containing the local transaction that holds a lock and the * lock's expiration time. */ private static class AuditRecord<T> { /** * The local transaction that holds/held the lock. */ private final T holder; /** * The expiration time of a the lock. */ private final Timepoint expires; /** * Cached hashCode. */ private int hashCode; private AuditRecord(T holder, Timepoint expires) { this.holder = holder; this.expires = expires; } /** * This implementation depends only on the lock holder and not on the * lock expiration time. */ @Override public int hashCode() { if (0 == hashCode) hashCode = holder.hashCode(); return hashCode; } /** * This implementation depends only on the lock holder and not on the * lock expiration time. */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; /* * This warning suppression is harmless because we are only going to * call other.holder.equals(...), and since equals(...) is part of * Object, it is guaranteed to be defined no matter the concrete * type of parameter T. */ @SuppressWarnings("rawtypes") AuditRecord other = (AuditRecord) obj; if (holder == null) { if (other.holder != null) return false; } else if (!holder.equals(other.holder)) return false; return true; } @Override public String toString() { return "AuditRecord [txn=" + holder + ", expires=" + expires + "]"; } } private class LockCleaner implements Runnable { @Override public void run() { try { while (true) { log.debug("Lock Cleaner service started"); ExpirableKeyColumn lock = expiryQueue.take(); log.debug("Expiring key column " + lock.getKeyColumn()); locks.remove(lock.getKeyColumn()); } } catch (InterruptedException e) { log.debug("Received interrupt. Exiting"); } } } private static class ExpirableKeyColumn implements Delayed { private Timepoint expiryTime; private KeyColumn kc; public ExpirableKeyColumn(KeyColumn keyColumn, Timepoint expiryTime) { this.kc = keyColumn; this.expiryTime = expiryTime; } @Override public long getDelay(TimeUnit unit) { return expiryTime.getTimestamp(TimeUnit.NANOSECONDS); } @Override public int compareTo(Delayed o) { if (this.expiryTime.getTimestamp(TimeUnit.NANOSECONDS) < ((ExpirableKeyColumn) o).expiryTime.getTimestamp(TimeUnit.NANOSECONDS)) { return -1; } if (this.expiryTime.getTimestamp(TimeUnit.NANOSECONDS) > ((ExpirableKeyColumn) o).expiryTime.getTimestamp(TimeUnit.NANOSECONDS)) { return 1; } return 0; } public KeyColumn getKeyColumn() { return kc; } } }