org.alfresco.repo.lock.JobLockServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.lock.JobLockServiceImpl.java

Source

/*
 * #%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);
                }
            }
        }
    }
}