splitLockQName(QName lockQName)
{
diff --git a/source/java/org/alfresco/repo/domain/locks/LockDAO.java b/source/java/org/alfresco/repo/domain/locks/LockDAO.java
index 0b9ba2222e..990e6490fb 100644
--- a/source/java/org/alfresco/repo/domain/locks/LockDAO.java
+++ b/source/java/org/alfresco/repo/domain/locks/LockDAO.java
@@ -37,12 +37,43 @@ public interface LockDAO
/**
* Aquire a given exclusive lock, assigning it (and any implicitly shared locks) a
* timeout. All shared locks are implicitly taken as well.
+ *
+ * A lock can be re-taken if it has expired and if the lock token has not changed
*
* @param lockQName the unique name of the lock to acquire
- * @param lockApplicant the potential lock holder's identifier (max 36 chars)
+ * @param lockToken the potential lock token (max 36 chars)
* @param timeToLive the time (in milliseconds) that the lock must remain
* @return Returns true if the lock was taken,
* otherwise false
*/
- boolean getLock(QName lockQName, String lockApplicant, long timeToLive);
+ boolean getLock(QName lockQName, String lockToken, long timeToLive);
+
+ /**
+ * Refresh a held lock. This is successful if the lock in question still exists
+ * and if the lock token has not changed. Lock expiry does not prevent the lock
+ * from being refreshed.
+ *
+ * @param lockQName the unique name of the lock to update
+ * @param lockToken the lock token for the lock held
+ * @param timeToLive the new time to live (in milliseconds)
+ * @return Returns true if the lock was updated,
+ * otherwise false
+ */
+ boolean refreshLock(QName lockQName, String lockToken, long timeToLive);
+
+ /**
+ * Release a lock. The lock token must still apply and all the shared and exclusive
+ * locks need to still be present. Lock expiration does not prevent this operation
+ * from succeeding.
+ *
+ * Note: Failure to release a lock due to a exception condition is dealt with by
+ * passing the exception out.
+ *
+ * @param lockQName the unique name of the lock to release
+ * @param lockToken the current lock token
+ * @return Returns true if all the required locks were
+ * (still) held under the lock token and were
+ * valid at the time of release, otherwise false
+ */
+ boolean releaseLock(QName lockQName, String lockToken);
}
diff --git a/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java b/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java
new file mode 100644
index 0000000000..b5b740bb43
--- /dev/null
+++ b/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java
@@ -0,0 +1,420 @@
+/*
+ * 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.domain.locks;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
+
+import junit.framework.TestCase;
+
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.ApplicationContextHelper;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * @see LockDAO
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+public class LockDAOTest extends TestCase
+{
+ public static final String NAMESPACE = "http://www.alfresco.org/test/LockDAOTest";
+
+ private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
+
+ private TransactionService transactionService;
+ private RetryingTransactionHelper txnHelper;
+ private LockDAO lockDAO;
+ // Lock names for the tests
+ private QName lockA;
+ private QName lockAA;
+ private QName lockAAA;
+ private QName lockAAB;
+ private QName lockAAC;
+ private QName lockAB;
+ private QName lockABA;
+ private QName lockABB;
+ private QName lockABC;
+
+ @Override
+ public void setUp() throws Exception
+ {
+ ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
+ transactionService = serviceRegistry.getTransactionService();
+ txnHelper = transactionService.getRetryingTransactionHelper();
+ txnHelper.setMinRetryWaitMs(10);
+ txnHelper.setRetryWaitIncrementMs(10);
+ txnHelper.setMaxRetryWaitMs(50);
+
+ lockDAO = (LockDAO) ctx.getBean("lockDAO");
+ // Get the test name
+ String testName = getName();
+ // Build lock names for the test
+ lockA = QName.createQName(NAMESPACE, "a-" + testName);
+ lockAA = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName);
+ lockAAA = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName + ".a-" + testName);
+ lockAAB = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName + ".b-" + testName);
+ lockAAC = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName + ".c-" + testName);
+ lockAB = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName);
+ lockABA = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName + ".a-" + testName);
+ lockABB = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName + ".b-" + testName);
+ lockABC = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName + ".c-" + testName);
+ }
+
+ private String lock(final QName lockName, final long timeToLive, boolean expectSuccess)
+ {
+ String token = lock(lockName, timeToLive);
+ if (expectSuccess)
+ {
+ assertNotNull(
+ "Expected to get lock " + lockName + " with TTL of " + timeToLive,
+ token);
+ }
+ else
+ {
+ assertNull(
+ "Expected lock " + lockName + " to have been denied",
+ token);
+ }
+ return token;
+ }
+ /**
+ * Do the lock in a new transaction
+ * @return Returns the lock token or null if it didn't work
+ */
+ private String lock(final QName lockName, final long timeToLive)
+ {
+ RetryingTransactionCallback callback = new RetryingTransactionCallback()
+ {
+ public String execute() throws Throwable
+ {
+ String txnId = AlfrescoTransactionSupport.getTransactionId();
+ boolean locked = lockDAO.getLock(lockName, txnId, timeToLive);
+ return locked ? txnId : null;
+ }
+ };
+ return txnHelper.doInTransaction(callback);
+ }
+
+ private void refresh(final QName lockName, final String lockToken, final long timeToLive, boolean expectSuccess)
+ {
+ RetryingTransactionCallback callback = new RetryingTransactionCallback()
+ {
+ public Boolean execute() throws Throwable
+ {
+ return lockDAO.refreshLock(lockName, lockToken, timeToLive);
+ }
+ };
+ Boolean released = txnHelper.doInTransaction(callback);
+ if (expectSuccess)
+ {
+ assertTrue(
+ "Expected to have refreshed lock " + lockName,
+ released.booleanValue());
+ }
+ else
+ {
+ assertFalse(
+ "Expected to have failed to refresh lock " + lockName,
+ released.booleanValue());
+ }
+ }
+
+ private void release(final QName lockName, final String lockToken, boolean expectSuccess)
+ {
+ RetryingTransactionCallback callback = new RetryingTransactionCallback()
+ {
+ public Boolean execute() throws Throwable
+ {
+ return lockDAO.releaseLock(lockName, lockToken);
+ }
+ };
+ Boolean released = txnHelper.doInTransaction(callback);
+ if (expectSuccess)
+ {
+ assertTrue(
+ "Expected to have released lock " + lockName,
+ released.booleanValue());
+ }
+ else
+ {
+ assertFalse(
+ "Expected to have failed to release lock " + lockName,
+ released.booleanValue());
+ }
+ }
+
+ public void testGetLockBasic() throws Exception
+ {
+ lock(lockAAA, 500L, true);
+ }
+
+ /**
+ * Ensure that the lock tables and queries scale
+ */
+ public void testLockTableScaling() throws Exception
+ {
+ int count = 500;
+ long before = System.currentTimeMillis();
+ for (int i = 1; i <= count; i++)
+ {
+ QName lockName = QName.createQName(lockAAA.getNamespaceURI(), lockAAA.getLocalName() + "-" + i);
+ lock(lockName, 500L, true);
+ if (i % 100 == 0)
+ {
+ long after = System.currentTimeMillis();
+ System.out.println("Creation of " + i + " locks took " + (after-before)/1000 + "s");
+ }
+ }
+ }
+
+ public void testGetLockFailureBasic() throws Exception
+ {
+ lock(lockAAA, 500L, true);
+ lock(lockAAA, 0L, false);
+ }
+
+ public void testSharedLocks() throws Exception
+ {
+ lock(lockAAA, 500L, true);
+ lock(lockAAB, 500L, true);
+ lock(lockAAC, 500L, true);
+ lock(lockABA, 500L, true);
+ lock(lockABB, 500L, true);
+ lock(lockABC, 500L, true);
+ }
+
+ public void testExclusiveLockBlockedByShared() throws Exception
+ {
+ lock(lockAAA, 100L, true);
+ lock(lockAA, 100L, false);
+ lock(lockAB, 100L, true);
+ lock(lockA, 100L, false);
+ lock(lockABA, 100L, false);
+ }
+
+ public void testReleaseLockBasic() throws Exception
+ {
+ String token = lock(lockAAA, 500000L, true);
+ release(lockAAA, token, true);
+ token = lock(lockAAA, 0L, true);
+ }
+
+ public void testSharedLockAndRelease() throws Exception
+ {
+ String tokenAAA = lock(lockAAA, 5000L, true);
+ String tokenAAB = lock(lockAAB, 5000L, true);
+ String tokenAAC = lock(lockAAC, 5000L, true);
+ String tokenABA = lock(lockABA, 5000L, true);
+ String tokenABB = lock(lockABB, 5000L, true);
+ String tokenABC = lock(lockABC, 5000L, true);
+ // Can't lock shared resources
+ lock(lockAA, 0L, false);
+ lock(lockAB, 0L, false);
+ lock(lockA, 0L, false);
+ // Release a lock and check again
+ release(lockAAA, tokenAAA, true);
+ lock(lockAA, 0L, false);
+ lock(lockAB, 0L, false);
+ lock(lockA, 0L, false);
+ // Release a lock and check again
+ release(lockAAB, tokenAAB, true);
+ lock(lockAA, 0L, false);
+ lock(lockAB, 0L, false);
+ lock(lockA, 0L, false);
+ // Release a lock and check again
+ release(lockAAC, tokenAAC, true);
+ String tokenAA = lock(lockAA, 5000L, true); // This should be open now
+ lock(lockAB, 0L, false);
+ lock(lockA, 0L, false);
+ // Release a lock and check again
+ release(lockABA, tokenABA, true);
+ lock(lockAB, 0L, false);
+ lock(lockA, 0L, false);
+ // Release a lock and check again
+ release(lockABB, tokenABB, true);
+ lock(lockAB, 0L, false);
+ lock(lockA, 0L, false);
+ // Release a lock and check again
+ release(lockABC, tokenABC, true);
+ String tokenAB = lock(lockAB, 5000L, true);
+ lock(lockA, 0L, false);
+ // Release AA and AB
+ release(lockAA, tokenAA, true);
+ release(lockAB, tokenAB, true);
+ String tokenA = lock(lockA, 5000L, true);
+ // ... and release
+ release(lockA, tokenA, true);
+ }
+
+ public synchronized void testLockExpiry() throws Exception
+ {
+ lock(lockAAA, 50L, true);
+ this.wait(50L);
+ lock(lockAA, 50L, true);
+ this.wait(50L);
+ lock(lockA, 100L, true);
+ }
+
+ /**
+ * Check that locks grabbed away due to expiry cannot be released
+ * @throws Exception
+ */
+ public synchronized void testLockExpiryAndRelease() throws Exception
+ {
+ String tokenAAA = lock(lockAAA, 500L, true);
+ release(lockAAA, tokenAAA, true);
+ tokenAAA = lock(lockAAA, 50L, true); // Make sure we can re-acquire the lock
+ this.wait(50L); // Wait for expiry
+ String grabbedTokenAAAA = lock(lockAAA, 50L, true); // Grabbed lock over the expiry
+ release(lockAAA, tokenAAA, false); // Can't release any more
+ this.wait(50L); // Wait for expiry
+ release(lockAAA, grabbedTokenAAAA, true); // Proof that expiry, on it's own, doesn't prevent release
+ }
+
+ public synchronized void testLockRefresh() throws Exception
+ {
+ String tokenAAA = lock(lockAAA, 1000L, true);
+ // Loop, refreshing and testing
+ for (int i = 0; i < 40; i++)
+ {
+ wait(50L);
+ // It will have expired, but refresh it anyway
+ refresh(lockAAA, tokenAAA, 1000L, true);
+ // Check that it is still holding
+ lock(lockAAA, 0L, false);
+ }
+ }
+
+ /**
+ * Uses a thread lock to ensure that the lock DAO only allows locks through one at a time.
+ */
+ public synchronized void testConcurrentLockAquisition() throws Exception
+ {
+ ReentrantLock threadLock = new ReentrantLock();
+ GetLockThread[] threads = new GetLockThread[50];
+ for (int i = 0; i < threads.length; i++)
+ {
+ threads[i] = new GetLockThread(threadLock);
+ threads[i].start();
+ }
+ // Wait a bit and see if any encountered errors
+ boolean allDone = false;
+ waitLoop:
+ for (int waitLoop = 0; waitLoop < 50; waitLoop++)
+ {
+ wait(2000L);
+ for (int i = 0; i < threads.length; i++)
+ {
+ if (!threads[i].done)
+ {
+ continue waitLoop;
+ }
+ }
+ // All the threads are done
+ allDone = true;
+ break;
+ }
+ // Check that all the threads got a turn
+ if (!allDone)
+ {
+ fail("Not all threads managed to acquire the lock");
+ }
+ // Get errors
+ StringBuilder errors = new StringBuilder(512);
+ for (int i = 0; i < threads.length; i++)
+ {
+ if (threads[i].error != null)
+ {
+ errors.append("\nThread ").append(i).append(" error: ").append(threads[i].error);
+ }
+ }
+ if (errors.toString().length() > 0)
+ {
+ fail(errors.toString());
+ }
+ }
+
+ /**
+ * Checks that the lock via the DAO forces a serialization
+ */
+ private class GetLockThread extends Thread
+ {
+ private final ReentrantLock threadLock;
+ private boolean done;
+ private String error;
+ private GetLockThread(ReentrantLock threadLock)
+ {
+ this.threadLock = threadLock;
+ this.done = false;
+ this.error = null;
+ setDaemon(true);
+ }
+ @Override
+ public synchronized void run()
+ {
+ boolean gotLock = false;
+ try
+ {
+ String tokenAAA = null;
+ while (true)
+ {
+ tokenAAA = lock(lockAAA, 100000L); // Lock for a long time
+ if (tokenAAA != null)
+ {
+ break; // Got the lock
+ }
+ try { wait(20L); } catch (InterruptedException e) {}
+ }
+ gotLock = threadLock.tryLock(0, TimeUnit.MILLISECONDS);
+ if (!gotLock)
+ {
+ error = "Got lock via DAO but not via thread lock";
+ return;
+ }
+ release(lockAAA, tokenAAA, true);
+ }
+ catch (Throwable e)
+ {
+ error = e.getMessage();
+ }
+ finally
+ {
+ done = true;
+ if (gotLock)
+ {
+ threadLock.unlock();
+ }
+ }
+ }
+ }
+}
diff --git a/source/java/org/alfresco/repo/domain/locks/LockEntity.java b/source/java/org/alfresco/repo/domain/locks/LockEntity.java
index bc325b0ac5..2353e285f5 100644
--- a/source/java/org/alfresco/repo/domain/locks/LockEntity.java
+++ b/source/java/org/alfresco/repo/domain/locks/LockEntity.java
@@ -41,9 +41,9 @@ public class LockEntity
private Long version;
private Long sharedResourceId;
private Long exclusiveResourceId;
- private String lockHolder;
+ private String lockToken;
private Long startTime;
- private Long expiryTime = Long.MAX_VALUE; // TODO:
+ private Long expiryTime = Long.MIN_VALUE; // 'expired' unless set
@Override
public int hashCode()
@@ -144,19 +144,19 @@ public class LockEntity
}
/**
- * @return Returns the ID of the lock holder
+ * @return Returns the token assigned when the lock was created
*/
- public String getLockHolder()
+ public String getLockToken()
{
- return lockHolder;
+ return lockToken;
}
/**
- * @param lockHolder the ID of the lock holder
+ * @param lockToken the token assigned when the lock was created
*/
- public void setLockHolder(String lockHolder)
+ public void setLockToken(String lockToken)
{
- this.lockHolder = lockHolder;
+ this.lockToken = lockToken;
}
/**
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 ac3419a3f0..5964daa65c 100644
--- a/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java
@@ -24,7 +24,9 @@
*/
package org.alfresco.repo.domain.locks.ibatis;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.alfresco.repo.domain.locks.AbstractLockDAOImpl;
import org.alfresco.repo.domain.locks.LockEntity;
@@ -41,9 +43,13 @@ 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";
private static final String SELECT_LOCK_BY_SHARED_IDS = "select.LockBySharedIds";
private static final String INSERT_LOCKRESOURCE = "insert.LockResource";
private static final String INSERT_LOCK = "insert.Lock";
+ private static final String UPDATE_LOCK = "update.Lock";
+ private static final String UPDATE_EXCLUSIVE_LOCK = "update.ExclusiveLock";
private SqlMapClientTemplate template;
@@ -78,25 +84,46 @@ public class LockDAOImpl extends AbstractLockDAOImpl
@SuppressWarnings("unchecked")
@Override
- protected List getLocks(List lockResourceIds)
+ protected List getLocksBySharedResourceIds(List sharedLockResourceIds)
{
- List locks = template.queryForList(SELECT_LOCK_BY_SHARED_IDS, lockResourceIds);
+ List locks = template.queryForList(SELECT_LOCK_BY_SHARED_IDS, sharedLockResourceIds);
// Done
return locks;
}
+
+ @Override
+ protected LockEntity getLock(Long id)
+ {
+ LockEntity lock = new LockEntity();
+ lock.setId(id);
+ lock = (LockEntity) template.queryForObject(SELECT_LOCK_BY_ID, lock);
+ // Done
+ return lock;
+ }
+
+ @Override
+ protected LockEntity getLock(Long sharedResourceId, Long exclusiveResourceId)
+ {
+ LockEntity lock = new LockEntity();
+ lock.setSharedResourceId(sharedResourceId);
+ lock.setExclusiveResourceId(exclusiveResourceId);
+ lock = (LockEntity) template.queryForObject(SELECT_LOCK_BY_KEY, lock);
+ // Done
+ return lock;
+ }
@Override
protected LockEntity createLock(
Long sharedResourceId,
Long exclusiveResourceId,
- String lockApplicant,
+ String lockToken,
long timeToLive)
{
LockEntity lock = new LockEntity();
lock.setVersion(CONST_LONG_ZERO);
lock.setSharedResourceId(sharedResourceId);
lock.setExclusiveResourceId(exclusiveResourceId);
- lock.setLockHolder(lockApplicant);
+ lock.setLockToken(lockToken);
long now = System.currentTimeMillis();
long exp = now + timeToLive;
lock.setStartTime(now);
@@ -106,4 +133,42 @@ public class LockDAOImpl extends AbstractLockDAOImpl
// Done
return lock;
}
+
+ @Override
+ protected LockEntity updateLock(LockEntity lockEntity, String lockToken, long timeToLive)
+ {
+ LockEntity updateLockEntity = new LockEntity();
+ updateLockEntity.setId(lockEntity.getId());
+ updateLockEntity.setVersion(lockEntity.getVersion());
+ updateLockEntity.incrementVersion(); // Increment the version number
+ updateLockEntity.setSharedResourceId(lockEntity.getSharedResourceId());
+ updateLockEntity.setExclusiveResourceId(lockEntity.getExclusiveResourceId());
+ updateLockEntity.setLockToken(lockToken);
+ long now = System.currentTimeMillis();
+ Long exp = now + timeToLive;
+ updateLockEntity.setStartTime(lockEntity.getStartTime()); // Keep original start time
+ updateLockEntity.setExpiryTime(exp); // Don't update the start time
+ template.update(UPDATE_LOCK, updateLockEntity, 1);
+ // Done
+ return updateLockEntity;
+ }
+
+ @Override
+ protected int updateLocks(
+ Long exclusiveLockResourceId,
+ String oldLockToken,
+ String newLockToken,
+ long timeToLive)
+ {
+ Map params = new HashMap(11);
+ params.put("exclusiveLockResourceId", exclusiveLockResourceId);
+ params.put("oldLockToken", oldLockToken);
+ params.put("newLockToken", oldLockToken);
+ long now = System.currentTimeMillis();
+ Long exp = new Long(now + timeToLive);
+ params.put("newExpiryTime", exp);
+ int updateCount = template.update(UPDATE_EXCLUSIVE_LOCK, params);
+ // Done
+ return updateCount;
+ }
}
diff --git a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java
index a702dba23a..8a93c6406c 100644
--- a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java
+++ b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java
@@ -54,6 +54,7 @@ import org.springframework.aop.framework.ProxyFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DeadlockLoserDataAccessException;
+import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException;
import org.springframework.jdbc.UncategorizedSQLException;
import com.ibatis.common.jdbc.exception.NestedSQLException;
@@ -95,6 +96,7 @@ public class RetryingTransactionHelper
ConcurrencyFailureException.class,
DeadlockLoserDataAccessException.class,
StaleObjectStateException.class,
+ JdbcUpdateAffectedIncorrectNumberOfRowsException.class, // Similar to StaleObjectState
LockAcquisitionException.class,
ConstraintViolationException.class,
UncategorizedSQLException.class,
@@ -137,7 +139,7 @@ public class RetryingTransactionHelper
/**
* Callback interface
- * @author britt
+ * @author Derek Hulley
*/
public interface RetryingTransactionCallback
{