diff --git a/source/java/org/alfresco/repo/lock/JobLockService.java b/source/java/org/alfresco/repo/lock/JobLockService.java index b44a9a72df..5e6e77ccc7 100644 --- a/source/java/org/alfresco/repo/lock/JobLockService.java +++ b/source/java/org/alfresco/repo/lock/JobLockService.java @@ -141,10 +141,25 @@ public interface JobLockService */ String getLock(QName lockQName, long timeToLive, long retryWait, int retryCount); + /** + * Take a manually-managed lock and provide a callback to refresh it periodically. + * A convenience wrapper around {@link #getLock(QName,long)} + * and {@link #refreshLock(String,QName,long,JobLockRefreshCallback)}. + * + * @param lockQName the name of the lock to acquire + * @param timeToLive the time (in milliseconds) for the lock to remain valid. + * This value must not be larger than either the anticipated + * operation time or a server startup time. Typically, it should be + * a few seconds. + * @param callback the object that will be called at intervals of timeToLive/2 (about) + * @return Returns the newly-created lock token, or null if callback not active. + * @throws LockAcquisitionException if the lock could not be acquired + */ + String getLock(QName lockQName, long timeToLive, JobLockRefreshCallback callback); + /** * Refresh the lock using a valid lock token. * - * @param lockToken the lock token returned when the lock was acquired * @param lockQName the name of the previously-acquired lock * @param timeToLive the time (in milliseconds) for the lock to remain valid * @throws LockAcquisitionException if the lock could not be refreshed or acquired @@ -159,8 +174,9 @@ public interface JobLockService * Since the lock is not actually refreshed by this method, there will be no LockAcquisitionException. *

* The TTL (time to live) will be divided by two and the result used to trigger a timer thread - * to initiate the callback. - * + * to initiate the callback. The first refresh will occur after TTL/2 and no significant work + * should be done between acquiring a lock and calling this method, to prevent expiration. + * * @param lockToken the lock token returned when the lock was acquired * @param lockQName the name of the previously-acquired lock * @param timeToLive the time (in milliseconds) for the lock to remain valid diff --git a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java index 474e3a1b2e..790365a70c 100644 --- a/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java +++ b/source/java/org/alfresco/repo/lock/JobLockServiceImpl.java @@ -222,7 +222,7 @@ public class JobLockServiceImpl implements JobLockService // Done return lockToken; } - + /** * {@inheritDoc} * @@ -269,6 +269,28 @@ public class JobLockServiceImpl implements JobLockService } } + /** + * {@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} */ diff --git a/source/test-java/org/alfresco/repo/lock/JobLockServiceTest.java b/source/test-java/org/alfresco/repo/lock/JobLockServiceTest.java index 5723e95c5e..e1deecf1ae 100644 --- a/source/test-java/org/alfresco/repo/lock/JobLockServiceTest.java +++ b/source/test-java/org/alfresco/repo/lock/JobLockServiceTest.java @@ -29,6 +29,8 @@ import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.ApplicationContextHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.junit.experimental.categories.Category; @@ -50,6 +52,8 @@ public class JobLockServiceTest extends TestCase { public static final String NAMESPACE = "http://www.alfresco.org/test/JobLockServiceTest"; + private static final Log logger = LogFactory.getLog(JobLockServiceTest.class); + private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); private TransactionService transactionService; @@ -473,4 +477,128 @@ public class JobLockServiceTest extends TestCase assertEquals("Lock callback timer was not terminated", checkedCount, checked[0]); assertEquals("Lock callback timer was not terminated", releasedCount, released[0]); } + + public void testGetLockWithCallbackNullLock() { runGetLockWithCallback(0); } + public void testGetLockWithCallbackNullCallback() { runGetLockWithCallback(1); } + public void testGetLockWithCallbackShortTTL() { runGetLockWithCallback(2); } + public void testGetLockWithCallbackLocked() { runGetLockWithCallback(3); } + public void testGetLockWithCallbackNormal() { runGetLockWithCallback(4); } + + public void runGetLockWithCallback(int t) + { + logger.debug("runGetLockWithCallback "+t+ + "\n----------------------------------------"+ + "\n"+Thread.currentThread().getStackTrace()[2].getMethodName()+ + "\n----------------------------------------"); + + String token = null; + String tokenB = null; + try + { + QName lockName = t==0 ? null : lockA; + TestCallback callback = t==1 ? null : new TestCallback(); + long timeToLive = t==2 ? 1 : 50; + + if (t==3) + { + long ttlLongerThanDefaultRetry = 300; + tokenB = jobLockService.getLock(lockA, ttlLongerThanDefaultRetry); + } + + token = jobLockService.getLock(lockName, timeToLive, callback); + + if (t<4) fail("expected getLock to fail"); + + if (callback == null) throw new IllegalStateException(); + + assertEquals(false,callback.released); + assertEquals(0,callback.isActiveCount); + + Thread.sleep(40); + + assertEquals(false,callback.released); + assertEquals(1,callback.isActiveCount); + + callback.isActive = false; + + Thread.sleep(40); + + assertEquals(true,callback.released); + assertEquals(2,callback.isActiveCount); + } + catch (IllegalArgumentException e) + { + switch (t) + { + case 0: logger.debug("null lock => exception as expected: "+e); break; + case 1: logger.debug("null callback => exception as expected: "+e); break; + case 2: logger.debug("short ttl => exception as expected: "+e); break; + default: fail("exception not expected: "+e); break; + } + } + catch (LockAcquisitionException e) + { + switch (t) + { + case 3: logger.debug("already locked => exception as expected: "+e); break; + default: fail("exception not expected: "+e); break; + } + } + catch (Exception e) + { + fail("exception not expected: "+e); + } + finally + { + if (token != null) + { + logger.debug("token should have been released"); + if (jobLockService.releaseLockVerify(token, lockA)) + { + fail("token not released"); + } + } + + if (tokenB != null) + { + logger.debug("tokenB should be released"); + jobLockService.releaseLockVerify(tokenB, lockA); + } + + try + { + logger.debug("lock should have been released so check can acquire"); + String tokenC = jobLockService.getLock(lockA, 50); + jobLockService.releaseLock(tokenC, lockA); + } + catch (LockAcquisitionException e) + { + fail("lock not released"); + } + + logger.debug("runGetLockWithCallback\n----------------------------------------"); + } + } + + private class TestCallback implements JobLockRefreshCallback + { + public volatile long isActiveCount; + public volatile boolean released; + public volatile boolean isActive = true; + + @Override + public boolean isActive() + { + isActiveCount++; + logger.debug("TestCallback.isActive => "+isActive+" ("+isActiveCount+")"); + return isActive; + } + + @Override + public void lockReleased() + { + logger.debug("TestCallback.lockReleased"); + released = true; + } + } }