diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index ed9fc86699..bb03702f7a 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -148,6 +148,15 @@ ${server.transaction.allow-writes} + + + + + + + 10 + + diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTest.java b/source/java/org/alfresco/repo/avm/AVMServiceTest.java index 0896d039da..f1e453fad8 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTest.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTest.java @@ -19,6 +19,7 @@ package org.alfresco.repo.avm; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Serializable; @@ -40,6 +41,7 @@ import org.alfresco.repo.avm.actions.SimpleAVMSubmitAction; import org.alfresco.repo.avm.util.BulkLoader; import org.alfresco.repo.domain.PropertyValue; import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.TransactionUtil; import org.alfresco.service.cmr.avm.AVMBadArgumentException; import org.alfresco.service.cmr.avm.AVMCycleException; @@ -3262,56 +3264,67 @@ public class AVMServiceTest extends AVMServiceTestBase try { setupBasicTree(); - class TxnWork implements TransactionUtil.TransactionWork + class TxnCallback implements RetryingTransactionHelper.Callback { - public Object doWork() throws Exception + public Object execute() { - AVMService service = (AVMService)fContext.getBean("avmService"); - service.createLayeredDirectory("main:/a", "main:/", "layer"); - // Modify something in an ordinary directory 3 times. - service.getFileOutputStream("main:/a/b/c/foo").close(); - service.getFileOutputStream("main:/a/b/c/foo").close(); - service.getFileOutputStream("main:/a/b/c/foo").close(); - service.createFile("main:/a/b/c", "pint").close(); - service.createFile("main:/a/b/c", "quart").close(); - // Modify another file in the same directory. - service.getFileOutputStream("main:/a/b/c/bar").close(); - service.getFileOutputStream("main:/a/b/c/bar").close(); - service.lookup(-1, "main:/a/b/c"); - service.createFile("main:/a/b/c", "figment").close(); - // Repeat in a layer. - service.getFileOutputStream("main:/layer/b/c/foo").close(); - service.getFileOutputStream("main:/layer/b/c/foo").close(); - service.getFileOutputStream("main:/layer/b/c/foo").close(); - service.createFile("main:/layer/b/c", "gallon").close(); - service.createFile("main:/layer/b/c", "dram").close(); - service.getFileOutputStream("main:/layer/b/c/bar").close(); - service.getFileOutputStream("main:/layer/b/c/bar").close(); try { - service.lookup(-1, "main:/a/b/c/froo"); + AVMService service = (AVMService)fContext.getBean("avmService"); + service.createLayeredDirectory("main:/a", "main:/", "layer"); + // Modify something in an ordinary directory 3 times. + service.getFileOutputStream("main:/a/b/c/foo").close(); + service.getFileOutputStream("main:/a/b/c/foo").close(); + service.getFileOutputStream("main:/a/b/c/foo").close(); + service.createFile("main:/a/b/c", "pint").close(); + service.createFile("main:/a/b/c", "quart").close(); + // Modify another file in the same directory. + service.getFileOutputStream("main:/a/b/c/bar").close(); + service.getFileOutputStream("main:/a/b/c/bar").close(); + service.lookup(-1, "main:/a/b/c"); + service.createFile("main:/a/b/c", "figment").close(); + // Repeat in a layer. + service.getFileOutputStream("main:/layer/b/c/foo").close(); + service.getFileOutputStream("main:/layer/b/c/foo").close(); + service.getFileOutputStream("main:/layer/b/c/foo").close(); + service.createFile("main:/layer/b/c", "gallon").close(); + service.createFile("main:/layer/b/c", "dram").close(); + service.getFileOutputStream("main:/layer/b/c/bar").close(); + service.getFileOutputStream("main:/layer/b/c/bar").close(); + try + { + service.lookup(-1, "main:/a/b/c/froo"); + } + catch (AVMException ae) + { + // Do nothing. + } + service.createDirectory("main:/a/b/c", "froo"); + service.createFile("main:/a/b/c/froo", "franistan").close(); + try + { + service.lookup(-1, "main:/layer/b/c/groo"); + } + catch (AVMException ae) + { + // Do nothing. + } + service.createDirectory("main:/layer/b/c", "groo"); + service.createFile("main:/layer/b/c/groo", "granistan").close(); + return null; } - catch (AVMException ae) + catch (IOException e) { - // Do nothing. + e.printStackTrace(); + fail(); + return null; } - service.createDirectory("main:/a/b/c", "froo"); - service.createFile("main:/a/b/c/froo", "franistan").close(); - try - { - service.lookup(-1, "main:/layer/b/c/groo"); - } - catch (AVMException ae) - { - // Do nothing. - } - service.createDirectory("main:/layer/b/c", "groo"); - service.createFile("main:/layer/b/c/groo", "granistan").close(); - return null; } } - TransactionUtil.executeInUserTransaction((TransactionService)fContext.getBean("transactionComponent"), - new TxnWork()); + RetryingTransactionHelper helper = + (RetryingTransactionHelper)fContext.getBean("retryingTransactionHelper"); + helper.doInTransaction(new TxnCallback(), false); + assertNotNull(fService.lookup(-1, "main:/layer/b/c/groo")); } catch (Exception e) { diff --git a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java new file mode 100644 index 0000000000..e6a16116a5 --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java @@ -0,0 +1,156 @@ +/** + * + */ +package org.alfresco.repo.transaction; + +import java.util.Random; + +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.transaction.TransactionService; +import org.apache.log4j.Logger; +import org.hibernate.StaleObjectStateException; +import org.hibernate.exception.LockAcquisitionException; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DeadlockLoserDataAccessException; + +/** + * A helper that runs a unit of work inside a UserTransaction, + * transparently retrying the unit of work if the cause of + * failure is an optimistic locking or deadlock condition. + * @author britt + */ +public class RetryingTransactionHelper +{ + private static Logger fgLogger = Logger.getLogger(RetryingTransactionHelper.class); + + /** + * Reference to the TransactionService instance. + */ + private TransactionService fTxnService; + + /** + * The maximum number of retries. -1 for infinity. + */ + private int fMaxRetries; + + /** + * Random number generator for retry delays. + */ + private Random fRandom; + + /** + * Callback interface + * @author britt + */ + public interface Callback + { + public Object execute(); + }; + + /** + * Default constructor. + */ + public RetryingTransactionHelper() + { + fRandom = new Random(System.currentTimeMillis()); + } + + // Setters. + /** + * Set the TransactionService. + */ + public void setTransactionService(TransactionService service) + { + fTxnService = service; + } + + /** + * Set the maximimum number of retries. -1 for infinity. + */ + public void setMaxRetries(int maxRetries) + { + fMaxRetries = maxRetries; + } + + /** + * Execute a callback in a transaction until it succeeds, fails + * because of an error not the result of an optimistic locking failure, + * or a deadlock loser failure, or until a maximum number of retries have + * been attempted. NB that this ignores transaction status and relies entirely + * on thrown exceptions to decide to rollback. Also this is non-reentrant, not + * to be called within an existing transaction. + * @param cb The callback containing the unit of work. + * @param readOnly Whether this is a read only transaction. + * @return The result of the unit of work. + */ + public Object doInTransaction(Callback cb, boolean readOnly) + { + RuntimeException lastException = null; + for (int count = 0; fMaxRetries < 0 || count < fMaxRetries; ++count) + { + UserTransaction txn = null; + try + { + txn = fTxnService.getNonPropagatingUserTransaction(readOnly); + txn.begin(); + Object result = cb.execute(); + txn.commit(); + if (fgLogger.isDebugEnabled()) + { + if (count != 0) + { + fgLogger.debug("Transaction succeeded after " + count + " retries"); + } + } + return result; + } + catch (Exception e) + { + if (txn != null) + { + try + { + txn.rollback(); + } + catch (IllegalStateException e1) + { + throw new AlfrescoRuntimeException("Failure during rollback.", e1); + } + catch (SecurityException e1) + { + throw new AlfrescoRuntimeException("Failure during rollback.", e1); + } + catch (SystemException e1) + { + throw new AlfrescoRuntimeException("Failure during rollback.", e1); + } + } + if (e instanceof ConcurrencyFailureException || + e instanceof DeadlockLoserDataAccessException || + e instanceof StaleObjectStateException || + e instanceof LockAcquisitionException) + { + lastException = (RuntimeException)e; + try + { + Thread.sleep(fRandom.nextInt(500 * count + 500)); + } + catch (InterruptedException ie) + { + // Do nothing. + } + continue; + } + if (e instanceof RuntimeException) + { + throw (RuntimeException)e; + } + throw new AlfrescoRuntimeException("Exception in Transaction.", e); + } + } + throw lastException; + } +}