diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml
index 8b17960576..d67aedf0b9 100644
--- a/config/alfresco/core-services-context.xml
+++ b/config/alfresco/core-services-context.xml
@@ -1256,5 +1256,31 @@
+
+
+
+
+
+
+
+
+
+ 10
+
+
+ 10
+
+
+ 10
+
+
+ 0
+
+
+
+
+ 10
+ 20
+
diff --git a/config/alfresco/messages/system-messages.properties b/config/alfresco/messages/system-messages.properties
index cef83f502a..d5fbf61b71 100644
--- a/config/alfresco/messages/system-messages.properties
+++ b/config/alfresco/messages/system-messages.properties
@@ -24,4 +24,4 @@ system.openoffice.err.connection_remade=The OpenOffice connection was re-establi
system.locks.err.failed_to_acquire_lock=Failed to get lock ''{0}'' using token ''{1}''.
system.locks.err.lock_resource_missing=Failed to manipulate lock ''{0}'' using token ''{1}''. The lock resource no longer exists.
system.locks.err.lock_update_count=Failed to update lock ''{0}'' using token ''{1}''. {2} locks were updated when {3} should have been.
-system.locks.err.excl_lock_exists=Failed to get lock ''{0}'' using token ''{1}''. An exclusive lock in the hierarchy exists.
\ No newline at end of file
+system.locks.err.excl_lock_exists=Failed to get lock ''{0}'' using token ''{1}''. An exclusive lock exists: {2}
\ No newline at end of file
diff --git a/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java b/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java
index 5b323010a0..b4e4558300 100644
--- a/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java
@@ -117,7 +117,7 @@ public abstract class AbstractLockDAOImpl implements LockDAO
{
throw new LockAcquisitionException(
LockAcquisitionException.ERR_EXCLUSIVE_LOCK_EXISTS,
- lockQName, lockToken);
+ lockQName, lockToken, existingLock);
}
existingLocksMap.put(existingLock, existingLock);
}
diff --git a/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java b/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java
index a1003b2b09..4e439498c6 100644
--- a/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java
+++ b/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java
@@ -335,7 +335,7 @@ public class LockDAOTest extends TestCase
public synchronized void testConcurrentLockAquisition() throws Exception
{
ReentrantLock threadLock = new ReentrantLock();
- GetLockThread[] threads = new GetLockThread[50];
+ GetLockThread[] threads = new GetLockThread[5];
for (int i = 0; i < threads.length; i++)
{
threads[i] = new GetLockThread(threadLock);
@@ -346,7 +346,7 @@ public class LockDAOTest extends TestCase
waitLoop:
for (int waitLoop = 0; waitLoop < 50; waitLoop++)
{
- wait(2000L);
+ wait(1000L);
for (int i = 0; i < threads.length; i++)
{
if (!threads[i].done)
diff --git a/source/java/org/alfresco/repo/domain/locks/LockEntity.java b/source/java/org/alfresco/repo/domain/locks/LockEntity.java
index 2353e285f5..2250eff212 100644
--- a/source/java/org/alfresco/repo/domain/locks/LockEntity.java
+++ b/source/java/org/alfresco/repo/domain/locks/LockEntity.java
@@ -37,6 +37,8 @@ import org.alfresco.util.EqualsHelper;
*/
public class LockEntity
{
+ public static final Long CONST_LONG_ZERO = new Long(0L);
+
private Long id;
private Long version;
private Long sharedResourceId;
@@ -71,6 +73,18 @@ public class LockEntity
}
}
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder(512);
+ sb.append("LockEntity")
+ .append("[ ID=").append(id)
+ .append(", sharedResourceId=").append(sharedResourceId)
+ .append(", exclusiveResourceId=").append(exclusiveResourceId)
+ .append("]");
+ return sb.toString();
+ }
+
/**
* Determine if the lock is logically exclusive. A lock is exclusive if the
* shared lock resource matches the exclusive lock resource.
@@ -111,9 +125,20 @@ public class LockEntity
this.version = version;
}
+ /**
+ * Increments the version number or resets it if it reaches a large number
+ */
public void incrementVersion()
{
- this.version = new Long(version.longValue() + 1L);
+ long currentVersion = version.longValue();
+ if (currentVersion >= 10E6)
+ {
+ this.version = CONST_LONG_ZERO;
+ }
+ else
+ {
+ this.version = new Long(version.longValue() + 1L);
+ }
}
/**
diff --git a/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java b/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java
index ef07e9131e..8822038357 100644
--- a/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java
@@ -41,7 +41,6 @@ import org.springframework.orm.ibatis.SqlMapClientTemplate;
*/
public class LockDAOImpl extends AbstractLockDAOImpl
{
- private static final Long CONST_LONG_ZERO = new Long(0L);
private static final String SELECT_LOCKRESOURCE_BY_QNAME = "select.LockResourceByQName";
private static final String SELECT_LOCK_BY_ID = "select.LockByID";
private static final String SELECT_LOCK_BY_KEY = "select.LockByKey";
@@ -73,7 +72,7 @@ public class LockDAOImpl extends AbstractLockDAOImpl
protected LockResourceEntity createLockResource(Long qnameNamespaceId, String qnameLocalName)
{
LockResourceEntity lockResource = new LockResourceEntity();
- lockResource.setVersion(CONST_LONG_ZERO);
+ lockResource.setVersion(LockEntity.CONST_LONG_ZERO);
lockResource.setQnameNamespaceId(qnameNamespaceId);
lockResource.setQnameLocalName(qnameLocalName);
Long id = (Long) template.insert(INSERT_LOCKRESOURCE, lockResource);
@@ -120,7 +119,7 @@ public class LockDAOImpl extends AbstractLockDAOImpl
long timeToLive)
{
LockEntity lock = new LockEntity();
- lock.setVersion(CONST_LONG_ZERO);
+ lock.setVersion(LockEntity.CONST_LONG_ZERO);
lock.setSharedResourceId(sharedResourceId);
lock.setExclusiveResourceId(exclusiveResourceId);
lock.setLockToken(lockToken);
diff --git a/source/java/org/alfresco/repo/lock/JobLockService.java b/source/java/org/alfresco/repo/lock/JobLockService.java
new file mode 100644
index 0000000000..0d1f47c47f
--- /dev/null
+++ b/source/java/org/alfresco/repo/lock/JobLockService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2005-2009 Alfresco Software Limited.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program 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 General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ * As a special exception to the terms and conditions of version 2.0 of
+ * the GPL, you may redistribute this Program in connection with Free/Libre
+ * and Open Source Software ("FLOSS") applications as described in Alfresco's
+ * FLOSS exception. You should have recieved a copy of the text describing
+ * the FLOSS exception, and it is also available here:
+ * http://www.alfresco.com/legal/licensing"
+ */
+package org.alfresco.repo.lock;
+
+import org.alfresco.service.namespace.QName;
+
+/**
+ * Service interface for managing job locks.
+ *
+ * Locks are identified by a fully qualified name ({@link QName}) and follow a hierarchical
+ * naming convention i.e. locks higher up a hierarchy can be shared but will prevent explicit
+ * (exclusive) locks from being taken. For example: If exclusive lock a.a.a has been
+ * taken, then a.a and a are all implicitly taken as shared locks. Exclusive lock
+ * a.a.b can be taken by another process and will share locks a.a and a
+ * with the first process. It will not be possible for a third process to take a lock on
+ * a.a, however.
+ *
+ * LOCK ORDERING:
+ * The transactional locks will be applied in strict alphabetical order. A very basic deadlock
+ * prevention system (at least) must be in place when applying or reapplying locks and be biased
+ * against locks applied non-alphabetically.
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+public interface JobLockService
+{
+ /**
+ * Take a transactionally-managed lock. This method can be called repeatedly to both
+ * initially acquire the lock as well as to maintain the lock. This method should
+ * either be called again before the lock expires or the transaction should end before
+ * the lock expires.
+ *
+ * The following rules apply to taking and releasing locks:
+ * - Expired locks can be taken by any process
+ * - Lock expiration does not prevent a lock from being refreshed or released
+ * - Only locks that were manipulated using another token will cause failure
+ *
+ * The locks are automatically released when the transaction is terminated.
+ *
+ * Any failure to acquire the lock (after retries), refresh the lock or subsequently
+ * release the owned locks will invalidate the transaction and cause rollback.
+ *
+ * @param lockQName the name of the lock to acquire
+ * @param timeToLive the time (in milliseconds) for the lock to remain valid
+ * @throws LockAcquisitionException if the lock could not be acquired
+ * @throws IllegalStateException if a transaction is not active
+ */
+ void getTransacionalLock(QName lockQName, long timeToLive);
+
+ /**
+ * {@inheritDoc JobLockService#getTransacionalLock(QName, long)}
+ *
+ * If the lock cannot be immediately acquired, the process will wait and retry. Note
+ * that second and subsequent attempts to get the lock during a transaction cannot
+ * make use of retrying; the lock is actually being refreshed and will therefore never
+ * become valid if it doesn't refresh directly.
+ *
+ * @param retryWait the time (in milliseconds) to wait before trying again
+ * @param retryCount the maximum number of times to attempt the lock acquisition
+ * @throws LockAcquisitionException if the lock could not be acquired
+ * @throws IllegalStateException if a transaction is not active
+ */
+ void getTransacionalLock(QName lockQName, long timeToLive, long retryWait, int retryCount);
+}
diff --git a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java
new file mode 100644
index 0000000000..6555a37170
--- /dev/null
+++ b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2005-2009 Alfresco Software Limited.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program 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 General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ * As a special exception to the terms and conditions of version 2.0 of
+ * the GPL, you may redistribute this Program in connection with Free/Libre
+ * and Open Source Software ("FLOSS") applications as described in Alfresco's
+ * FLOSS exception. You should have recieved a copy of the text describing
+ * the FLOSS exception, and it is also available here:
+ * http://www.alfresco.com/legal/licensing"
+ */
+package org.alfresco.repo.lock;
+
+import java.util.TreeSet;
+
+import org.alfresco.repo.domain.locks.LockDAO;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.repo.transaction.TransactionListenerAdapter;
+import org.alfresco.repo.transaction.TransactionalResourceHelper;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.namespace.QName;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * {@inheritDoc JobLockService}
+ *
+ * @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;
+
+ /**
+ * Stateless listener that does post-transaction cleanup.
+ */
+ private final LockTransactionListener txnListener;
+
+ public JobLockServiceImpl()
+ {
+ defaultRetryWait = 20;
+ defaultRetryCount = 10;
+ txnListener = new LockTransactionListener();
+ }
+
+ /**
+ * 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}
+ */
+ public void getTransacionalLock(QName lockQName, long timeToLive)
+ {
+ getTransacionalLock(lockQName, timeToLive, defaultRetryWait, defaultRetryCount);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void getTransacionalLock(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 heldLocks = TransactionalResourceHelper.getTreeSet(KEY_RESOURCE_LOCKS);
+ // We don't want the lock registered as being held if something goes wrong
+ TreeSet heldLocksTemp = new TreeSet(heldLocks);
+ boolean added = heldLocksTemp.add(lockQName);
+ if (!added)
+ {
+ // It's a refresh. Ordering is not important here as we already hold the lock.
+ refreshLock(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.
+ getLock(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
+ getLock(lockQName, timeToLive, retryWait, 1);
+ }
+ }
+ // It went in, so add it to the transactionally-stored set
+ heldLocks.add(lockQName);
+ // Done
+ }
+
+ /**
+ * @throws LockAcquisitionException on failure
+ */
+ private void refreshLock(final QName lockQName, final long timeToLive)
+ {
+ // The lock token is the current transaction ID
+ final String txnId = AlfrescoTransactionSupport.getTransactionId();
+ RetryingTransactionCallback