Index: extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/JdbcMetadataRepositoryTest.java =================================================================== --- extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/JdbcMetadataRepositoryTest.java (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/JdbcMetadataRepositoryTest.java (working copy) @@ -107,7 +107,7 @@ public class JdbcMetadataRepositoryTest { assertThat(workspace, is(notNullValue())); try { - TestEnvironment.executeDdl(this.source.getDataSource(), "create.ddl", this); + TestEnvironment.executeDdl(this.source.getDataSource(), "create.ddl", this); } catch (SQLException se) { } @@ -327,7 +327,11 @@ public class JdbcMetadataRepositoryTest { invalidSchemaName, nameFactory.create(JdbcMetadataRepository.TABLES_SEGMENT_NAME), nameFactory.create(identifier("sales"))); - assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + if (ignoresSchemaPatterns()) { + assertThat(workspace.getNode(invalidSchemaPath), is(notNullValue())); + } else { + assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + } } @Test @@ -392,7 +396,11 @@ public class JdbcMetadataRepositoryTest { nameFactory.create(JdbcMetadataRepository.TABLES_SEGMENT_NAME), nameFactory.create(identifier("sales")), nameFactory.create(identifier("amount"))); - assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + if (ignoresSchemaPatterns()) { + assertThat(workspace.getNode(invalidSchemaPath), is(notNullValue())); + } else { + assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + } } @Test @@ -453,6 +461,15 @@ public class JdbcMetadataRepositoryTest { assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); } + /** + * Not all databases will use the schema pattern when getting table and column metadata. + * + * @return true if the database ignores the schema patterns. + */ + protected boolean ignoresSchemaPatterns() { + return source.getDriverClassName().toLowerCase().contains("mysql"); + } + private Path pathFor( PathNode node ) { if (node == null) { return null; @@ -464,6 +481,7 @@ public class JdbcMetadataRepositoryTest { return pathFactory.create(node.getParent(), node.getName()); } + private Name nameFor( Property property ) { if (property == null) { return null; Index: extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/TestEnvironment.java =================================================================== --- extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/TestEnvironment.java (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/TestEnvironment.java (working copy) @@ -23,6 +23,9 @@ */ package org.modeshape.connector.meta.jdbc; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertThat; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -36,7 +39,8 @@ public class TestEnvironment { static Properties propertiesFor( Object testCase ) { Properties properties = new Properties(); - ClassLoader loader = testCase instanceof Class> ? ((Class>)testCase).getClassLoader() : testCase.getClass().getClassLoader(); + ClassLoader loader = testCase instanceof Class> ? ((Class>)testCase).getClassLoader() : testCase.getClass() + .getClassLoader(); try { properties.load(loader.getResourceAsStream("database.properties")); } catch (IOException e) { @@ -107,7 +111,7 @@ public class TestEnvironment { Object testCase ) throws Exception { Connection conn = null; Statement stmt = null; - InputStream is = null; + InputStream istream = null; BufferedReader reader = null; try { @@ -115,8 +119,9 @@ public class TestEnvironment { conn = dataSource.getConnection(); stmt = conn.createStatement(); - is = TestEnvironment.class.getResourceAsStream("/" + properties.getProperty("database") + "/" + fileName); - reader = new BufferedReader(new InputStreamReader(is)); + istream = TestEnvironment.class.getResourceAsStream("/" + properties.getProperty("database") + "/" + fileName); + assertThat(istream, is(notNullValue())); + reader = new BufferedReader(new InputStreamReader(istream)); /* * We have to send the DDL line-at-a-time because the MySQL driver doesn't like getting multiple DDL statements at once @@ -139,8 +144,8 @@ public class TestEnvironment { conn.close(); } catch (Exception ignore) { } - if (is != null) try { - is.close(); + if (istream != null) try { + istream.close(); } catch (Exception ignore) { } } Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/create.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/create.ddl (working copy) @@ -0,0 +1,28 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public +-- License along with this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +CREATE TABLE IF NOT EXISTS chain (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL) TYPE=INNODB; +CREATE TABLE IF NOT EXISTS area (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL, CHAIN_ID INT NOT NULL) TYPE=INNODB; +CREATE TABLE IF NOT EXISTS region (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL, AREA_ID INT NOT NULL) TYPE=INNODB; +CREATE TABLE IF NOT EXISTS district (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL, REGION_ID INT NOT NULL) TYPE=INNODB; + +CREATE TABLE IF NOT EXISTS sales (ID INT NOT NULL, SALES_DATE DATE NOT NULL, DISTRICT_ID INT NOT NULL, AMOUNT INT NULL) TYPE=INNODB; Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/drop.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/drop.ddl (working copy) @@ -0,0 +1,27 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public +-- License along with this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +DROP TABLE sales; +DROP TABLE district; +DROP TABLE region; +DROP TABLE area; +DROP TABLE chain; Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/create.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/create.ddl (working copy) @@ -0,0 +1,33 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public +-- License along with this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +CREATE TABLE chain (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL); +CREATE TABLE area (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL, CHAIN_ID INTEGER NOT NULL); +CREATE TABLE region (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL, AREA_ID INTEGER NOT NULL); +CREATE TABLE district (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL, REGION_ID INTEGER NOT NULL); + +CREATE TABLE sales (ID INTEGER NOT NULL, SALES_DATE DATE NOT NULL, DISTRICT_ID INTEGER NOT NULL, AMOUNT INTEGER NULL); +ALTER TABLE sales ADD CONSTRAINT PK_SALES PRIMARY KEY (ID, SALES_DATE); + +ALTER TABLE area ADD CONSTRAINT FK_CHAIN FOREIGN KEY(CHAIN_ID) REFERENCES CHAIN(ID) ON DELETE CASCADE; +ALTER TABLE region ADD CONSTRAINT FK_AREA FOREIGN KEY(AREA_ID) REFERENCES AREA(ID) ON DELETE CASCADE; +ALTER TABLE district ADD CONSTRAINT FK_REGION FOREIGN KEY(REGION_ID) REFERENCES REGION(ID) ON DELETE CASCADE; Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/drop.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/drop.ddl (working copy) @@ -0,0 +1,27 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public +-- License along with this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +DROP TABLE sales; +DROP TABLE district; +DROP TABLE region; +DROP TABLE area; +DROP TABLE chain; Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/JpaSource.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/JpaSource.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/JpaSource.java (working copy) @@ -141,6 +141,10 @@ public class JpaSource implements RepositorySource, ObjectFactory { */ protected static final boolean SUPPORTS_EVENTS = true; /** + * This source does not support automatic garbage collection. + */ + protected static final boolean SUPPORTS_AUTOMATIC_GARBAGE_COLLECTION = false; + /** * This source supports same-name-siblings. */ protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = true; @@ -333,7 +337,7 @@ public class JpaSource implements RepositorySource, ObjectFactory { DEFAULT_ALLOWS_UPDATES, SUPPORTS_EVENTS, DEFAULT_SUPPORTS_CREATING_WORKSPACES, - SUPPORTS_REFERENCES); + SUPPORTS_REFERENCES).withAutomaticGarbageCollection(SUPPORTS_AUTOMATIC_GARBAGE_COLLECTION); @Description( i18n = JpaConnectorI18n.class, value = "modelNamePropertyDescription" ) @Label( i18n = JpaConnectorI18n.class, value = "modelNamePropertyLabel" ) Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java (working copy) @@ -24,7 +24,6 @@ package org.modeshape.connector.store.jpa.model.simple; import java.security.NoSuchAlgorithmException; -import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityManager; @@ -32,7 +31,6 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Lob; -import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Query; import javax.persistence.Table; @@ -48,12 +46,33 @@ import org.modeshape.graph.property.PropertyType; */ @Entity @Table( name = "MODE_SIMPLE_LARGE_VALUES" ) -@NamedQueries( { - @NamedQuery( name = "LargeValueEntity.selectUnused", query = "select largeValue.hash from LargeValueEntity largeValue where largeValue.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ), - @NamedQuery( name = "LargeValueEntity.deleteAllUnused", query = "delete LargeValueEntity lve where lve.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ), - @NamedQuery( name = "LargeValueEntity.deleteIn", query = "delete LargeValueEntity lve where lve.hash in (:inValues)" )} ) +@NamedQuery( name = "LargeValueEntity.deleteAllUnused", query = "delete LargeValueEntity lve where lve.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ) public class LargeValueEntity { + /** + * The MySQL delete statement to remove all unused LargeValueEntity records. + *
+ * Normally, the "LargeValueEntity.deleteAllUnused" named query attempts to delete all of the LargeValueEntity records that + * have an SHA1 that is not in the set of SHA1 values in the "usages" intersect table. This works well on all DBMSes except + * for MySQL, which is unable to select from the table being deleted (see MODE-691). Therefore, we use a MySQL-specific native + * query to do this deletion. + *
+ *+ * This delete statement uses a left outer join between the LargeValueEntity and "usages" table and a criteria on the result + * to ensure that the only records returned are those without any "usages" records in the tuples. This works because a left + * outer join between tables A and B always contains all records from A, whether or not there are no corresponding records + * from B. And, if a criteria is added such that a column from B that can never be null is actually null, then we know the + * result will contain only those records from A that do not have a corresponding B record. In our case, this ends up + * deleting only those LargeValueEntity records that are not referenced in the "usages" table (i.e., they are not used). + *
+ *+ * We can do this native SQL because this only is used for the MySQL dialect. + *
+ */ + private static final String MYSQL_DELETE_ALL_UNUSED_LARGE_VALUE_ENTITIES = "DELETE lv FROM MODE_SIMPLE_LARGE_VALUES AS lv " + + "LEFT OUTER JOIN ModeShape_LARGEVALUE_USAGES AS lvu " + + "ON lv.SHA1 = lvu.largeValues_SHA1 WHERE lvu.ID IS NULL"; + @Id @Column( name = "SHA1", nullable = false, length = 40 ) private String hash; @@ -189,43 +208,29 @@ public class LargeValueEntity { * * @param manager the manager; never null * @param dialect the dialect - * @return the number of deleted large values + * @return whether all the unused records were able to be removed in this pass */ - @SuppressWarnings( "unchecked" ) - public static int deleteUnused( EntityManager manager, - String dialect ) { + public static boolean deleteUnused( EntityManager manager, + String dialect ) { assert manager != null; - int result = 0; - if (dialect != null && dialect.toLowerCase().indexOf("mysql") != -1) { - // Unfortunately, we cannot delete all the unused large values in a single statement - // because of MySQL (see MODE-691). Therefore, we need to do this in multiple steps: - // 1) Find the set of hashes that are not used anymore - // 2) Delete each of these rows, using bulk deletes with a small number (20) of hashes at a time - - Query select = manager.createNamedQuery("LargeValueEntity.selectUnused"); - List* Like its enclosing class, this class only survives for the lifetime of a single request (which may be a @@ -409,9 +420,6 @@ public class SimpleJpaRepository extends MapRepository { branch.deleteSubgraph(true); branch.close(); - // Delete unused large values ... - LargeValueEntity.deleteUnused(entityManager, dialect); - // And clean up the local cache by paths by brute force ... this.nodesByPath.clear(); } @@ -433,9 +441,6 @@ public class SimpleJpaRepository extends MapRepository { Query query = entityManager.createQuery("NodeEntity.deleteAllInWorkspace"); query.setParameter("workspaceId", workspaceId); query.executeUpdate(); - - // Delete unused large values ... - LargeValueEntity.deleteUnused(entityManager, dialect); } /* Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleRequestProcessor.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleRequestProcessor.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleRequestProcessor.java (working copy) @@ -15,6 +15,7 @@ import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathFactory; import org.modeshape.graph.property.PathNotFoundException; import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.CreateWorkspaceRequest; import org.modeshape.graph.request.InvalidRequestException; import org.modeshape.graph.request.ReadBranchRequest; @@ -161,4 +162,16 @@ public class SimpleRequestProcessor extends MapRequestProcessor { super.process(request); } + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CollectGarbageRequest) + */ + @Override + public void process( CollectGarbageRequest request ) { + boolean additionalPassRequired = !repository.collectGarbage(); + request.setAdditionalPassRequired(additionalPassRequired); + super.process(request); + } + } Index: extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JdbcConnectionTest.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JdbcConnectionTest.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JdbcConnectionTest.java (working copy) @@ -127,7 +127,12 @@ public class JdbcConnectionTest { connection = source.getConnection(); assertThat(source.getDialect(), is(notNullValue())); if (expectedDialect != null) { - assertThat(source.getDialect(), is(expectedDialect)); + if (expectedDialect.toLowerCase().contains("mysql")) { + // The MySQL auto-detected dialect may be different than the dialect that was explicitly set + assertThat(source.getDialect().toLowerCase().contains("mysql"), is(true)); + } else { + assertThat(source.getDialect(), is(expectedDialect)); + } } } finally { if (connection != null) connection.close(); Index: extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JpaConnectorWritingTest.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JpaConnectorWritingTest.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JpaConnectorWritingTest.java (working copy) @@ -24,17 +24,15 @@ package org.modeshape.connector.store.jpa; import java.util.UUID; +import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.common.statistic.Stopwatch; import org.modeshape.graph.Graph; import org.modeshape.graph.Location; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.test.WritableConnectorTest; import org.modeshape.graph.property.ReferentialIntegrityException; -import org.junit.Test; -/** - * @author Randall Hauch - */ public class JpaConnectorWritingTest extends WritableConnectorTest { private boolean isReferentialIntegrityEnforced = false; @@ -49,6 +47,8 @@ public class JpaConnectorWritingTest extends WritableConnectorTest { // Set the connection properties using the environment defined in the POM files ... JpaSource source = TestEnvironment.configureJpaSource("Test Repository", this); + source.setLargeValueSizeInBytes(100L); + // Override the inherited properties ... source.setReferentialIntegrityEnforced(isReferentialIntegrityEnforced); source.setCompressData(true); @@ -119,4 +119,49 @@ public class JpaConnectorWritingTest extends WritableConnectorTest { .into("/newUuids") .replacingExistingNodesWithSameUuids(); } + + @FixFor( {"MODE-1071", "MODE-1066"} ) + @Test + public void shouldSuccessfullyCollectGarbageOnePassAfterCreatingContentButNotDeletingAnyNodes() { + String workspaceName = "copyChildrenSource"; + tryCreatingAWorkspaceNamed(workspaceName); + + graph.useWorkspace(workspaceName); + String initialPath = ""; + int depth = 4; + int numChildrenPerNode = 3; + int numPropertiesPerNode = 10; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + useLargeValues = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + int passes = 1; + collectGarbage(passes); + } + + @FixFor( {"MODE-1071", "MODE-1066"} ) + @Test + public void shouldSuccessfullyCollectGarbageMultiplePassesAfterCreatingContentAndDeletingSome() { + String workspaceName = "copyChildrenSource"; + tryCreatingAWorkspaceNamed(workspaceName); + + graph.useWorkspace(workspaceName); + String initialPath = ""; + int depth = 4; + int numChildrenPerNode = 6; + int numPropertiesPerNode = 10; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + useLargeValues = true; + useUniqueLargeValues = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + collectGarbage(1); + + graph.delete("/node2").and(); + + int passes = 3; + collectGarbage(passes); + } } Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySourceCapabilities.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySourceCapabilities.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySourceCapabilities.java (working copy) @@ -24,6 +24,7 @@ package org.modeshape.graph.connector; import net.jcip.annotations.Immutable; +import org.modeshape.graph.request.CollectGarbageRequest; /** * The capabilities of a {@link RepositorySource}. This class can be used as is, or subclassed by a connector to define more @@ -74,6 +75,11 @@ public class RepositorySourceCapabilities { */ public static final boolean DEFAULT_SUPPORT_LOCKS = false; + /** + * The default support for automatic garbage collection is {@value} . + */ + public static final boolean DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION = true; + private final boolean sameNameSiblings; private final boolean updates; private final boolean events; @@ -82,6 +88,7 @@ public class RepositorySourceCapabilities { private final boolean locks; private final boolean queries; private final boolean searches; + private final boolean autoGarbageCollection; /** * Create a capabilities object using the defaults, . @@ -89,19 +96,20 @@ public class RepositorySourceCapabilities { public RepositorySourceCapabilities() { this(DEFAULT_SUPPORT_SAME_NAME_SIBLINGS, DEFAULT_SUPPORT_UPDATES, DEFAULT_SUPPORT_EVENTS, DEFAULT_SUPPORT_CREATING_WORKSPACES, DEFAULT_SUPPORT_REFERENCES, DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, - DEFAULT_SUPPORT_SEARCHES); + DEFAULT_SUPPORT_SEARCHES, DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION); } public RepositorySourceCapabilities( RepositorySourceCapabilities capabilities ) { this(capabilities.supportsSameNameSiblings(), capabilities.supportsUpdates(), capabilities.supportsEvents(), capabilities.supportsCreatingWorkspaces(), capabilities.supportsReferences(), capabilities.supportsLocks(), - capabilities.supportsQueries(), capabilities.supportsSearches()); + capabilities.supportsQueries(), capabilities.supportsSearches(), capabilities.supportsAutomaticGarbageCollection()); } public RepositorySourceCapabilities( boolean supportsSameNameSiblings, boolean supportsUpdates ) { this(supportsSameNameSiblings, supportsUpdates, DEFAULT_SUPPORT_EVENTS, DEFAULT_SUPPORT_CREATING_WORKSPACES, - DEFAULT_SUPPORT_REFERENCES, DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES); + DEFAULT_SUPPORT_REFERENCES, DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES, + DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION); } public RepositorySourceCapabilities( boolean supportsSameNameSiblings, @@ -110,7 +118,8 @@ public class RepositorySourceCapabilities { boolean supportsCreatingWorkspaces, boolean supportsReferences ) { this(supportsSameNameSiblings, supportsUpdates, supportsEvents, supportsCreatingWorkspaces, supportsReferences, - DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES); + DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES, + DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION); } public RepositorySourceCapabilities( boolean supportsSameNameSiblings, @@ -120,7 +129,8 @@ public class RepositorySourceCapabilities { boolean supportsReferences, boolean supportsLocks, boolean supportsQueries, - boolean supportsSearches ) { + boolean supportsSearches, + boolean supportsAutomaticGarbageCollection ) { this.sameNameSiblings = supportsSameNameSiblings; this.updates = supportsUpdates; @@ -130,6 +140,7 @@ public class RepositorySourceCapabilities { this.locks = supportsLocks; this.queries = supportsQueries; this.searches = supportsSearches; + this.autoGarbageCollection = supportsAutomaticGarbageCollection; } /** @@ -205,4 +216,117 @@ public class RepositorySourceCapabilities { public boolean supportsSearches() { return searches; } + + /** + * Return whether the source supports automatic garbage collection. If not, then the source expects explicit + * {@link CollectGarbageRequest} calls. + * + * @return true if automatic garbage collection is supported, or false if the source is not capable of automatically + * collecting all of its garbage and requires periodic, manual collection + */ + public boolean supportsAutomaticGarbageCollection() { + return autoGarbageCollection; + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsSameNameSiblings()}. + * + * @param sameNameSiblings the new value + * @return the new instance + */ + public RepositorySourceCapabilities withSameNameSiblings( boolean sameNameSiblings ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsUpdates()}. + * + * @param updates the new value + * @return the new instance + */ + public RepositorySourceCapabilities withUpdates( boolean updates ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsEvents()}. + * + * @param events the new value + * @return the new instance + */ + public RepositorySourceCapabilities withEvents( boolean events ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsCreatingWorkspaces()} + * . + * + * @param creatingWorkspaces the new value + * @return the new instance + */ + public RepositorySourceCapabilities withCreatingWorkspaces( boolean creatingWorkspaces ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsReferences()}. + * + * @param references the new value + * @return the new instance + */ + public RepositorySourceCapabilities withReferences( boolean references ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsLocks()}. + * + * @param locks the new value + * @return the new instance + */ + public RepositorySourceCapabilities withLocks( boolean locks ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsQueries()}. + * + * @param queries the new value + * @return the new instance + */ + public RepositorySourceCapabilities withQueries( boolean queries ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsSearches()}. + * + * @param searches the new value + * @return the new instance + */ + public RepositorySourceCapabilities withSearches( boolean searches ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for + * {@link #supportsAutomaticGarbageCollection()}. + * + * @param autoGarbageCollection the new value + * @return the new instance + */ + public RepositorySourceCapabilities withAutomaticGarbageCollection( boolean autoGarbageCollection ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + } Index: modeshape-graph/src/main/java/org/modeshape/graph/request/CollectGarbageRequest.java new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/CollectGarbageRequest.java (working copy) @@ -0,0 +1,109 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * Unless otherwise indicated, all code in ModeShape is licensed + * to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.request; + +/** + * Request that garbage collection be performed. Processors may disregard this request. + */ +public final class CollectGarbageRequest extends Request { + + private static final long serialVersionUID = 1L; + + private boolean additionalPassRequired = false; + + /** + * Create a request to destroy an existing workspace. + */ + public CollectGarbageRequest() { + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.Request#isReadOnly() + */ + @Override + public boolean isReadOnly() { + return false; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return 1; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals( Object obj ) { + if (obj == this) return true; + if (this.getClass().isInstance(obj)) return true; + return false; + } + + /** + * After collecting garbage during one pass (per this request), set whether additional passes are still required. + * + * @param additionalPassRequired true if this pass did not collect all known gargabe and additional passes are required, or + * false otherwise + * @throws IllegalStateException if the request is frozen + */ + public void setAdditionalPassRequired( boolean additionalPassRequired ) { + checkNotFrozen(); + this.additionalPassRequired = additionalPassRequired; + } + + /** + * Determine whether additional garbage collection passes are still required after this pass. In other words, if 'true', then + * this pass did not completely collect all known garbage. + * + * @return true if this pass did not collect all known gargabe and additional passes are required, or false otherwise + */ + public boolean isAdditionalPassRequired() { + return additionalPassRequired; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "collect garbage"; + } + + @Override + public RequestType getType() { + return RequestType.COLLECT_GARBAGE; + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/request/RequestType.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/RequestType.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/RequestType.java (working copy) @@ -8,6 +8,7 @@ public enum RequestType { COMPOSITE, CLONE_BRANCH, CLONE_WORKSPACE, + COLLECT_GARBAGE, COPY_BRANCH, CREATE_NODE, CREATE_WORKSPACE, Index: modeshape-graph/src/main/java/org/modeshape/graph/request/processor/LoggingRequestProcessor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/processor/LoggingRequestProcessor.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/processor/LoggingRequestProcessor.java (working copy) @@ -30,6 +30,7 @@ import org.modeshape.graph.GraphI18n; import org.modeshape.graph.request.AccessQueryRequest; import org.modeshape.graph.request.CloneBranchRequest; import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.CompositeRequest; import org.modeshape.graph.request.CopyBranchRequest; import org.modeshape.graph.request.CreateNodeRequest; @@ -433,6 +434,18 @@ public class LoggingRequestProcessor extends RequestProcessor { /** * {@inheritDoc} * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CollectGarbageRequest) + */ + @Override + public void process( CollectGarbageRequest request ) { + LOGGER.log(level, GraphI18n.executingRequest, request); + delegate.process(request); + LOGGER.log(level, GraphI18n.executedRequest, request); + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.Request) */ @Override Index: modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (working copy) @@ -51,6 +51,7 @@ import org.modeshape.graph.request.CacheableRequest; import org.modeshape.graph.request.ChangeRequest; import org.modeshape.graph.request.CloneBranchRequest; import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.CompositeRequest; import org.modeshape.graph.request.CopyBranchRequest; import org.modeshape.graph.request.CreateNodeRequest; @@ -233,6 +234,9 @@ public abstract class RequestProcessor { case CLONE_WORKSPACE: process((CloneWorkspaceRequest)request); break; + case COLLECT_GARBAGE: + process((CollectGarbageRequest)request); + break; case COPY_BRANCH: process((CopyBranchRequest)request); break; @@ -1024,6 +1028,18 @@ public abstract class RequestProcessor { } /** + * Process a request to collect garbage. + *
+ * The default implementation of this method does nothing. + *
+ * + * @param request the request + */ + public void process( CollectGarbageRequest request ) { + // do nothing by default + } + + /** * Close this processor, allowing it to clean up any open resources. */ public void close() { Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/test/AbstractConnectorTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/test/AbstractConnectorTest.java (revision 2666) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/test/AbstractConnectorTest.java (working copy) @@ -62,6 +62,7 @@ import org.modeshape.graph.property.Name; import org.modeshape.graph.property.Path; import org.modeshape.graph.property.Property; import org.modeshape.graph.property.ValueFormatException; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.ReadAllChildrenRequest; import org.modeshape.graph.request.ReadAllPropertiesRequest; import org.modeshape.graph.request.ReadNodeRequest; @@ -92,7 +93,10 @@ public abstract class AbstractConnectorTest { protected Observer observer; protected LinkedList+ * This method does all this work in the calling thread, blocking until all such requests have been issued and completed. It + * actually uses a queue, first enqueuing all RepositorySource instances that don't + * {@link RepositorySourceCapabilities#supportsAutomaticGarbageCollection() support automatic garbage collection}. It then + * pulls the first source from the queue, obtains a connection, submits a single {@link CollectGarbageRequest}, and + * re-enqueues the source {@link CollectGarbageRequest#isAdditionalPassRequired() if required}. However, this method never + * requests a source collect garbage more than {@link #MAXIMUM_NUMBER_OF_PASSES_PER_GC_RUN} times. + *
+ *+ * Thus a source can implement a garbage collection sweep in a manner that does not require excess amount of time so as to not + * block other requests. After that pass is completed, the source can simply denote in the CollectGarbageRequest whether at + * least one additional GC pass should be performed. + *
+ * + * @param problems the problems container in which any errors should be reported; if null, then any problems will be logged + */ + public void runGarbageCollection( Problems problems ) { + final Logger logger = Logger.getLogger(getClass()); + Queue