diff --git a/config/alfresco/node-services-context.xml b/config/alfresco/node-services-context.xml index cbac88e91b..9734b48bb5 100644 --- a/config/alfresco/node-services-context.xml +++ b/config/alfresco/node-services-context.xml @@ -67,25 +67,6 @@ - - - - - - - - - - - - - - - - - - - @@ -146,5 +127,58 @@ + + + + + org.alfresco.service.cmr.repository.NodeService + + + + + + + sessionSizeResourceInterceptor + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10000 + + + 5000 + + + + + + + + 2000 + + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index ac671dd5ee..2a45770328 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -44,7 +44,9 @@ lucene.query.maxClauses=10000 # Events are generated as nodes are changed, this is the maximum size of the queue used to coalesce event # When this size is reached the lists of nodes will be indexed # -lucene.indexer.batchSize=1000 +# http://issues.alfresco.com/browse/AR-1280: Setting this high is the workaround as of 1.4.3. +# +lucene.indexer.batchSize=1000000 # # Lucene index min merge docs - the in memory size of the index # diff --git a/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java b/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java index 9d27e174d7..e0e98178cd 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java +++ b/source/java/org/alfresco/repo/domain/hibernate/HibernateNodeTest.java @@ -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 UNCOMMENT FOR FAILURE 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 nodeIds = new ArrayList(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 aspects = node.getAspects(); + aspects.add(ContentModel.ASPECT_AUDITABLE); + + // add some properties + Map 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 nodeIds = (List) 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 aspects = node.getAspects(); + Map 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 diff --git a/source/java/org/alfresco/repo/domain/hibernate/SessionSizeResourceManager.java b/source/java/org/alfresco/repo/domain/hibernate/SessionSizeResourceManager.java new file mode 100644 index 0000000000..aa0026ad9c --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/SessionSizeResourceManager.java @@ -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 Session's + * entity count doesn't exceed a given threshold. + *

+ * NOTE: VERY IMPORTANT
+ * 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 Session-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 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); + } + } + } +} diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java index 0b271c7f84..4eabd8ddc9 100644 --- a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java @@ -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 values = new ArrayList(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) {} + } } /** diff --git a/source/java/org/alfresco/repo/node/db/hibernate/SessionSizeManagementTest.java b/source/java/org/alfresco/repo/node/db/hibernate/SessionSizeManagementTest.java new file mode 100644 index 0000000000..7f69a81292 --- /dev/null +++ b/source/java/org/alfresco/repo/node/db/hibernate/SessionSizeManagementTest.java @@ -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(); + } +} diff --git a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java index 893ecb885d..64988ad93a 100644 --- a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java +++ b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupport.java @@ -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 daoServices; private final Set integrityCheckers; @@ -442,6 +471,7 @@ public abstract class AlfrescoTransactionSupport */ public TransactionSynchronizationImpl(String txnId) { + this.txnStartTime = System.currentTimeMillis(); this.txnId = txnId; daoServices = new HashSet(3); integrityCheckers = new HashSet(3); @@ -450,6 +480,11 @@ public abstract class AlfrescoTransactionSupport resources = new HashMap(17); } + public long getTransactionStartTime() + { + return txnStartTime; + } + public String getTransactionId() { return txnId; diff --git a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java index 3b55349df3..505d7aa08a 100644 --- a/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java +++ b/source/java/org/alfresco/repo/transaction/AlfrescoTransactionSupportTest.java @@ -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(); diff --git a/source/java/org/alfresco/repo/transaction/TransactionResourceInterceptor.java b/source/java/org/alfresco/repo/transaction/TransactionResourceInterceptor.java new file mode 100644 index 0000000000..b1a2af12cd --- /dev/null +++ b/source/java/org/alfresco/repo/transaction/TransactionResourceInterceptor.java @@ -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 after the method + * invocations, but it doesn't matter too much since they are called on a time + * frequency basis. + *

+ * 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. + *

+ * The default is to activate after 10s and call through every 5s. + *

+ * 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 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 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 not to the transaction. 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 methodStatsByMethod = + (Map) AlfrescoTransactionSupport.getResource(resourceKey); + if (methodStatsByMethod == null) + { + methodStatsByMethod = new HashMap(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). + *

+ * 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. + *

+ * You should get the Method, 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 methodStatsByMethod = + (Map) AlfrescoTransactionSupport.getResource(resourceKey); + if (methodStatsByMethod == null) + { + methodStatsByMethod = new HashMap(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; + } +} diff --git a/source/java/org/alfresco/util/resource/MethodResourceManager.java b/source/java/org/alfresco/util/resource/MethodResourceManager.java new file mode 100644 index 0000000000..bc9a449f30 --- /dev/null +++ b/source/java/org/alfresco/util/resource/MethodResourceManager.java @@ -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 methodStatsByMethod, + long transactionElapsedTimeNs, + Method currentMethod); +} diff --git a/source/test-resources/session-size-test-context.xml b/source/test-resources/session-size-test-context.xml new file mode 100644 index 0000000000..dc5e7758f1 --- /dev/null +++ b/source/test-resources/session-size-test-context.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + 10000 + + + 5000 + + + + + + + + + 2000 + + + + + + org.alfresco.service.cmr.repository.NodeService + + + + + + + testSessionSizeResourceInterceptor + + + + + + + + + + + + + + + + + true + + + false + + + true + + + 5 + + + + + \ No newline at end of file