/* * ModeShape (http://www.modeshape.org) * * 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.modeshape.jcr.locking; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import org.modeshape.common.annotation.ThreadSafe; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.CheckArg; /** * Base class for {@link LockingService} implementations. * * @author Horia Chiorean (hchiorea@redhat.com) * @since 5.0 */ @ThreadSafe public abstract class AbstractLockingService<T extends Lock> implements LockingService { protected final Logger logger = Logger.getLogger(getClass()); private final ConcurrentHashMap<String, T> locksByName = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> locksByWaiters = new ConcurrentHashMap<>(); private final AtomicBoolean running = new AtomicBoolean(false); private final long lockTimeoutMillis; protected AbstractLockingService() { this(0); } protected AbstractLockingService(long lockTimeoutMillis) { CheckArg.isNonNegative(lockTimeoutMillis, "lockTimeoutMillis"); this.lockTimeoutMillis = lockTimeoutMillis; this.running.compareAndSet(false, true); } @Override public boolean tryLock(String... names) throws InterruptedException { return tryLock(lockTimeoutMillis, TimeUnit.MILLISECONDS, names); } @Override public boolean tryLock(long time, TimeUnit unit, String... names) throws InterruptedException { if (!running.get()) { throw new IllegalStateException("Service has been shut down"); } Set<String> successfullyLocked = new LinkedHashSet<>(); for (String name : names) { logger.debug("attempting to lock {0}", name); locksByWaiters.computeIfAbsent(name, lockName -> new AtomicInteger(0)).incrementAndGet(); boolean success = false; T lock = null; try { lock = locksByName.computeIfAbsent(name, this::createLock); success = doLock(lock, time, unit); } catch (Exception e) { logger.debug(e, "unexpected exception while attempting to lock '{0}'", name); } // if lock acquisition was not successful (for whatever reason) revert the lock if (!success) { // decrement the waiter value for the lock we failed to get locksByWaiters.computeIfPresent(name, (lockName, atomicInteger) -> { if (atomicInteger.decrementAndGet() == 0) { // we couldn't get the lock, but no one is using it, meaning it just became unlocked so just remove it locksByName.computeIfPresent(name, (internalLockName, internalLock) -> null); return null; } return atomicInteger; }); if (!successfullyLocked.isEmpty()) { logger.debug("Unable to acquire lock on {0}. Reverting back the already obtained locks: {1}", name, successfullyLocked); // and unlock all the rest of the locks... unlock(successfullyLocked.toArray(new String[0])); } return false; } logger.debug("{0} locked successfully (ref {1})", name, lock); successfullyLocked.add(name); } return true; } @Override public boolean unlock(String... names) { if (!running.get()) { throw new IllegalStateException("Service has been shut down"); } return Arrays.stream(names) .map(this::unlock) .allMatch(Boolean::booleanValue); } protected boolean unlock(String name) { logger.debug("attempting to unlock {0}", name); AtomicBoolean unlocked = new AtomicBoolean(false); locksByName.computeIfPresent(name, (key, lock) -> { AtomicInteger waiters = locksByWaiters.computeIfPresent(name, (lockName, atomicInteger) -> { if (releaseLock(lock)) { logger.debug("{0} unlocked (ref {1})...", name, lock); unlocked.compareAndSet(false, true); if (atomicInteger.decrementAndGet() > 0) { // there are still threads waiting on this, so don't remove it... return atomicInteger; } else { // no one waiting on the this lock anymore return null; } } // the lock was not unlocked logger.debug("{0} failed to unlock (ref {1})...", name, lock); return atomicInteger; }); if (waiters != null) { logger.debug("lock '{0}' is not currently locked but will be", name); return lock; } else { // The lock is not used, and returning null will remove it from the map ... logger.debug("lock '{0}' not used anymore; removing it from map", name); return null; } }); return unlocked.get(); } @Override public synchronized boolean shutdown() { if (!running.get()) { return false; } logger.debug("Shutting down locking service..."); doShutdown(); locksByName.clear(); running.compareAndSet(true, false); return true; } protected void doShutdown() { locksByName.forEach((key, lock) -> { if (releaseLock(lock)) { logger.debug("{0} unlocked successfully ", key); } else { logger.debug("{0} cannot be released...", key); } }); } protected boolean doLock(T lock, long time, TimeUnit timeUnit) throws InterruptedException { return time > 0 ? lock.tryLock(time, timeUnit) : lock.tryLock(); } protected abstract T createLock(String name); protected abstract boolean releaseLock(T lock); }