/* * Copyright (c) 2013-2017 Cinchapi Inc. * * 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.cinchapi.concourse.server.concurrent; import java.util.ConcurrentModificationException; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import com.cinchapi.concourse.util.Logger; import com.google.common.base.MoreObjects; import com.google.common.collect.Sets; import com.google.common.util.concurrent.ThreadFactoryBuilder; /** * Internally, Concourse uses various lock services to control concurrent * access to resources. In order to ensure high throughput, each of the lock * services provide dynamic locks for granular notions of things (i.e. records, * keys in records, ranges, * keys, etc). * <p> * This base class implements the bulk of the logic for concurrently dealing * with the locks in a secure way. * </p> * * @author Jeff Nelson */ public abstract class AbstractLockService<T extends Token, L extends ReferenceCountingLock> { // --- Global GC State /** * The amount of time to wait between GC cycles. */ private static int GC_DELAY = 1000; /** * A collection of services that are GC eligible. */ private static Set<AbstractLockService<?, ?>> services = Sets.newHashSet(); /** * The service that is responsible for carrying out garbage collection for * all the lock services. */ private static final ScheduledExecutorService gc = Executors .newScheduledThreadPool(1, new ThreadFactoryBuilder() .setNameFormat("Lock Service GC").setDaemon(true).build()); static { gc.scheduleWithFixedDelay(new GarbageCollector(), GC_DELAY, GC_DELAY, TimeUnit.MILLISECONDS); } /** * A cache of locks that have been requested, each of which is mapped from a * corresponding {@link Token}. This cache is periodically cleaned (e.g. * stale locks are removed) up using a protocol defined in the subclass. */ protected final ConcurrentMap<T, L> locks; /** * Construct a new NOOP instance. */ protected AbstractLockService() { this.locks = null; } /** * Construct a new instance. * * @param locks */ protected AbstractLockService(ConcurrentMap<T, L> locks) { this.locks = locks; services.add(this); } /** * Return the ReadLock that is identified by {@code token}. Every caller * requesting a lock for {@code token} is guaranteed to get the same * instance if the lock is currently held by a reader of a writer. * * @param token * @return the ReadLock */ public ReadLock getReadLock(T token) { return (ReadLock) getLock(token, true); } /** * Return the WriteLock that is identified by {@code token}. Every caller * requesting a lock for {@code token} is guaranteed to get the same * instance if the lock is currently held by a reader of a writer. * * @param token * @return the WriteLock */ public WriteLock getWriteLock(T token) { return (WriteLock) getLock(token, false); } /** * Shutdown the lock service. */ public void shutdown() { services.remove(this); } /** * Retrieve the lock that corresponds to {@code token} with the option to * return a shared (read) view or an exclusive (write) one. * <p> * This method will handle race conditions where another thread manages to * add a new lock for {@code token} into the cache while this operation is * executing. This method will also handle race conditions with the internal * garbage collection to ensure that any lock that is returned is indeed the * canonical one in the cache and safe from garbage collection during the * duration of its existence. * </p> * * @param token * @param readLock * @return a lock view of the canonical lock for {@code token} */ private Lock getLock(T token, boolean readLock) { L existing = locks.get(token); if(existing == null) { L created = createLock(token); existing = locks.putIfAbsent(token, created); existing = MoreObjects.firstNonNull(existing, created); } existing.refs.incrementAndGet(); L gced = null; if(existing.refs.get() <= 0 || (gced = locks.putIfAbsent(token, existing)) != existing) { // Indicates // that // the // existing // lock // was // garbage // collected existing.refs.decrementAndGet(); Logger.debug("Lock Service GC Race Condition: Expected " + "{} but was {}", existing, gced); Thread.yield(); return getLock(token, readLock); } else { return readLock ? existing.readLock() : existing.writeLock(); } } /** * Return a new {@code lock} that is associated with {@code token}. * * @param token * @return the new lock */ protected abstract L createLock(T token); /** * The internal garbage collector is a task that periodically checks the * {@link #locks} cache for entries with 0 references and removes them. * * @author Jeff Nelson */ private static class GarbageCollector implements Runnable { @Override public void run() { try { for (AbstractLockService<?, ?> service : services) { for (Object token : service.locks.keySet()) { ReferenceCountingLock lock = service.locks.get(token); if(lock.refs.compareAndSet(0, Integer.MIN_VALUE)) { service.locks.remove(token, lock); } } } } catch (ConcurrentModificationException e) { return; } } } }