/* * Copyright 2014 the original author or 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.gradle.cache.internal; import org.gradle.api.Action; import org.gradle.cache.internal.filelock.LockOptions; import org.gradle.internal.Factory; import org.gradle.internal.UncheckedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.concurrent.locks.Lock; class LockOnDemandCrossProcessCacheAccess extends AbstractCrossProcessCacheAccess { private static final Logger LOGGER = LoggerFactory.getLogger(LockOnDemandCrossProcessCacheAccess.class); private final String cacheDisplayName; private final File lockTarget; private final LockOptions lockOptions; private final FileLockManager lockManager; private final Lock stateLock; private final Action<FileLock> onOpen; private final Action<FileLock> onClose; private final Runnable unlocker; private final Runnable whenContended; private int lockCount; private FileLock fileLock; private CacheInitializationAction initAction; private boolean contended; /** * Actions are notified when lock is opened or closed. Actions are called while holding state lock, so that no other threads are working with cache while these are running. * * @param stateLock Lock to hold while mutating state. * @param onOpen Action to run when the lock is opened. Action is called while holding state lock * @param onClose Action to run when the lock is closed. Action is called while holding state lock */ public LockOnDemandCrossProcessCacheAccess(String cacheDisplayName, File lockTarget, LockOptions lockOptions, FileLockManager lockManager, Lock stateLock, CacheInitializationAction initAction, Action<FileLock> onOpen, Action<FileLock> onClose) { this.cacheDisplayName = cacheDisplayName; this.lockTarget = lockTarget; this.lockOptions = lockOptions; this.lockManager = lockManager; this.stateLock = stateLock; this.initAction = initAction; this.onOpen = onOpen; this.onClose = onClose; unlocker = new UnlockAction(); whenContended = new ContendedAction(); } @Override public void open() { // Don't need to do anything } @Override public void close() { stateLock.lock(); try { if (lockCount != 0) { throw new IllegalStateException(String.format("Cannot close cache access for %s as it is currently in use for %s operations.", cacheDisplayName, lockCount)); } releaseLockIfHeld(); } finally { stateLock.unlock(); } } @Override public <T> T withFileLock(Factory<T> factory) { incrementLockCount(); try { return factory.create(); } finally { decrementLockCount(); } } private void incrementLockCount() { stateLock.lock(); try { if (fileLock == null) { if (lockCount != 0) { throw new IllegalStateException("Mismatched lock count."); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Acquiring file lock for {}", cacheDisplayName); } fileLock = lockManager.lock(lockTarget, lockOptions, cacheDisplayName); try { if (initAction.requiresInitialization(fileLock)) { fileLock.writeFile(new Runnable() { @Override public void run() { initAction.initialize(fileLock); } }); } onOpen.execute(fileLock); lockManager.allowContention(fileLock, whenContended); } catch (Exception e) { fileLock.close(); fileLock = null; throw UncheckedException.throwAsUncheckedException(e); } } lockCount++; } finally { stateLock.unlock(); } } private void decrementLockCount() { stateLock.lock(); try { if (lockCount <= 0 || fileLock == null) { throw new IllegalStateException("Mismatched lock count."); } lockCount--; if (lockCount == 0 && contended) { releaseLockIfHeld(); } // otherwise, keep lock open } finally { stateLock.unlock(); } } private void releaseLockIfHeld() { if (fileLock == null) { return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Releasing file lock for {}", cacheDisplayName); } try { onClose.execute(fileLock); } finally { fileLock.close(); fileLock = null; contended = false; } } @Override public Runnable acquireFileLock() { incrementLockCount(); return unlocker; } private class ContendedAction implements Runnable { @Override public void run() { stateLock.lock(); try { if (lockCount == 0) { LOGGER.debug("Lock on {} requested by another process - releasing lock.", cacheDisplayName); releaseLockIfHeld(); } else { // Lock is in use - mark as contended LOGGER.debug("Lock on {} requested by another process - lock is in use and will be released when operation completed.", cacheDisplayName); contended = true; } } finally { stateLock.unlock(); } } } private class UnlockAction implements Runnable { @Override public void run() { decrementLockCount(); } } }