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