diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties
index ee1920224c..ce1c98b6b9 100644
--- a/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties
+++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties
@@ -21,6 +21,11 @@ log4j.logger.org.alfresco.module.org_alfresco_module_rm.patch=info
#
#log4j.logger.org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService=debug
+#
+# Job debug
+#
+#log4j.logger.org.alfresco.module.org_alfresco_module_rm.job=debug
+
#
# Behaviour debug
#
diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml
index 072f0cce70..704b1015fd 100644
--- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml
+++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml
@@ -72,6 +72,15 @@
+
+
+
+
+ cutoff
+ retain
+
+
+
@@ -86,7 +95,7 @@
- 0 0/15 * * * ?
+ 0/30 * * * * ?
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java
index 650b54a783..0dc25b859c 100644
--- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuter.java
@@ -40,38 +40,110 @@ import org.apache.commons.logging.LogFactory;
/**
* The Disposition Lifecycle Job Finds all disposition action nodes which are
- * for "retain" or "cutOff" actions Where asOf > now OR
+ * for disposition actions specified Where asOf > now OR
* dispositionEventsEligible = true;
*
* Runs the cut off or retain action for
- * elligible records.
+ * eligible records.
*
* @author mrogers
+ * @author Roy Wetherall
*/
public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecuter
{
+ /** logger */
private static Log logger = LogFactory.getLog(DispositionLifecycleJobExecuter.class);
+
+ /** list of disposition actions to automatically execute */
+ private List dispositionActions;
+
+ /** query string */
+ private String query;
+ /** records management action service */
private RecordsManagementActionService recordsManagementActionService;
+ /** node service */
private NodeService nodeService;
+ /** search service */
private SearchService searchService;
+ /**
+ * List of disposition actions to automatically execute when eligible.
+ *
+ * @param dispositionActions disposition actions
+ */
+ public void setDispositionActions(List dispositionActions)
+ {
+ this.dispositionActions = dispositionActions;
+ }
+
+ /**
+ * @param recordsManagementActionService records management action service
+ */
public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService)
{
this.recordsManagementActionService = recordsManagementActionService;
}
+ /**
+ * @param nodeService node service
+ */
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
+ /**
+ * @param searchService search service
+ */
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
+
+ /**
+ * Get the search query string.
+ *
+ * @return job query string
+ */
+ private String getQuery()
+ {
+ if (query == null)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("+TYPE:\"rma:dispositionAction\" ");
+ sb.append("+(@rma\\:dispositionAction:(");
+
+ boolean bFirst = true;
+ for (String dispositionAction : dispositionActions)
+ {
+ if (bFirst)
+ {
+ bFirst = false;
+ }
+ else
+ {
+ sb.append(" OR ");
+ }
+
+ sb.append("\"").append(dispositionAction).append("\"");
+ }
+
+ sb.append("))");
+ sb.append("+ISNULL:\"rma:dispositionActionCompletedAt\" ");
+ sb.append("+( ");
+ sb.append("@rma\\:dispositionEventsEligible:true ");
+ sb.append("OR @rma\\:dispositionAsOf:[MIN TO NOW] ");
+ sb.append(") ");
+
+ query = sb.toString();
+ }
+
+ return query;
+ }
/**
* @see org.alfresco.module.org_alfresco_module_rm.job.RecordsManagementJobExecuter#execute()
@@ -81,64 +153,58 @@ public class DispositionLifecycleJobExecuter extends RecordsManagementJobExecute
try
{
logger.debug("Job Starting");
-
- StringBuilder sb = new StringBuilder();
- sb.append("+TYPE:\"rma:dispositionAction\" ");
- sb.append("+(@rma\\:dispositionAction:(\"cutoff\" OR \"retain\"))");
- sb.append("+ISNULL:\"rma:dispositionActionCompletedAt\" ");
- sb.append("+( ");
- sb.append("@rma\\:dispositionEventsEligible:true ");
- sb.append("OR @rma\\:dispositionAsOf:[MIN TO NOW] ");
- sb.append(") ");
-
- String query = sb.toString();
-
- ResultSet results = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,
- SearchService.LANGUAGE_LUCENE, query);
- List resultNodes = results.getNodeRefs();
- results.close();
-
-
- for (NodeRef node : resultNodes)
+
+ if (dispositionActions != null && !dispositionActions.isEmpty())
{
- final NodeRef currentNode = node;
-
- RetryingTransactionCallback processTranCB = new RetryingTransactionCallback()
+ // execute search
+ ResultSet results = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_LUCENE, getQuery());
+ List resultNodes = results.getNodeRefs();
+ results.close();
+
+ if (logger.isDebugEnabled())
{
- public Boolean execute() throws Throwable
+ logger.debug("Processing " + resultNodes.size() + " nodes");
+ }
+
+ // process search results
+ for (NodeRef node : resultNodes)
+ {
+ final NodeRef currentNode = node;
+
+ RetryingTransactionCallback processTranCB = new RetryingTransactionCallback()
{
- final String dispAction = (String) nodeService.getProperty(currentNode,
- RecordsManagementModel.PROP_DISPOSITION_ACTION);
-
- // Run "retain" and "cutoff" actions.
-
- if (dispAction != null && (dispAction.equalsIgnoreCase("cutoff") ||
- dispAction.equalsIgnoreCase("retain")))
+ public Boolean execute() throws Throwable
{
- ChildAssociationRef parent = nodeService.getPrimaryParent(currentNode);
- if (parent.getTypeQName().equals(RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION))
+ final String dispAction = (String) nodeService.getProperty(currentNode, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+
+ // Run disposition action
+ if (dispAction != null && dispositionActions.contains(dispAction))
{
- Map props = new HashMap(1);
- props.put(RMDispositionActionExecuterAbstractBase.PARAM_NO_ERROR_CHECK, Boolean.FALSE);
- recordsManagementActionService.executeRecordsManagementAction(parent.getParentRef(), dispAction, props);
- if (logger.isDebugEnabled())
+ ChildAssociationRef parent = nodeService.getPrimaryParent(currentNode);
+ if (parent.getTypeQName().equals(RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION))
{
- logger.debug("Processed action: " + dispAction + "on" + parent);
+ Map props = new HashMap(1);
+ props.put(RMDispositionActionExecuterAbstractBase.PARAM_NO_ERROR_CHECK, Boolean.FALSE);
+
+ // execute disposition action
+ recordsManagementActionService.executeRecordsManagementAction(parent.getParentRef(), dispAction, props);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Processed action: " + dispAction + "on" + parent);
+ }
}
}
- return null;
+
+ return Boolean.TRUE;
}
- return Boolean.TRUE;
+ };
+
+ // if exists
+ if (nodeService.exists(currentNode))
+ {
+ retryingTransactionHelper.doInTransaction(processTranCB);
}
- };
-
- /**
- * Now do the work, one action in each transaction
- */
-
- if (!nodeService.exists(currentNode))
- {
- retryingTransactionHelper.doInTransaction(processTranCB);
}
}
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java
new file mode 100644
index 0000000000..8d82002c19
--- /dev/null
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJobExecuterUnitTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.module.org_alfresco_module_rm.job;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.anyMap;
+import static org.mockito.Mockito.contains;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
+import org.alfresco.module.org_alfresco_module_rm.test.util.BaseUnitTest;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.search.ResultSet;
+import org.alfresco.service.cmr.search.SearchService;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+/**
+ * Disposition lifecycle job execution unit test.
+ *
+ * @author Roy Wetherall
+ * @since 2.2
+ */
+public class DispositionLifecycleJobExecuterUnitTest extends BaseUnitTest
+{
+ /** disposition actions */
+ private static final String CUTOFF = "cutoff";
+ private static final String RETAIN = "retain";
+ private static final String DESTROY = "destroy";
+
+ /** test query snipit */
+ private static final String QUERY = "\"" + CUTOFF + "\" OR \"" + RETAIN + "\"";
+
+ /** mocked result set */
+ @Mock ResultSet mockedResultSet;
+
+ /** disposition lifecycle job executer */
+ @InjectMocks DispositionLifecycleJobExecuter executer;
+
+ /**
+ * @see org.alfresco.module.org_alfresco_module_rm.test.util.BaseUnitTest#before()
+ */
+ @Override
+ @Before
+ public void before()
+ {
+ super.before();
+
+ // setup data
+ List dispositionActions = buildList(CUTOFF, RETAIN);
+ executer.setDispositionActions(dispositionActions);
+
+ // setup interactions
+ doReturn(mockedResultSet).when(mockedSearchService).query(eq(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE), eq(SearchService.LANGUAGE_LUCENE), anyString());
+ }
+
+ /**
+ * Helper method to verify that the query has been executed and closed
+ */
+ private void verifyQuery()
+ {
+ verify(mockedSearchService, times(1)).query(eq(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE), eq(SearchService.LANGUAGE_LUCENE), contains(QUERY));
+ verify(mockedResultSet, times(1)).getNodeRefs();
+ verify(mockedResultSet, times(1)).close();
+ }
+
+ /**
+ * When the are no results in query.
+ */
+ @Test
+ public void noResultsInQuery()
+ {
+ // given
+ doReturn(Collections.EMPTY_LIST).when(mockedResultSet).getNodeRefs();
+
+ // when
+ executer.executeImpl();
+
+ // then
+
+ // ensure the query is executed and closed
+ verifyQuery();
+
+ // ensure nothing else happens becuase we have no results
+ verifyZeroInteractions(mockedNodeService, mockedRecordFolderService, mockedRetryingTransactionHelper);
+ }
+
+ /**
+ * When the disposition actions do not match those that can be processed automatically.
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void dispositionActionDoesNotMatch()
+ {
+ // test data
+ NodeRef node1 = generateNodeRef();
+ NodeRef node2 = generateNodeRef();
+ List nodeRefs = buildList(node1, node2);
+
+ // given
+ doReturn(nodeRefs).when(mockedResultSet).getNodeRefs();
+ doReturn(DESTROY).when(mockedNodeService).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+ doReturn(DESTROY).when(mockedNodeService).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+
+ // when
+ executer.executeImpl();
+
+ // then
+
+ // ensure the query is executed and closed
+ verifyQuery();
+
+ // ensure work is executed in transaction for each node processed
+ verify(mockedNodeService, times(2)).exists(any(NodeRef.class));
+ verify(mockedRetryingTransactionHelper, times(2)).doInTransaction(any(RetryingTransactionCallback.class));
+
+ // ensure each node is process correctly
+ verify(mockedNodeService, times(1)).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+ verify(mockedNodeService, times(1)).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+
+ // ensure no more interactions
+ verifyNoMoreInteractions(mockedNodeService);
+ verifyZeroInteractions(mockedRecordsManagementActionService);
+
+ }
+
+ /**
+ * When a node does not exist
+ */
+ @Test
+ public void nodeDoesNotExist()
+ {
+ // test data
+ NodeRef node1 = generateNodeRef(null, false);
+ List nodeRefs = buildList(node1);
+
+ // given
+ doReturn(nodeRefs).when(mockedResultSet).getNodeRefs();
+
+ // when
+ executer.executeImpl();
+
+ // then
+
+ // ensure the query is executed and closed
+ verifyQuery();
+
+ // ensure the node exist check is made for the node
+ verify(mockedNodeService, times(1)).exists(any(NodeRef.class));
+
+ // ensure no more interactions
+ verifyNoMoreInteractions(mockedNodeService);
+ verifyZeroInteractions(mockedRecordsManagementActionService, mockedRetryingTransactionHelper);
+ }
+
+ /**
+ * When there are disposition actions eligible for processing
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void dispositionActionProcessed()
+ {
+ // test data
+ NodeRef node1 = generateNodeRef();
+ NodeRef node2 = generateNodeRef();
+ List nodeRefs = buildList(node1, node2);
+ NodeRef parent = generateNodeRef();
+ ChildAssociationRef parentAssoc = new ChildAssociationRef(ASSOC_NEXT_DISPOSITION_ACTION, parent, generateQName(), generateNodeRef());
+
+ // given
+ doReturn(nodeRefs).when(mockedResultSet).getNodeRefs();
+ doReturn(CUTOFF).when(mockedNodeService).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+ doReturn(RETAIN).when(mockedNodeService).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+ doReturn(parentAssoc).when(mockedNodeService).getPrimaryParent(any(NodeRef.class));
+
+ // when
+ executer.executeImpl();
+
+ // then
+
+ // ensure the query is executed and closed
+ verifyQuery();
+
+ // ensure work is executed in transaction for each node processed
+ verify(mockedNodeService, times(2)).exists(any(NodeRef.class));
+ verify(mockedRetryingTransactionHelper, times(2)).doInTransaction(any(RetryingTransactionCallback.class));
+
+ // ensure each node is process correctly
+ // node1
+ verify(mockedNodeService, times(1)).getProperty(node1, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+ verify(mockedNodeService, times(1)).getPrimaryParent(node1);
+ verify(mockedRecordsManagementActionService, times(1)).executeRecordsManagementAction(eq(parent), eq(CUTOFF), anyMap());
+ // node2
+ verify(mockedNodeService, times(1)).getProperty(node2, RecordsManagementModel.PROP_DISPOSITION_ACTION);
+ verify(mockedNodeService, times(1)).getPrimaryParent(node2);
+ verify(mockedRecordsManagementActionService, times(1)).executeRecordsManagementAction(eq(parent), eq(RETAIN), anyMap());
+
+ // ensure no more interactions
+ verifyNoMoreInteractions(mockedNodeService, mockedRecordsManagementActionService);
+ }
+}
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/AllUnitTestSuite.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/AllUnitTestSuite.java
index 509802fe48..e16ff323bd 100644
--- a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/AllUnitTestSuite.java
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/AllUnitTestSuite.java
@@ -21,6 +21,7 @@ package org.alfresco.module.org_alfresco_module_rm.test;
import org.alfresco.module.org_alfresco_module_rm.capability.declarative.condition.HoldCapabilityConditionUnitTest;
import org.alfresco.module.org_alfresco_module_rm.forms.RecordsManagementTypeFormFilterUnitTest;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldServiceImplUnitTest;
+import org.alfresco.module.org_alfresco_module_rm.job.DispositionLifecycleJobExecuterUnitTest;
import org.alfresco.module.org_alfresco_module_rm.jscript.app.evaluator.FrozenEvaluatorUnitTest;
import org.alfresco.module.org_alfresco_module_rm.jscript.app.evaluator.TransferEvaluatorUnitTest;
import org.alfresco.module.org_alfresco_module_rm.record.RecordMetadataBootstrapUnitTest;
@@ -44,6 +45,7 @@ import org.junit.runners.Suite.SuiteClasses;
{
RecordMetadataBootstrapUnitTest.class,
RecordsManagementTypeFormFilterUnitTest.class,
+ DispositionLifecycleJobExecuterUnitTest.class,
// services
RecordServiceImplUnitTest.class,
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseUnitTest.java
index 4c3afa8474..9389e5b40f 100644
--- a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseUnitTest.java
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseUnitTest.java
@@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.List;
import org.alfresco.model.ContentModel;
+import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService;
@@ -38,11 +39,14 @@ import org.alfresco.module.org_alfresco_module_rm.record.RecordService;
import org.alfresco.module.org_alfresco_module_rm.recordfolder.RecordFolderService;
import org.alfresco.module.org_alfresco_module_rm.util.ServiceBaseImpl;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.OwnableService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
@@ -78,18 +82,21 @@ public class BaseUnitTest implements RecordsManagementModel
protected NodeRef record;
/** core service mocks */
- @Mock(name="nodeService") protected NodeService mockedNodeService;
- @Mock(name="dictionaryService") protected DictionaryService mockedDictionaryService;
- @Mock(name="namespaceService") protected NamespaceService mockedNamespaceService;
- @Mock(name="identifierService") protected IdentifierService mockedIdentifierService;
- @Mock(name="permissionService") protected PermissionService mockedPermissionService;
- @Mock(name="ownableService") protected OwnableService mockedOwnableService;
+ @Mock(name="nodeService") protected NodeService mockedNodeService;
+ @Mock(name="dictionaryService") protected DictionaryService mockedDictionaryService;
+ @Mock(name="namespaceService") protected NamespaceService mockedNamespaceService;
+ @Mock(name="identifierService") protected IdentifierService mockedIdentifierService;
+ @Mock(name="permissionService") protected PermissionService mockedPermissionService;
+ @Mock(name="ownableService") protected OwnableService mockedOwnableService;
+ @Mock(name="searchService") protected SearchService mockedSearchService;
+ @Mock(name="retryingTransactionHelper") protected RetryingTransactionHelper mockedRetryingTransactionHelper;
/** rm service mocks */
- @Mock(name="filePlanService") protected FilePlanService mockedFilePlanService;
- @Mock(name="recordFolderService") protected RecordFolderService mockedRecordFolderService;
- @Mock(name="recordService") protected RecordService mockedRecordService;
- @Mock(name="holdService") protected HoldService mockedHoldService;
+ @Mock(name="filePlanService") protected FilePlanService mockedFilePlanService;
+ @Mock(name="recordFolderService") protected RecordFolderService mockedRecordFolderService;
+ @Mock(name="recordService") protected RecordService mockedRecordService;
+ @Mock(name="holdService") protected HoldService mockedHoldService;
+ @Mock(name="recordsManagementActionService") protected RecordsManagementActionService mockedRecordsManagementActionService;
/** application context mock */
@Mock(name="applicationContext") protected ApplicationContext mockedApplicationContext;
@@ -101,6 +108,7 @@ public class BaseUnitTest implements RecordsManagementModel
/**
* Test method setup
*/
+ @SuppressWarnings("unchecked")
@Before
public void before()
{
@@ -108,6 +116,19 @@ public class BaseUnitTest implements RecordsManagementModel
// setup application context
doReturn(mockedNodeService).when(mockedApplicationContext).getBean("nodeService");
+
+ // setup retrying transaction helper
+ Answer