Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.lock; import java.util.TreeSet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.alfresco.repo.domain.locks.LockDAO; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.TransactionalResourceHelper; import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; import org.alfresco.util.TraceableThreadFactory; import org.alfresco.util.VmShutdownListener; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author Derek Hulley * @since 3.2 */ public class JobLockServiceImpl implements JobLockService { private static final String KEY_RESOURCE_LOCKS = "JobLockServiceImpl.Locks"; private static Log logger = LogFactory.getLog(JobLockServiceImpl.class); private LockDAO lockDAO; private RetryingTransactionHelper retryingTransactionHelper; private int defaultRetryCount; private long defaultRetryWait; private ScheduledExecutorService scheduler; private VmShutdownListener shutdownListener; /** * Stateless listener that does post-transaction cleanup. */ private final LockTransactionListener txnListener; public JobLockServiceImpl() { defaultRetryWait = 20; defaultRetryCount = 10; txnListener = new LockTransactionListener(); TraceableThreadFactory threadFactory = new TraceableThreadFactory(); threadFactory.setThreadDaemon(false); threadFactory.setNamePrefix("JobLockService"); scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory); shutdownListener = new VmShutdownListener("JobLockService"); } /** * Lifecycle method. This method should be called when the JobLockService * is no longer required allowing proper clean up before disposing of the object. * <p> * This is mostly used to tell the thread pool to shut itself down * so as to allow the JVM to terminate. */ public void shutdown() { if (logger.isInfoEnabled()) { logger.info("shutting down."); } // If we don't tell the thread pool to shutdown, then the JVM won't shutdown. scheduler.shutdown(); } /** * Set the lock DAO */ public void setLockDAO(LockDAO lockDAO) { this.lockDAO = lockDAO; } /** * Set the helper that will handle low-level concurrency conditions i.e. that * enforces optimistic locking and deals with stale state issues. */ public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) { this.retryingTransactionHelper = retryingTransactionHelper; } /** * Set the maximum number of attempts to make at getting a lock * @param defaultRetryCount the number of attempts */ public void setDefaultRetryCount(int defaultRetryCount) { this.defaultRetryCount = defaultRetryCount; } /** * Set the default time to wait between attempts to acquire a lock * @param defaultRetryWait the wait time in milliseconds */ public void setDefaultRetryWait(long defaultRetryWait) { this.defaultRetryWait = defaultRetryWait; } /** * {@inheritDoc} */ @Override public void getTransactionalLock(QName lockQName, long timeToLive) { getTransactionalLock(lockQName, timeToLive, defaultRetryWait, defaultRetryCount); } /** * {@inheritDoc} */ @Override public void getTransactionalLock(QName lockQName, long timeToLive, long retryWait, int retryCount) { // Check that transaction is present final String txnId = AlfrescoTransactionSupport.getTransactionId(); if (txnId == null) { throw new IllegalStateException("Locking requires an active transaction"); } // Get the set of currently-held locks TreeSet<QName> heldLocks = TransactionalResourceHelper.getTreeSet(KEY_RESOURCE_LOCKS); // We don't want the lock registered as being held if something goes wrong TreeSet<QName> heldLocksTemp = new TreeSet<QName>(heldLocks); boolean added = heldLocksTemp.add(lockQName); if (!added) { // It's a refresh. Ordering is not important here as we already hold the lock. refreshLock(txnId, lockQName, timeToLive); } else { QName lastLock = heldLocksTemp.last(); if (lastLock.equals(lockQName)) { if (logger.isDebugEnabled()) { logger.debug("Attempting to acquire ordered lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + txnId); } // If it was last in the set, then the order is correct and we use the // full retry behaviour. getLockImpl(txnId, lockQName, timeToLive, retryWait, retryCount); } else { if (logger.isDebugEnabled()) { logger.debug("Attempting to acquire UNORDERED lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + txnId); } // The lock request is made out of natural order. // Unordered locks do not get any retry behaviour getLockImpl(txnId, lockQName, timeToLive, retryWait, 1); } } // It went in, so add it to the transactionally-stored set heldLocks.add(lockQName); // Done } /** * {@inheritDoc} * * @see #getLock(QName, long, long, int) */ @Override public String getLock(QName lockQName, long timeToLive) { return getLock(lockQName, timeToLive, defaultRetryWait, defaultRetryCount); } /** * {@inheritDoc} */ @Override public String getLock(QName lockQName, long timeToLive, long retryWait, int retryCount) { String lockToken = GUID.generate(); getLockImpl(lockToken, lockQName, timeToLive, retryWait, retryCount); // Done return lockToken; } /** * {@inheritDoc} * * @throws LockAcquisitionException on failure */ @Override public void refreshLock(final String lockToken, final QName lockQName, final long timeToLive) { RetryingTransactionCallback<Object> refreshLockCallback = new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { lockDAO.refreshLock(lockQName, lockToken, timeToLive); return null; } }; try { // It must succeed retryingTransactionHelper.doInTransaction(refreshLockCallback, false, true); // Success if (logger.isDebugEnabled()) { logger.debug("Refreshed Lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken); } } catch (LockAcquisitionException e) { // Failure if (logger.isDebugEnabled()) { logger.debug("Lock refresh failed: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken + "\n" + " Error: " + e.getMessage()); } throw e; } } /** * {@inheritDoc} */ @Override public String getLock(QName lockQName, long timeToLive, JobLockRefreshCallback callback) { if (lockQName == null) throw new IllegalArgumentException("lock name null"); if (callback == null) throw new IllegalArgumentException("callback null"); String lockToken = getLock(lockQName, timeToLive); try { refreshLock(lockToken, lockQName, timeToLive, callback); return lockToken; } catch (IllegalArgumentException | LockAcquisitionException e) { this.releaseLockVerify(lockToken, lockQName); throw e; } } /** * {@inheritDoc} */ @Override public void refreshLock(final String lockToken, final QName lockQName, final long timeToLive, final JobLockRefreshCallback callback) { // Do nothing if the scheduler has shut down if (scheduler.isShutdown() || scheduler.isTerminated()) { if (logger.isDebugEnabled()) { logger.debug("Lock refresh failed: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken + "\n" + " Error: " + "Lock refresh scheduler has shut down. The VM may be terminating."); } // Don't schedule throw new LockAcquisitionException(lockQName, lockToken); } final long delay = timeToLive / 2; if (delay < 1) { throw new IllegalArgumentException("Very small timeToLive: " + timeToLive); } // Our runnable does the callbacks Runnable runnable = new Runnable() { @Override public void run() { // Most lock debug is done elsewhere; just note that this is a timed process. if (logger.isDebugEnabled()) { logger.debug("Initiating timed Lock refresh: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken); } // First check the VM if (shutdownListener.isVmShuttingDown()) { callLockReleased(callback); return; } boolean isActive = false; try { isActive = callIsActive(callback, delay); } catch (Throwable e) { logger.error("Lock isActive check failed: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken, e); // The callback must be informed callLockReleased(callback); return; } if (!isActive) { // Debug if (logger.isDebugEnabled()) { logger.debug("Lock callback is inactive. Releasing lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken); } // The callback is no longer active, so we don't need to refresh. // Release the lock in case the initiator did not do it. // We just want to release and don't care if the lock was already released // or taken by another process if (releaseLockVerify(lockToken, lockQName)) { // The callback must be informed as we released the lock automatically callLockReleased(callback); } } else { try { refreshLock(lockToken, lockQName, timeToLive); // Success. The callback does not need to know. // NB: Reschedule this task scheduler.schedule(this, delay, TimeUnit.MILLISECONDS); } catch (LockAcquisitionException e) { // The callback must be informed callLockReleased(callback); } } } }; // Schedule this scheduler.schedule(runnable, delay, TimeUnit.MILLISECONDS); } /** * Calls the callback {@link JobLockRefreshCallback#isActive() isActive} with time-check. */ private boolean callIsActive(JobLockRefreshCallback callback, long delay) throws Throwable { long t1 = System.nanoTime(); boolean isActive = callback.isActive(); long t2 = System.nanoTime(); double timeWastedMs = (double) (t2 - t1) / (double) 10E6; if (timeWastedMs > delay || timeWastedMs > 1000L) { // The isActive did not come back quickly enough. There is no point taking longer than // the delay, but in any case 1s to provide a boolean is too much. This is probably an // indication that the isActive implementation is performing complex state determination, // which is specifically referenced in the API doc. throw new RuntimeException("isActive check took " + timeWastedMs + "ms to return, which is too long."); } return isActive; } /** * Calls the callback {@link JobLockRefreshCallback#lockReleased()}. */ private void callLockReleased(JobLockRefreshCallback callback) { try { callback.lockReleased(); } catch (Throwable ee) { logger.error("Callback to lockReleased failed.", ee); } } /** * {@inheritDoc} */ @Override public void releaseLock(final String lockToken, final QName lockQName) { RetryingTransactionCallback<Boolean> releaseCallback = new RetryingTransactionCallback<Boolean>() { public Boolean execute() throws Throwable { return lockDAO.releaseLock(lockQName, lockToken, false); } }; retryingTransactionHelper.doInTransaction(releaseCallback, false, true); } /** * {@inheritDoc} */ public boolean releaseLockVerify(final String lockToken, final QName lockQName) { RetryingTransactionCallback<Boolean> releaseCallback = new RetryingTransactionCallback<Boolean>() { public Boolean execute() throws Throwable { return lockDAO.releaseLock(lockQName, lockToken, true); } }; return retryingTransactionHelper.doInTransaction(releaseCallback, false, true); } /** * @throws LockAcquisitionException on failure */ private void getLockImpl(final String lockToken, final QName lockQName, final long timeToLive, long retryWait, int retryCount) { if (retryCount < 0) { throw new IllegalArgumentException("Job lock retry count cannot be negative: " + retryCount); } RetryingTransactionCallback<Object> getLockCallback = new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { lockDAO.getLock(lockQName, lockToken, timeToLive); return null; } }; try { int iterations = doWithRetry(getLockCallback, retryWait, retryCount); // Bind in a listener, if we are in a transaction if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE) { AlfrescoTransactionSupport.bindListener(txnListener); } // Success if (logger.isDebugEnabled()) { logger.debug("Acquired Lock: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken + "\n" + " Attempts: " + iterations); } } catch (LockAcquisitionException e) { // Failure if (logger.isDebugEnabled()) { logger.debug("Lock acquisition failed: \n" + " Lock: " + lockQName + "\n" + " TTL: " + timeToLive + "\n" + " Txn: " + lockToken + "\n" + " Error: " + e.getMessage()); } throw e; } } /** * Does the high-level retrying around the callback. At least one attempt is made to call the * provided callback. */ private int doWithRetry(RetryingTransactionCallback<? extends Object> callback, long retryWait, int retryCount) { int maxAttempts = retryCount > 0 ? retryCount : 1; int lockAttempt = 0; LockAcquisitionException lastException = null; while (++lockAttempt <= maxAttempts) // lockAttempt incremented before check i.e. 1 for first check { try { retryingTransactionHelper.doInTransaction(callback, false, true); // Success. Clear the exception indicator! lastException = null; break; } catch (LockAcquisitionException e) { if (logger.isDebugEnabled()) { logger.debug( "Lock attempt " + lockAttempt + " of " + maxAttempts + " failed: " + e.getMessage()); } lastException = e; if (lockAttempt >= maxAttempts) { // Avoid an unnecessary wait if this is the last attempt break; } } // Before running again, do a wait synchronized (callback) { try { callback.wait(retryWait); } catch (InterruptedException e) { } } } if (lastException == null) { // Success return lockAttempt; } else { // Failure throw lastException; } } /** * Handles the transction synchronization activity, ensuring locks are rolled back as * required. * * @author Derek Hulley * @since 3.2 */ private class LockTransactionListener extends TransactionListenerAdapter { /** * Release any open locks with extreme prejudice i.e. the commit will fail if the * locks cannot be released. The locks are released in a single transaction - * ordering is therefore not important. Should this fail, the post-commit phase * will do a final cleanup with individual locks. */ @Override public void beforeCommit(boolean readOnly) { final String txnId = AlfrescoTransactionSupport.getTransactionId(); final TreeSet<QName> heldLocks = TransactionalResourceHelper.getTreeSet(KEY_RESOURCE_LOCKS); // Shortcut if there are no locks if (heldLocks.size() == 0) { return; } // Clean up the locks RetryingTransactionCallback<Object> releaseCallback = new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { // Any one of the them could fail for (QName lockQName : heldLocks) { lockDAO.releaseLock(lockQName, txnId, false); } return null; } }; retryingTransactionHelper.doInTransaction(releaseCallback, false, true); // So they were all successful heldLocks.clear(); } /** * This will be called if something went wrong. It might have been the lock releases, but * it could be anything else as well. Each remaining lock is released with warnings where * it fails. */ @Override public void afterRollback() { final String txnId = AlfrescoTransactionSupport.getTransactionId(); final TreeSet<QName> heldLocks = TransactionalResourceHelper.getTreeSet(KEY_RESOURCE_LOCKS); // Shortcut if there are no locks if (heldLocks.size() == 0) { return; } // Clean up any remaining locks for (final QName lockQName : heldLocks) { RetryingTransactionCallback<Object> releaseCallback = new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { lockDAO.releaseLock(lockQName, txnId, false); return null; } }; try { retryingTransactionHelper.doInTransaction(releaseCallback, false, true); } catch (Throwable e) { // There is no point propagating this, so just log a warning and // hope that it expires soon enough logger.warn("Failed to release a lock in 'afterRollback':\n" + " Lock Name: " + lockQName + "\n" + " Lock Token: " + txnId, e); } } } } }