mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
Merged V2.0 to HEAD
5447: (From V1.4 5278, 5279, 5280, 5285, 5298, 5299, 5304): Hibernate session size management for large transactions git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5481 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -25,9 +25,11 @@
|
||||
package org.alfresco.repo.domain.hibernate;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -44,6 +46,7 @@ import org.alfresco.repo.domain.Store;
|
||||
import org.alfresco.repo.domain.StoreKey;
|
||||
import org.alfresco.repo.domain.Transaction;
|
||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
||||
import org.alfresco.repo.transaction.TransactionListenerAdapter;
|
||||
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
|
||||
import org.alfresco.service.cmr.repository.StoreRef;
|
||||
import org.alfresco.service.namespace.QName;
|
||||
@@ -51,6 +54,7 @@ import org.alfresco.service.transaction.TransactionService;
|
||||
import org.alfresco.util.BaseSpringTest;
|
||||
import org.alfresco.util.GUID;
|
||||
import org.hibernate.CacheMode;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
import org.hibernate.exception.GenericJDBCException;
|
||||
|
||||
@@ -64,7 +68,6 @@ import org.hibernate.exception.GenericJDBCException;
|
||||
public class HibernateNodeTest extends BaseSpringTest
|
||||
{
|
||||
private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/HibernateNodeTest";
|
||||
private static int i = 0;
|
||||
|
||||
private Store store;
|
||||
private Server server;
|
||||
@@ -360,6 +363,120 @@ public class HibernateNodeTest extends BaseSpringTest
|
||||
txn.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test demonstrates how entities are effectively rendered useless when the session
|
||||
* is cleared. The object itself will appear to behave properly, but it is only when
|
||||
* it comes to retrieving the associated values that one discovers that they were not
|
||||
* persisted at all. Uncomment at <b>UNCOMMENT FOR FAILURE</b> to see the effect in action.
|
||||
*/
|
||||
public void testPostCommitClearIssue() throws Exception
|
||||
{
|
||||
// commit the transaction
|
||||
setComplete();
|
||||
endTransaction();
|
||||
// Start a transaction explicitly
|
||||
TransactionService transactionService = (TransactionService) applicationContext.getBean("transactionComponent");
|
||||
UserTransaction txn = transactionService.getUserTransaction();
|
||||
|
||||
// We need a listener
|
||||
TestPostCommitClearIssueHelper listener = new TestPostCommitClearIssueHelper();
|
||||
try
|
||||
{
|
||||
txn.begin();
|
||||
|
||||
// Bind the listener
|
||||
AlfrescoTransactionSupport.bindListener(listener);
|
||||
|
||||
// Bind a list of node IDs into the transaction
|
||||
List<Long> nodeIds = new ArrayList<Long>(100);
|
||||
AlfrescoTransactionSupport.bindResource("node_ids", nodeIds);
|
||||
// Bind the session in, too
|
||||
Session session = getSession();
|
||||
AlfrescoTransactionSupport.bindResource("session", session);
|
||||
|
||||
// Make a whole lot of nodes with aspects and properties
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
// make a node
|
||||
Node node = new NodeImpl();
|
||||
node.setStore(store);
|
||||
node.setUuid(GUID.generate());
|
||||
node.setTypeQName(ContentModel.TYPE_CONTENT);
|
||||
Long nodeId = (Long) getSession().save(node);
|
||||
|
||||
// Record the ID
|
||||
nodeIds.add(nodeId);
|
||||
|
||||
// Now flush and clear
|
||||
/* UNCOMMENT FOR FAILURE */
|
||||
/* flushAndClear(); */
|
||||
|
||||
// add some aspects to the node
|
||||
Set<QName> aspects = node.getAspects();
|
||||
aspects.add(ContentModel.ASPECT_AUDITABLE);
|
||||
|
||||
// add some properties
|
||||
Map<QName, PropertyValue> properties = node.getProperties();
|
||||
properties.put(ContentModel.PROP_NAME, new PropertyValue(DataTypeDefinition.TEXT, "ABC"));
|
||||
}
|
||||
// Commit the transaction
|
||||
txn.commit();
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
try { txn.rollback(); } catch (Throwable ee) {}
|
||||
}
|
||||
// Did the listener find any issues?
|
||||
if (listener.err != null)
|
||||
{
|
||||
fail(listener.err);
|
||||
}
|
||||
}
|
||||
/** Helper class to test entities during transaction wind-down */
|
||||
private class TestPostCommitClearIssueHelper extends TransactionListenerAdapter
|
||||
{
|
||||
public String err = null;
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void beforeCommit(boolean readOnly)
|
||||
{
|
||||
// Get the session
|
||||
Session session = (Session) AlfrescoTransactionSupport.getResource("session");
|
||||
// Get the node IDs
|
||||
List<Long> nodeIds = (List<Long>) AlfrescoTransactionSupport.getResource("node_ids");
|
||||
// Check each node for the aspects and properties required
|
||||
int incorrectAspectCount = 0;
|
||||
int incorrectPropertyCount = 0;
|
||||
for (Long nodeId : nodeIds)
|
||||
{
|
||||
Node node = (Node) session.get(NodeImpl.class, nodeId);
|
||||
Set<QName> aspects = node.getAspects();
|
||||
Map<QName, PropertyValue> properties = node.getProperties();
|
||||
if (!aspects.contains(ContentModel.ASPECT_AUDITABLE))
|
||||
{
|
||||
// Missing the aspect
|
||||
incorrectAspectCount++;
|
||||
}
|
||||
if (!properties.containsKey(ContentModel.PROP_NAME))
|
||||
{
|
||||
// Missing property
|
||||
incorrectPropertyCount++;
|
||||
}
|
||||
}
|
||||
// What is the outcome?
|
||||
if (incorrectAspectCount > 0 || incorrectPropertyCount > 0)
|
||||
{
|
||||
this.err =
|
||||
"Checked " + nodeIds.size() + " nodes and found: \n" +
|
||||
" " + incorrectAspectCount + " missing aspects and \n" +
|
||||
" " + incorrectPropertyCount + " missing properties.";
|
||||
|
||||
}
|
||||
// Force a rollback anyway, just to stop an explosion of data
|
||||
throw new RuntimeException("ROLLBACK");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create some simple parent-child relationships and flush them. Then read them back in without
|
||||
|
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2007 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.hibernate;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.alfresco.util.resource.MethodResourceManager;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.stat.SessionStatistics;
|
||||
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
|
||||
|
||||
/**
|
||||
* A Hibernate-specific resource manager that ensures that the current <code>Session</code>'s
|
||||
* entity count doesn't exceed a given threshold.
|
||||
* <p/>
|
||||
* <b>NOTE: VERY IMPORTANT</b><br/>
|
||||
* Do not, under any circumstances, attach an instance of this class to an API that
|
||||
* passes stateful objects back and forth. There must be no <code>Session</code>-linked
|
||||
* objects up the stack from where this instance resides. Failure to observe this will
|
||||
* most likely result in data loss of a sporadic nature.
|
||||
*
|
||||
* @see org.alfresco.repo.domain.hibernate.HibernateNodeTest#testPostCommitClearIssue()
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class SessionSizeResourceManager extends HibernateDaoSupport implements MethodResourceManager
|
||||
{
|
||||
private static Log logger = LogFactory.getLog(SessionSizeResourceManager.class);
|
||||
|
||||
/** Default 1000 */
|
||||
private int threshold = 1000;
|
||||
|
||||
/**
|
||||
* Set the {@link Session#clear()} threshold. If the number of entities and collections in the
|
||||
* current session exceeds this number, then the session will be cleared. Have you read the
|
||||
* disclaimer?
|
||||
*
|
||||
* @param threshold the maximum number of entities and associations to keep in memory
|
||||
*
|
||||
* @see #threshold
|
||||
*/
|
||||
public void setThreshold(int threshold)
|
||||
{
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public void manageResources(
|
||||
Map<Method, MethodStatistics> methodStatsByMethod,
|
||||
long transactionElapsedTimeNs,
|
||||
Method currentMethod)
|
||||
{
|
||||
Session session = getSession(false);
|
||||
SessionStatistics stats = session.getStatistics();
|
||||
int entityCount = stats.getEntityCount();
|
||||
int collectionCount = stats.getCollectionCount();
|
||||
if ((entityCount + collectionCount) > threshold)
|
||||
{
|
||||
session.flush();
|
||||
session.clear();
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
String msg = String.format(
|
||||
"Cleared %5d entities and %5d collections from Hibernate Session",
|
||||
entityCount,
|
||||
collectionCount);
|
||||
logger.debug(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,6 +36,8 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.transaction.UserTransaction;
|
||||
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.content.MimetypeMap;
|
||||
import org.alfresco.repo.dictionary.DictionaryComponent;
|
||||
@@ -1042,12 +1044,6 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest
|
||||
ASSOC_TYPE_QNAME_TEST_CHILDREN,
|
||||
QName.createQName("pathA"),
|
||||
TYPE_QNAME_TEST_MULTIPLE_TESTER).getChildRef();
|
||||
// commit as we will be breaking the transaction in the test
|
||||
setComplete();
|
||||
endTransaction();
|
||||
|
||||
// each of these tests will be in a new transaction started by the NodeService
|
||||
|
||||
ArrayList<String> values = new ArrayList<String>(1);
|
||||
values.add("ABC");
|
||||
values.add("DEF");
|
||||
@@ -1062,15 +1058,26 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest
|
||||
nodeService.setProperty(nodeRef, PROP_QNAME_ANY_PROP_MULTIPLE, values);
|
||||
nodeService.setProperty(nodeRef, undeclaredPropQName, "ABC");
|
||||
nodeService.setProperty(nodeRef, undeclaredPropQName, values);
|
||||
// this should fail as we are passing multiple values into a non-any that is multiple=false
|
||||
|
||||
// commit as we will be breaking the transaction in the next test
|
||||
setComplete();
|
||||
endTransaction();
|
||||
|
||||
UserTransaction txn = transactionService.getUserTransaction();
|
||||
try
|
||||
{
|
||||
txn.begin();
|
||||
// this should fail as we are passing multiple values into a non-any that is multiple=false
|
||||
nodeService.setProperty(nodeRef, PROP_QNAME_STRING_PROP_SINGLE, values);
|
||||
}
|
||||
catch (DictionaryException e)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { txn.rollback(); } catch (Throwable e) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2007 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.node.db.hibernate;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.node.BaseNodeServiceTest;
|
||||
import org.alfresco.repo.node.db.DbNodeServiceImpl;
|
||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
||||
import org.alfresco.repo.transaction.TransactionResourceInterceptor;
|
||||
import org.alfresco.service.cmr.repository.NodeService;
|
||||
import org.alfresco.service.namespace.NamespaceService;
|
||||
import org.alfresco.service.namespace.QName;
|
||||
|
||||
/**
|
||||
* Tests the session size limiters in the context of a full stack.
|
||||
*
|
||||
* @see org.alfresco.util.resource.MethodResourceManager
|
||||
* @see org.alfresco.repo.transaction.TransactionResourceInterceptor
|
||||
* @see org.alfresco.repo.domain.hibernate.SessionSizeResourceManager
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class SessionSizeManagementTest extends BaseNodeServiceTest
|
||||
{
|
||||
private TransactionResourceInterceptor interceptor;
|
||||
private Method createNodesMethod;
|
||||
|
||||
public SessionSizeManagementTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
Class clazz = SessionSizeManagementTest.class;
|
||||
createNodesMethod = clazz.getMethod(
|
||||
"createNodes",
|
||||
new Class[] {NodeService.class, Integer.TYPE, Boolean.TYPE});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Instantiation failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config locations
|
||||
*
|
||||
* @return an array containing the config locations
|
||||
*/
|
||||
protected String[] getConfigLocations()
|
||||
{
|
||||
return new String[] {"session-size-test-context.xml"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeService getNodeService()
|
||||
{
|
||||
NodeService nodeService = (NodeService) applicationContext.getBean("testSessionSizeDbNodeService");
|
||||
return nodeService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetUpInTransaction() throws Exception
|
||||
{
|
||||
super.onSetUpInTransaction();
|
||||
// Get the interceptor for manual testing
|
||||
interceptor = (TransactionResourceInterceptor) applicationContext.getBean("testSessionSizeResourceInterceptor");
|
||||
}
|
||||
|
||||
/** Helper to create a given number of nodes using the provided service */
|
||||
public void createNodes(NodeService nodeService, int count, boolean manualFlush)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long beforeNs = System.nanoTime();
|
||||
nodeService.createNode(
|
||||
rootNodeRef,
|
||||
ContentModel.ASSOC_CHILDREN,
|
||||
QName.createQName(NamespaceService.ALFRESCO_URI, "child-" + i),
|
||||
ContentModel.TYPE_FOLDER);
|
||||
long deltaNs = System.nanoTime() - beforeNs;
|
||||
// Perform manual flush if necessary
|
||||
if (manualFlush)
|
||||
{
|
||||
interceptor.performManualCheck(createNodesMethod, deltaNs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final int LOAD_COUNT = 1000;
|
||||
/**
|
||||
* Create a bunch of nodes and see that the auto-clear is working
|
||||
*/
|
||||
public synchronized void testBulkLoad() throws Exception
|
||||
{
|
||||
NodeService nodeService = getNodeService();
|
||||
createNodes(nodeService, LOAD_COUNT, false);
|
||||
// We can't check the session size as this is dependent on machine speed
|
||||
|
||||
// Now flush integrity to be sure things are not broken
|
||||
AlfrescoTransactionSupport.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bunch of nodes and see that the manual clearing is working. The
|
||||
* original node service is used for this.
|
||||
*/
|
||||
public synchronized void testManualOperation() throws Exception
|
||||
{
|
||||
NodeService nodeService = (NodeService) applicationContext.getBean("dbNodeServiceImpl");
|
||||
if (!(nodeService instanceof DbNodeServiceImpl))
|
||||
{
|
||||
fail("This test requires the unwrapped raw DbNodeServiceImpl");
|
||||
}
|
||||
|
||||
createNodes(nodeService, LOAD_COUNT, true);
|
||||
// Check the session size
|
||||
int entityCount = getSession().getStatistics().getEntityCount();
|
||||
assertTrue("Manual flush: Entity count should be less than " + LOAD_COUNT, entityCount < LOAD_COUNT);
|
||||
|
||||
// Now flush integrity to be sure things are not broken
|
||||
AlfrescoTransactionSupport.flush();
|
||||
}
|
||||
}
|
@@ -71,6 +71,34 @@ public abstract class AlfrescoTransactionSupport
|
||||
|
||||
private static Log logger = LogFactory.getLog(AlfrescoTransactionSupport.class);
|
||||
|
||||
/**
|
||||
* @return Returns the system time when the transaction started, or -1 if there is no current transaction.
|
||||
*/
|
||||
public static long getTransactionStartTime()
|
||||
{
|
||||
/*
|
||||
* This method can be called outside of a transaction, so we can go direct to the synchronizations.
|
||||
*/
|
||||
TransactionSynchronizationImpl txnSynch =
|
||||
(TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH);
|
||||
if (txnSynch == null)
|
||||
{
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive())
|
||||
{
|
||||
// need to lazily register synchronizations
|
||||
return registerSynchronizations().getTransactionStartTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1; // not in a transaction
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return txnSynch.getTransactionStartTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique identifier associated with each transaction of each thread. Null is returned if
|
||||
* no transaction is currently active.
|
||||
@@ -428,6 +456,7 @@ public abstract class AlfrescoTransactionSupport
|
||||
*/
|
||||
private static class TransactionSynchronizationImpl extends TransactionSynchronizationAdapter
|
||||
{
|
||||
private long txnStartTime;
|
||||
private final String txnId;
|
||||
private final Set<TransactionalDao> daoServices;
|
||||
private final Set<IntegrityChecker> integrityCheckers;
|
||||
@@ -442,6 +471,7 @@ public abstract class AlfrescoTransactionSupport
|
||||
*/
|
||||
public TransactionSynchronizationImpl(String txnId)
|
||||
{
|
||||
this.txnStartTime = System.currentTimeMillis();
|
||||
this.txnId = txnId;
|
||||
daoServices = new HashSet<TransactionalDao>(3);
|
||||
integrityCheckers = new HashSet<IntegrityChecker>(3);
|
||||
@@ -450,6 +480,11 @@ public abstract class AlfrescoTransactionSupport
|
||||
resources = new HashMap<Object, Object>(17);
|
||||
}
|
||||
|
||||
public long getTransactionStartTime()
|
||||
{
|
||||
return txnStartTime;
|
||||
}
|
||||
|
||||
public String getTransactionId()
|
||||
{
|
||||
return txnId;
|
||||
|
@@ -65,15 +65,20 @@ public class AlfrescoTransactionSupportTest extends TestCase
|
||||
TransactionService transactionService = serviceRegistry.getTransactionService();
|
||||
UserTransaction txn = transactionService.getUserTransaction();
|
||||
assertNull("Thread shouldn't have a txn ID", AlfrescoTransactionSupport.getTransactionId());
|
||||
assertEquals("No transaction start time expected", -1, AlfrescoTransactionSupport.getTransactionStartTime());
|
||||
|
||||
// begine the txn
|
||||
// begin the txn
|
||||
txn.begin();
|
||||
String txnId = AlfrescoTransactionSupport.getTransactionId();
|
||||
assertNotNull("Expected thread to have a txn id", txnId);
|
||||
long txnStartTime = AlfrescoTransactionSupport.getTransactionStartTime();
|
||||
assertTrue("Expected a transaction start time", txnStartTime > 0);
|
||||
|
||||
// check that the txn id doesn't change
|
||||
// check that the txn id and time doesn't change
|
||||
String txnIdCheck = AlfrescoTransactionSupport.getTransactionId();
|
||||
assertEquals("Transaction ID changed on same thread", txnId, txnIdCheck);
|
||||
long txnStartTimeCheck = AlfrescoTransactionSupport.getTransactionStartTime();
|
||||
assertEquals("Transaction start time changed on same thread", txnStartTime, txnStartTimeCheck);
|
||||
|
||||
// begin a new, inner transaction
|
||||
{
|
||||
@@ -81,12 +86,19 @@ public class AlfrescoTransactionSupportTest extends TestCase
|
||||
|
||||
String txnIdInner = AlfrescoTransactionSupport.getTransactionId();
|
||||
assertEquals("Inner transaction not started, so txn ID should not change", txnId, txnIdInner);
|
||||
long txnStartTimeInner = AlfrescoTransactionSupport.getTransactionStartTime();
|
||||
assertEquals("Inner transaction not started, so txn start time should not change", txnStartTime, txnStartTimeInner);
|
||||
|
||||
// begin the nested txn
|
||||
txnInner.begin();
|
||||
// check the ID for the outer transaction
|
||||
txnIdInner = AlfrescoTransactionSupport.getTransactionId();
|
||||
assertNotSame("Inner txn ID must be different from outer txn ID", txnIdInner, txnId);
|
||||
// Check the time against the outer transaction
|
||||
txnStartTimeInner = AlfrescoTransactionSupport.getTransactionStartTime();
|
||||
assertTrue(
|
||||
"Inner transaction start time should be greater or equal (accuracy) to the outer's",
|
||||
txnStartTime <= txnStartTimeInner);
|
||||
|
||||
// rollback the nested txn
|
||||
txnInner.rollback();
|
||||
|
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2007 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.transaction;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.alfresco.util.resource.MethodResourceManager;
|
||||
import org.alfresco.util.resource.MethodResourceManager.MethodStatistics;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
/**
|
||||
* This interceptor gathers basic method call statistics and calls the
|
||||
* {@link org.alfresco.util.resource.MethodResourceManager resource managers} for
|
||||
* further processing. The resource managers are called <i>after</i> the method
|
||||
* invocations, but it doesn't matter too much since they are called on a time
|
||||
* frequency basis.
|
||||
* <p>
|
||||
* It acts as a sampling tool, but doesn't make any decisions or take any action
|
||||
* with regard system or transaction-related resources. All samples are stored
|
||||
* against the current transaction. If there is no current transaction then no
|
||||
* action will be taken with respect to the resource management.
|
||||
* <p>
|
||||
* The default is to activate after 10s and call through every 5s.
|
||||
* <p>
|
||||
* This class supports both interceptor-based calling as well as manual calling.
|
||||
* Long-running processes can call an this manually on every iteration and
|
||||
* get the same behaviour of regular calls to the resouce managers.
|
||||
*
|
||||
* @see org.alfresco.util.resource.MethodResourceManager
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class TransactionResourceInterceptor implements MethodInterceptor
|
||||
{
|
||||
private List<MethodResourceManager> methodResourceManagers;
|
||||
/** Default 10000ms (10s) */
|
||||
private long elapsedTimeBeforeActivationMillis = 10000L;
|
||||
/** Default 5000ms (5s) */
|
||||
private long resourceManagerCallFrequencyMillis = 5000L;
|
||||
/** When the last call to the resource managers was made by this instance */
|
||||
private volatile long lastCallMillis;
|
||||
|
||||
/**
|
||||
* A resource key unique to this interceptor. This avoids clashes with other instances up
|
||||
* and down the stack operating in the same transaction.
|
||||
*/
|
||||
private String resourceKey;
|
||||
|
||||
public TransactionResourceInterceptor()
|
||||
{
|
||||
resourceKey = "MethodStats" + super.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method-based resource managers that will be notified of the statistics.
|
||||
*
|
||||
* @param methodResourceManagers a list of resource managers - may be null or empty
|
||||
*/
|
||||
public void setMethodResourceManagers(List<MethodResourceManager> methodResourceManagers)
|
||||
{
|
||||
this.methodResourceManagers = methodResourceManagers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum number of seconds that a transaction must have been running for
|
||||
* before method sampling begins. The interceptor does nothing prior to this.
|
||||
*
|
||||
* @param elapsedTimeBeforeActivationMillis an initial idle time in milliseconds
|
||||
*/
|
||||
public void setElapsedTimeBeforeActivationMillis(long elapsedTimeBeforeActivationMillis)
|
||||
{
|
||||
this.elapsedTimeBeforeActivationMillis = elapsedTimeBeforeActivationMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the approximate time between calls to the
|
||||
* {@link #setMethodResourceManagers(List) registered resource managers}. This applies to this instance
|
||||
* of the interceptor and <u>not to the transaction</u>. If a single instance of this
|
||||
* class is used in multiple places, then the resource managers will still only get called at a steady
|
||||
* rate. This is mainly in order to streamline the interception prior to the activation phase, but suits
|
||||
* the resource managers since they get given the exact methods that were called anyway.
|
||||
*
|
||||
* @param resourceManagerCallFrequencyMillis an approximate time between calls to the resource managers
|
||||
*/
|
||||
public void setResourceManagerCallFrequencyMillis(long resourceManagerCallFrequencyMillis)
|
||||
{
|
||||
this.resourceManagerCallFrequencyMillis = resourceManagerCallFrequencyMillis;
|
||||
}
|
||||
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable
|
||||
{
|
||||
if (methodResourceManagers == null || methodResourceManagers.size() == 0)
|
||||
{
|
||||
// We just ignore everything
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
// Get the txn start time
|
||||
long txnStartTime = AlfrescoTransactionSupport.getTransactionStartTime();
|
||||
if (txnStartTime < 0)
|
||||
{
|
||||
// There is no transaction
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
// Check if the required time has passed
|
||||
long now = System.currentTimeMillis();
|
||||
long txnElapsedTime = (now - txnStartTime);
|
||||
if (txnElapsedTime < elapsedTimeBeforeActivationMillis)
|
||||
{
|
||||
// It's not been long enough
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
// We need to start timing the method calls
|
||||
Method calledMethod = invocation.getMethod();
|
||||
long beforeNs = System.nanoTime();
|
||||
Object ret = invocation.proceed();
|
||||
long deltaNs = System.nanoTime() - beforeNs;
|
||||
|
||||
// Get the method stats
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Method, MethodStatistics> methodStatsByMethod =
|
||||
(Map<Method, MethodStatistics>) AlfrescoTransactionSupport.getResource(resourceKey);
|
||||
if (methodStatsByMethod == null)
|
||||
{
|
||||
methodStatsByMethod = new HashMap<Method, MethodStatistics>(11);
|
||||
AlfrescoTransactionSupport.bindResource(resourceKey, methodStatsByMethod);
|
||||
}
|
||||
|
||||
// Update method stats
|
||||
MethodStatistics calledMethodStats = methodStatsByMethod.get(calledMethod);
|
||||
if (calledMethodStats == null)
|
||||
{
|
||||
calledMethodStats = new MethodStatistics();
|
||||
methodStatsByMethod.put(calledMethod, calledMethodStats);
|
||||
}
|
||||
calledMethodStats.accumulateNs(deltaNs);
|
||||
|
||||
// Check if we need to call the resource managers to clean up
|
||||
if ((now - lastCallMillis) >= resourceManagerCallFrequencyMillis)
|
||||
{
|
||||
for (MethodResourceManager resourceManager : methodResourceManagers)
|
||||
{
|
||||
resourceManager.manageResources(methodStatsByMethod, txnElapsedTime, calledMethod);
|
||||
}
|
||||
lastCallMillis = now;
|
||||
}
|
||||
|
||||
// Done
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative method allowing a manual call to check the resources. This is useful
|
||||
* in the cases where long running iterations don't necessarily pass through the
|
||||
* necessary API stack, or where the specific resources in hand can't be dealt with
|
||||
* blindly before and after resource management. The elapsed time should be that of the
|
||||
* iteration within the method (it is assumed that there won't be more than one).
|
||||
* <p>
|
||||
* If you have a loop in a method that doesn't call anything that can be intercepted
|
||||
* and handle safely, then get a pre-configured instance (usually from the application context)
|
||||
* and mimic the interceptor call.
|
||||
* <p>
|
||||
* You should get the <code>Method</code>, which is used for informational purposes, in a
|
||||
* single call when you calling code is loaded by the classloader. Introspecting every time
|
||||
* you wish to call this method is unnecessary.
|
||||
*
|
||||
* @param calledMethod the method that this check applies to
|
||||
* @param deltaNs the time in milliseconds that the repeated operation took
|
||||
*/
|
||||
public void performManualCheck(Method calledMethod, long deltaNs)
|
||||
{
|
||||
/*
|
||||
* This is mainly duplicated code, but it can be heavily used so nice patterns
|
||||
* are not preferable to speed of execution.
|
||||
*/
|
||||
|
||||
if (methodResourceManagers == null || methodResourceManagers.size() == 0)
|
||||
{
|
||||
// We just ignore everything
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the txn start time
|
||||
long txnStartTime = AlfrescoTransactionSupport.getTransactionStartTime();
|
||||
if (txnStartTime < 0)
|
||||
{
|
||||
// There is no transaction
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the required time has passed
|
||||
long now = System.currentTimeMillis();
|
||||
long txnElapsedTime = (now - txnStartTime);
|
||||
if (txnElapsedTime < elapsedTimeBeforeActivationMillis)
|
||||
{
|
||||
// It's not been long enough
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the method stats
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Method, MethodStatistics> methodStatsByMethod =
|
||||
(Map<Method, MethodStatistics>) AlfrescoTransactionSupport.getResource(resourceKey);
|
||||
if (methodStatsByMethod == null)
|
||||
{
|
||||
methodStatsByMethod = new HashMap<Method, MethodStatistics>(11);
|
||||
AlfrescoTransactionSupport.bindResource(resourceKey, methodStatsByMethod);
|
||||
}
|
||||
|
||||
// Update method stats
|
||||
MethodStatistics calledMethodStats = methodStatsByMethod.get(calledMethod);
|
||||
if (calledMethodStats == null)
|
||||
{
|
||||
calledMethodStats = new MethodStatistics();
|
||||
methodStatsByMethod.put(calledMethod, calledMethodStats);
|
||||
}
|
||||
calledMethodStats.accumulateNs(deltaNs);
|
||||
|
||||
// Check if we need to call the resource managers to clean up
|
||||
if ((now - lastCallMillis) >= resourceManagerCallFrequencyMillis)
|
||||
{
|
||||
for (MethodResourceManager resourceManager : methodResourceManagers)
|
||||
{
|
||||
resourceManager.manageResources(methodStatsByMethod, txnElapsedTime, calledMethod);
|
||||
}
|
||||
lastCallMillis = now;
|
||||
}
|
||||
// Done
|
||||
return;
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2007 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.util.resource;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A controller of system or in-transaction resources. Given a few statistics
|
||||
* regarding a method's call history, and using whatever other measurements
|
||||
* are needed, implementations will decide whether and how to clear up
|
||||
* sufficient system resources.
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public interface MethodResourceManager
|
||||
{
|
||||
/**
|
||||
* Helper class to carry basic method call statistics.
|
||||
*/
|
||||
public static class MethodStatistics
|
||||
{
|
||||
private long callCount;
|
||||
private long accumulatedTimeNs;
|
||||
public void accumulateNs(long durationNs)
|
||||
{
|
||||
accumulatedTimeNs += durationNs;
|
||||
callCount++;
|
||||
}
|
||||
public long getAccumulatedTimeNs()
|
||||
{
|
||||
return accumulatedTimeNs;
|
||||
}
|
||||
public long getCallCount()
|
||||
{
|
||||
return callCount;
|
||||
}
|
||||
/**
|
||||
* @return Returns the average call time in nanoseconds
|
||||
*/
|
||||
public double getAverageCallTimeNs()
|
||||
{
|
||||
if (callCount == 0)
|
||||
{
|
||||
return 0.0D;
|
||||
}
|
||||
return (double) accumulatedTimeNs / (double) callCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and free any required resources for an imminent. Details of the
|
||||
* current transaction and some gathered information about previous calls
|
||||
* to associated methods is also provided.
|
||||
*
|
||||
* @param methodStatsByMethod all known methods and their basic call stats
|
||||
* @param transactionElapsedTimeNs the elapsed time in the current transaction
|
||||
* @param currentMethod the method about to be called
|
||||
*/
|
||||
public void manageResources(
|
||||
Map<Method, MethodStatistics> methodStatsByMethod,
|
||||
long transactionElapsedTimeNs,
|
||||
Method currentMethod);
|
||||
}
|
72
source/test-resources/session-size-test-context.xml
Normal file
72
source/test-resources/session-size-test-context.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
|
||||
|
||||
<!-- Check that the resource interceptor is working -->
|
||||
<beans>
|
||||
|
||||
<import resource="classpath:alfresco/application-context.xml"/>
|
||||
|
||||
<bean id="testSessionSizeResourceInterceptor" class="org.alfresco.repo.transaction.TransactionResourceInterceptor" >
|
||||
<property name="methodResourceManagers">
|
||||
<list>
|
||||
<ref bean="sessionSizeResourceManager"></ref>
|
||||
</list>
|
||||
</property>
|
||||
<property name="elapsedTimeBeforeActivationMillis">
|
||||
<value>10000</value>
|
||||
</property>
|
||||
<property name="resourceManagerCallFrequencyMillis">
|
||||
<value>5000</value>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="sessionSizeResourceManager" class="org.alfresco.repo.domain.hibernate.SessionSizeResourceManager">
|
||||
<property name="sessionFactory">
|
||||
<ref bean="sessionFactory" />
|
||||
</property>
|
||||
<property name="threshold">
|
||||
<value>2000</value>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="testSessionSizeDbNodeService" class="org.springframework.aop.framework.ProxyFactoryBean">
|
||||
<property name="proxyInterfaces">
|
||||
<value>org.alfresco.service.cmr.repository.NodeService</value>
|
||||
</property>
|
||||
<property name="target">
|
||||
<ref bean="dbNodeService" />
|
||||
</property>
|
||||
<property name="interceptorNames">
|
||||
<list>
|
||||
<value>testSessionSizeResourceInterceptor</value>
|
||||
</list>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<!-- Point the IntegrityChecker to the correct NodeService -->
|
||||
<bean id="integrityChecker" class="org.alfresco.repo.node.integrity.IntegrityChecker" init-method="init">
|
||||
<property name="policyComponent">
|
||||
<ref bean="policyComponent"/>
|
||||
</property>
|
||||
<property name="dictionaryService">
|
||||
<ref bean="dictionaryService" />
|
||||
</property>
|
||||
<property name="nodeService">
|
||||
<ref bean="testSessionSizeDbNodeService" />
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<value>true</value>
|
||||
</property>
|
||||
<property name="traceOn">
|
||||
<value>false</value>
|
||||
</property>
|
||||
<property name="failOnViolation" >
|
||||
<value>true</value>
|
||||
</property>
|
||||
<property name="maxErrorsPerTransaction" >
|
||||
<value>5</value>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
|
||||
</beans>
|
Reference in New Issue
Block a user