(5);
if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_TAGGABLE) == false)
@@ -586,16 +577,28 @@ public class TaggingServiceImpl implements TaggingService,
* @return NodeRef tag node reference or null not exist
*/
public NodeRef getTagNodeRef(StoreRef storeRef, String tag)
+ {
+ return getTagNodeRef(storeRef, tag, false);
+ }
+
+ /**
+ * Gets the node reference for a given tag.
+ *
+ * Returns null if tag is not present and not created.
+ *
+ * @param storeRef store reference
+ * @param tag tag
+ * @param create create a node if one doesn't exist?
+ * @return NodeRef tag node reference or null not exist
+ */
+ private NodeRef getTagNodeRef(StoreRef storeRef, String tag, boolean create)
{
NodeRef tagNodeRef = null;
- String query = "+PATH:\"cm:taggable/cm:" + ISO9075.encode(tag) + "\"";
- ResultSet resultSet = this.searchService.query(storeRef, SearchService.LANGUAGE_LUCENE, query);
- if (resultSet.length() != 0)
+ Collection results = this.categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, tag, create);
+ if (!results.isEmpty())
{
- tagNodeRef = resultSet.getNodeRef(0);
+ tagNodeRef = results.iterator().next().getChildRef();
}
- resultSet.close();
-
return tagNodeRef;
}
@@ -701,12 +704,7 @@ public class TaggingServiceImpl implements TaggingService,
tag = tag.toLowerCase();
// Get the tag node reference
- NodeRef newTagNodeRef = getTagNodeRef(nodeRef.getStoreRef(), tag);
- if (newTagNodeRef == null)
- {
- // Create the new tag
- newTagNodeRef = this.categoryService.createRootCategory(nodeRef.getStoreRef(), ContentModel.ASPECT_TAGGABLE, tag);
- }
+ NodeRef newTagNodeRef = getTagNodeRef(nodeRef.getStoreRef(), tag, true);
if (tagNodeRefs.contains(newTagNodeRef) == false)
{
@@ -953,10 +951,10 @@ public class TaggingServiceImpl implements TaggingService,
/*package*/ static List readTagDetails(InputStream is)
{
List result = new ArrayList(25);
-
+ BufferedReader reader = null;
try
{
- BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+ reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String nextLine = reader.readLine();
while (nextLine != null)
{
@@ -970,6 +968,10 @@ public class TaggingServiceImpl implements TaggingService,
{
throw new AlfrescoRuntimeException("Unable to read tag details", exception);
}
+ finally
+ {
+ try { reader.close(); } catch (Exception e) {}
+ }
return result;
}
diff --git a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java
index a6205c7e87..4179ca3c6e 100644
--- a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java
+++ b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java
@@ -121,6 +121,21 @@ public class RetryingTransactionHelper
private int maxRetryWaitMs;
/** How much to increase the wait time with each retry. */
private int retryWaitIncrementMs;
+
+ /**
+ * Optional time limit for execution time. When non-zero, retries will not continue when the projected time is
+ * beyond this time.
+ */
+ private long maxExecutionMs;
+
+ /** The number of concurrently exeucting transactions. Only maintained when maxExecutionMs is set. */
+ private int txnCount;
+
+ /**
+ * A 'ceiling' for the number of concurrent transactions that can execute. Dynamically maintained so that exeuction
+ * time is within maxExecutionMs. Transactions above this limit will be rejected with a {@link TooBusyException}.
+ */
+ private Integer txnCeiling;
/**
* Whether the the transactions may only be reads
@@ -205,6 +220,11 @@ public class RetryingTransactionHelper
this.retryWaitIncrementMs = retryWaitIncrementMs;
}
+ public void setMaxExecutionMs(long maxExecutionMs)
+ {
+ this.maxExecutionMs = maxExecutionMs;
+ }
+
/**
* Set whether this helper only supports read transactions.
*/
@@ -273,185 +293,256 @@ public class RetryingTransactionHelper
{
throw new AccessDeniedException(MSG_READ_ONLY);
}
- // Track the last exception caught, so that we
- // can throw it if we run out of retries.
- RuntimeException lastException = null;
- for (int count = 0; count == 0 || count < maxRetries; count++)
+
+ // If we are time limiting, set ourselves a time limit and maintain the count of concurrent transactions
+ long startTime = 0, endTime = 0, txnStartTime = 0;
+ int txnCountWhenStarted = 0;
+ if (maxExecutionMs > 0)
{
- UserTransaction txn = null;
- try
+ startTime = System.currentTimeMillis();
+ synchronized (this)
{
- if (requiresNew)
+ // If this transaction would take us above our ceiling, reject it
+ if (txnCeiling != null && txnCount >= txnCeiling)
{
- txn = txnService.getNonPropagatingUserTransaction(readOnly);
+ throw new TooBusyException("Too busy: " + txnCount + " transactions");
}
- else
+ txnCountWhenStarted = ++txnCount;
+ }
+ endTime = startTime + maxExecutionMs;
+ }
+
+ try
+ {
+ // Track the last exception caught, so that we
+ // can throw it if we run out of retries.
+ RuntimeException lastException = null;
+ for (int count = 0; count == 0 || count < maxRetries; count++)
+ {
+ // Monitor duration of each retry so that we can project an end time
+ if (maxExecutionMs > 0)
+ {
+ txnStartTime = System.currentTimeMillis();
+ }
+
+ UserTransaction txn = null;
+ try
{
- TxnReadState readState = AlfrescoTransactionSupport.getTransactionReadState();
- switch (readState)
+ if (requiresNew)
{
- case TXN_READ_ONLY:
- if (!readOnly)
- {
- // The current transaction is read-only, but a writable transaction is requested
- throw new AlfrescoRuntimeException("Read-Write transaction started within read-only transaction");
- }
- // We are in a read-only transaction and this is what we require so continue with it.
- break;
- case TXN_READ_WRITE:
- // We are in a read-write transaction. It cannot be downgraded so just continue with it.
- break;
- case TXN_NONE:
- // There is no current transaction so we need a new one.
- txn = txnService.getUserTransaction(readOnly);
- break;
- default:
- throw new RuntimeException("Unknown transaction state: " + readState);
- }
- }
- if (txn != null)
- {
- txn.begin();
- // Wrap it to protect it
- UserTransactionProtectionAdvise advise = new UserTransactionProtectionAdvise();
- ProxyFactory proxyFactory = new ProxyFactory(txn);
- proxyFactory.addAdvice(advise);
- UserTransaction wrappedTxn = (UserTransaction) proxyFactory.getProxy();
- // Store the UserTransaction for static retrieval. There is no need to unbind it
- // because the transaction management will do that for us.
- AlfrescoTransactionSupport.bindResource(KEY_ACTIVE_TRANSACTION, wrappedTxn);
- }
- // Do the work.
- R result = cb.execute();
- // Only commit if we 'own' the transaction.
- if (txn != null)
- {
- if (txn.getStatus() == Status.STATUS_MARKED_ROLLBACK)
- {
- if (logger.isDebugEnabled())
- {
- logger.debug("\n" +
- "Transaction marked for rollback: \n" +
- " Thread: " + Thread.currentThread().getName() + "\n" +
- " Txn: " + txn + "\n" +
- " Iteration: " + count);
- }
- // Something caused the transaction to be marked for rollback
- // There is no recovery or retrying with this
- txn.rollback();
+ txn = txnService.getNonPropagatingUserTransaction(readOnly);
}
else
{
- // The transaction hasn't been flagged for failure so the commit
- // sould still be good.
- txn.commit();
- }
- }
- if (logger.isDebugEnabled())
- {
- if (count != 0)
- {
- logger.debug("\n" +
- "Transaction succeeded: \n" +
- " Thread: " + Thread.currentThread().getName() + "\n" +
- " Txn: " + txn + "\n" +
- " Iteration: " + count);
- }
- }
- return result;
- }
- catch (Throwable e)
- {
- // Somebody else 'owns' the transaction, so just rethrow.
- if (txn == null)
- {
- RuntimeException ee = AlfrescoRuntimeException.makeRuntimeException(
- e, "Exception from transactional callback: " + cb);
- throw ee;
- }
- if (logger.isDebugEnabled())
- {
- logger.debug("\n" +
- "Transaction commit failed: \n" +
- " Thread: " + Thread.currentThread().getName() + "\n" +
- " Txn: " + txn + "\n" +
- " Iteration: " + count + "\n" +
- " Exception follows:",
- e);
- }
- // Rollback if we can.
- if (txn != null)
- {
- try
- {
- int txnStatus = txn.getStatus();
- // We can only rollback if a transaction was started (NOT NO_TRANSACTION) and
- // if that transaction has not been rolled back (NOT ROLLEDBACK).
- // If an exception occurs while the transaction is being created (e.g. no database connection)
- // then the status will be NO_TRANSACTION.
- if (txnStatus != Status.STATUS_NO_TRANSACTION && txnStatus != Status.STATUS_ROLLEDBACK)
+ TxnReadState readState = AlfrescoTransactionSupport.getTransactionReadState();
+ switch (readState)
{
- txn.rollback();
+ case TXN_READ_ONLY:
+ if (!readOnly)
+ {
+ // The current transaction is read-only, but a writable transaction is requested
+ throw new AlfrescoRuntimeException("Read-Write transaction started within read-only transaction");
+ }
+ // We are in a read-only transaction and this is what we require so continue with it.
+ break;
+ case TXN_READ_WRITE:
+ // We are in a read-write transaction. It cannot be downgraded so just continue with it.
+ break;
+ case TXN_NONE:
+ // There is no current transaction so we need a new one.
+ txn = txnService.getUserTransaction(readOnly);
+ break;
+ default:
+ throw new RuntimeException("Unknown transaction state: " + readState);
}
}
- catch (Throwable e1)
+ if (txn != null)
{
- // A rollback failure should not preclude a retry, but logging of the rollback failure is required
- logger.error("Rollback failure. Normal retry behaviour will resume.", e1);
+ txn.begin();
+ // Wrap it to protect it
+ UserTransactionProtectionAdvise advise = new UserTransactionProtectionAdvise();
+ ProxyFactory proxyFactory = new ProxyFactory(txn);
+ proxyFactory.addAdvice(advise);
+ UserTransaction wrappedTxn = (UserTransaction) proxyFactory.getProxy();
+ // Store the UserTransaction for static retrieval. There is no need to unbind it
+ // because the transaction management will do that for us.
+ AlfrescoTransactionSupport.bindResource(KEY_ACTIVE_TRANSACTION, wrappedTxn);
}
- }
- if (e instanceof RollbackException)
- {
- lastException = (e.getCause() instanceof RuntimeException) ?
- (RuntimeException)e.getCause() : new AlfrescoRuntimeException("Exception in Transaction.", e.getCause());
- }
- else
- {
- lastException = (e instanceof RuntimeException) ?
- (RuntimeException)e : new AlfrescoRuntimeException("Exception in Transaction.", e);
- }
- // Check if there is a cause for retrying
- Throwable retryCause = extractRetryCause(e);
- if (retryCause != null)
- {
- // Sleep a random amount of time before retrying.
- // The sleep interval increases with the number of retries.
- int sleepIntervalRandom = (count > 0 && retryWaitIncrementMs > 0)
- ? random.nextInt(count * retryWaitIncrementMs)
- : minRetryWaitMs;
- int sleepInterval = Math.min(maxRetryWaitMs, sleepIntervalRandom);
- sleepInterval = Math.max(sleepInterval, minRetryWaitMs);
- if (logger.isInfoEnabled() && !logger.isDebugEnabled())
+ // Do the work.
+ R result = cb.execute();
+ // Only commit if we 'own' the transaction.
+ if (txn != null)
{
- String msg = String.format(
- "Retrying %s: count %2d; wait: %1.1fs; msg: \"%s\"; exception: (%s)",
- Thread.currentThread().getName(),
- count, (double)sleepInterval/1000D,
- retryCause.getMessage(),
- retryCause.getClass().getName());
- logger.info(msg);
+ if (txn.getStatus() == Status.STATUS_MARKED_ROLLBACK)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("\n" +
+ "Transaction marked for rollback: \n" +
+ " Thread: " + Thread.currentThread().getName() + "\n" +
+ " Txn: " + txn + "\n" +
+ " Iteration: " + count);
+ }
+ // Something caused the transaction to be marked for rollback
+ // There is no recovery or retrying with this
+ txn.rollback();
+ }
+ else
+ {
+ // The transaction hasn't been flagged for failure so the commit
+ // sould still be good.
+ txn.commit();
+ }
}
- try
+ if (logger.isDebugEnabled())
{
- Thread.sleep(sleepInterval);
+ if (count != 0)
+ {
+ logger.debug("\n" +
+ "Transaction succeeded: \n" +
+ " Thread: " + Thread.currentThread().getName() + "\n" +
+ " Txn: " + txn + "\n" +
+ " Iteration: " + count);
+ }
}
- catch (InterruptedException ie)
- {
- // Do nothing.
- }
- // Try again
- continue;
+ return result;
}
- else
+ catch (Throwable e)
{
- // It was a 'bad' exception.
- throw lastException;
+ // Somebody else 'owns' the transaction, so just rethrow.
+ if (txn == null)
+ {
+ RuntimeException ee = AlfrescoRuntimeException.makeRuntimeException(
+ e, "Exception from transactional callback: " + cb);
+ throw ee;
+ }
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("\n" +
+ "Transaction commit failed: \n" +
+ " Thread: " + Thread.currentThread().getName() + "\n" +
+ " Txn: " + txn + "\n" +
+ " Iteration: " + count + "\n" +
+ " Exception follows:",
+ e);
+ }
+ // Rollback if we can.
+ if (txn != null)
+ {
+ try
+ {
+ int txnStatus = txn.getStatus();
+ // We can only rollback if a transaction was started (NOT NO_TRANSACTION) and
+ // if that transaction has not been rolled back (NOT ROLLEDBACK).
+ // If an exception occurs while the transaction is being created (e.g. no database connection)
+ // then the status will be NO_TRANSACTION.
+ if (txnStatus != Status.STATUS_NO_TRANSACTION && txnStatus != Status.STATUS_ROLLEDBACK)
+ {
+ txn.rollback();
+ }
+ }
+ catch (Throwable e1)
+ {
+ // A rollback failure should not preclude a retry, but logging of the rollback failure is required
+ logger.error("Rollback failure. Normal retry behaviour will resume.", e1);
+ }
+ }
+ if (e instanceof RollbackException)
+ {
+ lastException = (e.getCause() instanceof RuntimeException) ?
+ (RuntimeException)e.getCause() : new AlfrescoRuntimeException("Exception in Transaction.", e.getCause());
+ }
+ else
+ {
+ lastException = (e instanceof RuntimeException) ?
+ (RuntimeException)e : new AlfrescoRuntimeException("Exception in Transaction.", e);
+ }
+ // Check if there is a cause for retrying
+ Throwable retryCause = extractRetryCause(e);
+ if (retryCause != null)
+ {
+ // Sleep a random amount of time before retrying.
+ // The sleep interval increases with the number of retries.
+ int sleepIntervalRandom = (count > 0 && retryWaitIncrementMs > 0)
+ ? random.nextInt(count * retryWaitIncrementMs)
+ : minRetryWaitMs;
+ int maxRetryWaitMs;
+
+ // If we are time limiting only continue if we have enough time, based on the last duration
+ if (maxExecutionMs > 0)
+ {
+ long txnEndTime = System.currentTimeMillis();
+ long projectedEndTime = txnEndTime + (txnEndTime - txnStartTime);
+ if (projectedEndTime > endTime)
+ {
+ // Force the ceiling to be lowered and reject
+ endTime = 0;
+ throw new TooBusyException("Too busy to retry", e);
+ }
+ // Limit the wait duration to fit into the time we have left
+ maxRetryWaitMs = Math.min(this.maxRetryWaitMs, (int)(endTime - projectedEndTime));
+ }
+ else
+ {
+ maxRetryWaitMs = this.maxRetryWaitMs;
+ }
+ int sleepInterval = Math.min(maxRetryWaitMs, sleepIntervalRandom);
+ sleepInterval = Math.max(sleepInterval, minRetryWaitMs);
+ if (logger.isInfoEnabled() && !logger.isDebugEnabled())
+ {
+ String msg = String.format(
+ "Retrying %s: count %2d; wait: %1.1fs; msg: \"%s\"; exception: (%s)",
+ Thread.currentThread().getName(),
+ count, (double)sleepInterval/1000D,
+ retryCause.getMessage(),
+ retryCause.getClass().getName());
+ logger.info(msg);
+ }
+ try
+ {
+ Thread.sleep(sleepInterval);
+ }
+ catch (InterruptedException ie)
+ {
+ // Do nothing.
+ }
+ // Try again
+ continue;
+ }
+ else
+ {
+ // It was a 'bad' exception.
+ throw lastException;
+ }
}
}
+ // We've worn out our welcome and retried the maximum number of times.
+ // So, fail.
+ throw lastException;
+ }
+ finally
+ {
+ if (maxExecutionMs > 0)
+ {
+ synchronized (this)
+ {
+ txnCount--;
+ if(System.currentTimeMillis() > endTime)
+ {
+ // Lower the ceiling
+ if (txnCeiling == null || txnCeiling > txnCountWhenStarted - 1)
+ {
+ txnCeiling = Math.max(1, txnCountWhenStarted - 1);
+ }
+ }
+ else if (txnCeiling != null && txnCeiling < txnCountWhenStarted + 1)
+ {
+ // Raise the ceiling
+ txnCeiling = txnCountWhenStarted + 1;
+ }
+ }
+ }
}
- // We've worn out our welcome and retried the maximum number of times.
- // So, fail.
- throw lastException;
}
/**
diff --git a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelperTest.java b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelperTest.java
index 6f6c2a709b..2343342650 100644
--- a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelperTest.java
+++ b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelperTest.java
@@ -18,7 +18,12 @@
*/
package org.alfresco.repo.transaction;
+import java.util.Collections;
import java.util.ConcurrentModificationException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
@@ -40,6 +45,7 @@ import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
+import org.alfresco.util.Pair;
import org.apache.commons.lang.mutable.MutableInt;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -543,6 +549,178 @@ public class RetryingTransactionHelperTest extends TestCase
assertEquals("Should have been called exactly once", 1, callCount.intValue());
}
+ public void testTimeLimit()
+ {
+ final RetryingTransactionHelper txnHelper = new RetryingTransactionHelper();
+ txnHelper.setTransactionService(transactionService);
+ txnHelper.setMaxExecutionMs(3000);
+ final List caughtExceptions = Collections.synchronizedList(new LinkedList());
+
+ // Force ceiling of 2
+ runThreads(txnHelper, caughtExceptions, new Pair(2, 1000), new Pair(1, 5000));
+ if (caughtExceptions.size() > 0)
+ {
+ throw new RuntimeException("Unexpected exception", caughtExceptions.get(0));
+ }
+
+
+ // Try breaching ceiling
+ runThreads(txnHelper, caughtExceptions, new Pair(3, 1000));
+ assertTrue("Expected exception", caughtExceptions.size() > 0);
+ assertTrue("Excpected TooBusyException", caughtExceptions.get(0) instanceof TooBusyException);
+
+ // Stay within ceiling, forcing expansion
+ caughtExceptions.clear();
+ runThreads(txnHelper, caughtExceptions, new Pair(1, 1000), new Pair(1, 2000));
+ if (caughtExceptions.size() > 0)
+ {
+ throw new RuntimeException("Unexpected exception", caughtExceptions.get(0));
+ }
+
+ // Test expansion
+ caughtExceptions.clear();
+ runThreads(txnHelper, caughtExceptions, new Pair(3, 1000));
+ if (caughtExceptions.size() > 0)
+ {
+ throw new RuntimeException("Unexpected exception", caughtExceptions.get(0));
+ }
+
+ // Ensure expansion no too fast
+ caughtExceptions.clear();
+ runThreads(txnHelper, caughtExceptions, new Pair(5, 1000));
+ assertTrue("Expected exception", caughtExceptions.size() > 0);
+ assertTrue("Excpected TooBusyException", caughtExceptions.get(0) instanceof TooBusyException);
+
+ // Test contraction
+ caughtExceptions.clear();
+ runThreads(txnHelper, caughtExceptions, new Pair(2, 1000), new Pair(1, 5000));
+ if (caughtExceptions.size() > 0)
+ {
+ throw new RuntimeException("Unexpected exception", caughtExceptions.get(0));
+ }
+
+ // Try breaching new ceiling
+ runThreads(txnHelper, caughtExceptions, new Pair(3, 1000));
+ assertTrue("Expected exception", caughtExceptions.size() > 0);
+ assertTrue("Excpected TooBusyException", caughtExceptions.get(0) instanceof TooBusyException);
+
+ // Check retry limitation
+ long startTime = System.currentTimeMillis();
+ try
+ {
+ txnHelper.doInTransaction(new RetryingTransactionCallback()
+ {
+
+ public Void execute() throws Throwable
+ {
+ Thread.sleep(1000);
+ throw new ConcurrencyFailureException("Fake concurrency failure");
+ }
+ });
+ fail("Expected TooBusyException");
+ }
+ catch (TooBusyException e)
+ {
+ assertNotNull("Expected cause", e.getCause());
+ assertTrue("Too long", System.currentTimeMillis() < startTime + 5000);
+ }
+ }
+
+ private void runThreads(final RetryingTransactionHelper txnHelper, final List caughtExceptions,
+ Pair... countDurationPairs)
+ {
+ int threadCount = 0;
+ for (Pair pair : countDurationPairs)
+ {
+ threadCount += pair.getFirst();
+ }
+
+ final CountDownLatch endLatch = new CountDownLatch(threadCount);
+
+ class Callback implements RetryingTransactionCallback
+ {
+ private final CountDownLatch startLatch;
+ private final int duration;
+
+ public Callback(CountDownLatch startLatch, int duration)
+ {
+ this.startLatch = startLatch;
+ this.duration = duration;
+ }
+
+ public Void execute() throws Throwable
+ {
+ long endTime = System.currentTimeMillis() + duration;
+
+ // Signal that we've started
+ startLatch.countDown();
+
+ long duration = endTime - System.currentTimeMillis();
+ if (duration > 0)
+ {
+ Thread.sleep(duration);
+ }
+ return null;
+ }
+ }
+ ;
+ class Work implements Runnable
+ {
+ private final Callback callback;
+
+ public Work(Callback callback)
+ {
+ this.callback = callback;
+ }
+
+ public void run()
+ {
+ try
+ {
+ txnHelper.doInTransaction(callback);
+ }
+ catch (Throwable e)
+ {
+ caughtExceptions.add(e);
+ }
+ endLatch.countDown();
+ }
+ }
+ ;
+
+ // Fire the threads
+ int j = 0;
+ for (Pair pair : countDurationPairs)
+ {
+ CountDownLatch startLatch = new CountDownLatch(1);
+ Runnable work = new Work(new Callback(startLatch, pair.getSecond()));
+ for (int i = 0; i < pair.getFirst(); i++)
+ {
+ Thread thread = new Thread(work);
+ thread.setName(getName() + "-" + j++);
+ thread.setDaemon(true);
+ thread.start();
+ try
+ {
+ // Wait for the thread to get up and running. We need them starting in sequence
+ startLatch.await(60, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ }
+ }
+ }
+ // Wait for the threads to have finished
+ try
+ {
+ endLatch.await(60, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ }
+
+ }
+
/**
* Helper class to kill the session's DB connection
*/
diff --git a/source/java/org/alfresco/repo/transaction/TooBusyException.java b/source/java/org/alfresco/repo/transaction/TooBusyException.java
new file mode 100644
index 0000000000..3099fda25d
--- /dev/null
+++ b/source/java/org/alfresco/repo/transaction/TooBusyException.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2005-2010 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 received 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.transaction;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+
+/**
+ * An exception thrown by {@link RetryingTransactionHelper} when its maxExecutionMs property is set and there isn't
+ * enough capacity to execute / retry the transaction.
+ *
+ * @author dward
+ */
+public class TooBusyException extends AlfrescoRuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param msgId
+ */
+ public TooBusyException(String msgId)
+ {
+ super(msgId);
+ }
+
+ /**
+ * @param msgId
+ * @param msgParams
+ */
+ public TooBusyException(String msgId, Object[] msgParams)
+ {
+ super(msgId, msgParams);
+ }
+
+ /**
+ * @param msgId
+ * @param cause
+ */
+ public TooBusyException(String msgId, Throwable cause)
+ {
+ super(msgId, cause);
+ }
+
+ /**
+ * @param msgId
+ * @param msgParams
+ * @param cause
+ */
+ public TooBusyException(String msgId, Object[] msgParams, Throwable cause)
+ {
+ super(msgId, msgParams, cause);
+ }
+
+}
diff --git a/source/java/org/alfresco/service/cmr/search/CategoryService.java b/source/java/org/alfresco/service/cmr/search/CategoryService.java
index 0b3f7d8b7c..7489d56565 100644
--- a/source/java/org/alfresco/service/cmr/search/CategoryService.java
+++ b/source/java/org/alfresco/service/cmr/search/CategoryService.java
@@ -101,7 +101,38 @@ public interface CategoryService
*/
@Auditable(parameters = {"storeRef", "aspectName"})
public Collection getRootCategories(StoreRef storeRef, QName aspectName);
+
+ /**
+ * Looks up a category by name under its immediate parent. Index-independent so can be used for cluster-safe
+ * existence checks.
+ *
+ * @param parent
+ * the parent
+ * @param aspectName
+ * the aspect name
+ * @param name
+ * the category name
+ * @return the category child association reference
+ */
+ public ChildAssociationRef getCategory(NodeRef parent, QName aspectName, String name);
+ /**
+ * Gets root categories by name, optionally creating one if one does not exist. Index-independent so can be used for
+ * cluster-safe existence checks.
+ *
+ * @param storeRef
+ * the store ref
+ * @param aspectName
+ * the aspect name
+ * @param name
+ * the aspect name
+ * @param create
+ * should a category node be created if one does not exist?
+ * @return the root categories
+ */
+ public Collection getRootCategories(StoreRef storeRef, QName aspectName, String name,
+ boolean create);
+
/**
* Get all the types that represent categories
*