/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.jcr.Binary; import javax.jcr.ItemExistsException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.nodetype.NodeType; import javax.jcr.observation.EventIterator; import javax.jcr.observation.EventListener; import javax.jcr.query.Query; import javax.jcr.query.QueryManager; import javax.jcr.version.VersionHistory; import javax.jcr.version.VersionManager; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.modeshape.common.FixFor; import org.modeshape.common.util.IoUtil; import org.modeshape.common.util.StringUtil; import org.modeshape.jcr.api.JcrTools; import org.modeshape.jcr.api.observation.Event; /** * Unit test for various clustered repository scenarios. * * @author Horia Chiorean (hchiorea@redhat.com) */ public class ClusteredRepositoryTest { private static final Random RANDOM = new Random(); private String node1Id = "cnode_" + UUID.randomUUID().toString(); private String node2Id = "cnode_" + UUID.randomUUID().toString(); @BeforeClass public static void beforeClass() throws Exception { ClusteringHelper.bindJGroupsToLocalAddress(); } @AfterClass public static void afterClass() throws Exception { ClusteringHelper.removeJGroupsBindings(); } @Before public void before() throws Exception { // c3p0 is async, so it might take a bit until we can do this.... TestingUtil.waitUntilFolderCleanedUp("target/clustered"); } @Test @FixFor( "MODE-2409" ) public void shouldPropagateVersionableNodeInCluster() throws Exception { JcrRepository repository1 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered.json", node1Id); JcrSession session1 = repository1.login(); JcrRepository repository2 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered.json", node2Id); JcrSession session2 = repository2.login(); try { int eventTypes = Event.NODE_ADDED | Event.PROPERTY_ADDED; ClusteringEventListener listener = new ClusteringEventListener(3); session2.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, null, true, null, null, true); Node testNode = session1.getRootNode().addNode("testNode"); testNode.addMixin("mix:versionable"); String binary = "test string"; testNode.setProperty("binaryProperty", session1.getValueFactory().createBinary(binary.getBytes())); session1.save(); final String testNodePath = testNode.getPath(); session1.getWorkspace().getVersionManager().checkin(testNodePath); listener.waitForEvents(); List<String> paths = listener.getPaths(); assertTrue(paths.contains("/testNode")); assertTrue(paths.contains("/testNode/binaryProperty")); assertTrue(paths.contains("/testNode/jcr:uuid")); assertTrue(paths.contains("/testNode/jcr:baseVersion")); assertTrue(paths.contains("/testNode/jcr:primaryType")); assertTrue(paths.contains("/testNode/jcr:predecessors")); assertTrue(paths.contains("/testNode/jcr:mixinTypes")); assertTrue(paths.contains("/testNode/jcr:versionHistory")); assertTrue(paths.contains("/testNode/jcr:isCheckedOut")); // check whether the node can be found in the second repository ... try { session2.refresh(false); session2.getNode(testNodePath); // check that there are 2 versions (base & 1.0) VersionHistory versionHistory = session2.getWorkspace().getVersionManager().getVersionHistory("/testNode"); assertEquals(2, versionHistory.getAllVersions().getSize()); } catch (PathNotFoundException e) { fail("Should have found the '/testNode' created in other repository in this repository: "); } } finally { TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor( "MODE-2617" ) public void shouldCheckinNodesConcurrentlyInCluster() throws Exception { JcrRepository repository1 = TestingUtil.startRepositoryWithConfig("config/cluster/repo-config-clustered.json"); JcrSession session = repository1.login(); JcrRepository repository2 = TestingUtil.startRepositoryWithConfig("config/cluster/repo-config-clustered.json"); repository2.login(); int threadCount = 5; ExecutorService executorService = Executors.newFixedThreadPool(threadCount); try { Node testRoot = session.getRootNode().addNode("testRoot"); String testRootPath = testRoot.getPath(); IntStream.range(0, threadCount).forEach(i -> { try { Node parent = testRoot.addNode("parent-" + i); parent.addMixin(NodeType.MIX_VERSIONABLE); } catch (RepositoryException e) { throw new RuntimeException(e); } }); session.save(); session.logout(); List<Callable<String>> tasks = IntStream.range(0, threadCount) .mapToObj(i -> (Callable<String>) () -> { JcrSession taskSession = repository1.login(); try { VersionManager versionManager = taskSession.getWorkspace() .getVersionManager(); Node parent = taskSession.getNode(testRootPath + "/parent-" + i); versionManager.checkout(parent.getPath()); Node child = parent.addNode("child-" + i); child.addMixin(NodeType.MIX_VERSIONABLE); child.getSession().save(); versionManager.checkout(child.getPath()); versionManager.checkin(child.getPath()); versionManager.checkin(parent.getPath()); return child.getPath(); } finally { taskSession.logout(); } }) .collect(Collectors.toList()); List<String> expectedResults = IntStream.range(0, threadCount) .mapToObj(i -> testRootPath + "/parent-" + i + "/child-" + i) .collect(Collectors.toList()); List<String> actualResults = executorService.invokeAll(tasks) .stream() .map(result -> { try { return result.get(5, TimeUnit.SECONDS); } catch (Exception e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); Collections.sort(actualResults); assertEquals(expectedResults, actualResults); } finally { executorService.shutdown(); TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor( {"MODE-1618", "MODE-2830", "MODE-1733", "MODE-1943", "MODE-2051", "MODE-2369"} ) public void shouldPropagateNodeChangesInCluster() throws Exception { JcrRepository repository1 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered.json", node1Id); JcrSession session1 = repository1.login(); assertInitialContentPersisted(session1); JcrRepository repository2 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered.json", node2Id); JcrSession session2 = repository2.login(); assertInitialContentPersisted(session2); try { assertChangesVisibleViaListener(session1, session2); assertChangesArePropagatedInCluster(session1, session2, "node1"); assertChangesArePropagatedInCluster(session2, session1, "node2"); } finally { TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor( {"MODE-2077"} ) public void shouldPropagateNodeChangesInClusterWithDBLocking() throws Exception { JcrRepository repository1 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered-db-locking.json", node1Id); JcrSession session1 = repository1.login(); assertInitialContentPersisted(session1); JcrRepository repository2 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered-db-locking.json", node2Id); JcrSession session2 = repository2.login(); assertInitialContentPersisted(session2); try { assertChangesVisibleViaListener(session1, session2); assertChangesArePropagatedInCluster(session1, session2, "node1"); assertChangesArePropagatedInCluster(session2, session1, "node2"); } finally { TestingUtil.killRepositories(repository1, repository2); } } private void assertChangesVisibleViaListener(JcrSession session1, JcrSession session2) throws RepositoryException, InterruptedException { int eventTypes = Event.NODE_ADDED | Event.PROPERTY_ADDED; ClusteringEventListener listener = new ClusteringEventListener(3); session2.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, null, true, null, null, true); Node testNode = session1.getRootNode().addNode("testNode"); String binary = "test string"; testNode.setProperty("binaryProperty", session1.getValueFactory().createBinary(binary.getBytes())); session1.save(); final String testNodePath = testNode.getPath(); listener.waitForEvents(); List<String> paths = listener.getPaths(); assertEquals(3, paths.size()); assertTrue(paths.contains("/testNode")); assertTrue(paths.contains("/testNode/binaryProperty")); assertTrue(paths.contains("/testNode/jcr:primaryType")); // check whether the node can be found in the second repository ... Thread.sleep(500); try { session2.refresh(false); session2.getNode(testNodePath); } catch (PathNotFoundException e) { fail("Should have found the '/testNode' created in other repository in this repository: "); } } private void assertInitialContentPersisted( Session session ) throws RepositoryException { assertThat(session.getRootNode(), is(notNullValue())); assertThat(session.getNode("/Cars"), is(notNullValue())); assertThat(session.getNode("/Cars/Hybrid"), is(notNullValue())); assertThat(session.getNode("/Cars/Hybrid/Toyota Prius"), is(notNullValue())); assertThat(session.getWorkspace().getNodeTypeManager().getNodeType("car:Car"), is(notNullValue())); assertThat(session.getWorkspace().getNodeTypeManager().getNodeType("air:Aircraft"), is(notNullValue())); } @Test @FixFor("MODE-1683") public void shouldClusterJournals() throws Exception { JcrRepository repository1 = null; JcrRepository repository2 = null; try { // Start the first process completely ... repository1 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-indexes.json", node1Id); // Start the second process completely ... repository2 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-indexes.json", node2Id); assertEquals(repository1.runningState().journal().allRecords(false).size(), repository2.runningState().journal().allRecords(false).size()); // make 1 change which should be propagated in the cluster Session session1 = repository1.login(); session1.getRootNode().addNode("node1"); session1.save(); Thread.sleep(300); assertEquals(repository1.runningState().journal().allRecords(false).size(), repository2.runningState().journal().allRecords(false).size()); //shut down the 2nd repo's journal repository2.runningState().journal().shutdown(); // add another node to repo1 - this should be local to repo1 until repo2 comes up session1.getRootNode().addNode("node1"); session1.save(); session1.logout(); Thread.sleep(300); // start the 2nd repo's journal back up and check that it received the additional node from the 1st node. repository2.runningState().journal().start(); Thread.sleep(500); assertEquals(repository1.runningState().journal().allRecords(false).size(), repository2.runningState().journal().allRecords(false).size()); } finally { TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor( "MODE-1903" ) public void shouldReindexContentInClusterBasedOnTimestamp() throws Exception { JcrRepository repository1 = null; JcrRepository repository2 = null; try { repository1 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-indexes.json", node1Id); // Start the second process completely ... repository2 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-indexes.json", node2Id); RepositoryConfiguration repository2Config = repository2.getConfiguration(); // make 1 change which should be propagated in the cluster Session session1 = repository1.login(); Node node = session1.getRootNode().addNode("repo1_node1"); node.addMixin("mix:title"); node.setProperty("jcr:title", "title1"); session1.save(); Thread.sleep(300); // remote events should've been sent out and processed through the cluster causing indexes to be updated on both cluster nodes Session session2 = repository2.login(); Query query = session2.getWorkspace().getQueryManager().createQuery("select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title1'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node1").useIndex("titleIndex").validate(query, query.execute()); session2.logout(); // shut the second repo down long priorToShutdown = System.currentTimeMillis(); assertTrue("Second repository has not shutdown in the expected amount of time", repository2.shutdown().get(5, TimeUnit.SECONDS)); // add a new node in the first repo node = session1.getRootNode().addNode("repo1_node2"); node.addMixin("mix:title"); node.setProperty("jcr:title", "title2"); session1.save(); // start the 2nd repo back up - at the end of this the journals should be up-to-date repository2 = new JcrRepository(repository2Config); repository2.start(); Thread.sleep(300); // run a query to check that the index are not yet up-to-date session2 = repository2.login(); org.modeshape.jcr.api.Workspace workspace2 = (org.modeshape.jcr.api.Workspace)session2.getWorkspace(); query = workspace2.getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().rowCount(0).useIndex("titleIndex").validate(query, query.execute()); // reindex since before stopping the repository workspace2.reindexSince(priorToShutdown); // run a new query to check that we got the change query = workspace2.getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node2").useIndex("titleIndex").validate(query, query.execute()); } finally { TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor( "MODE-1903" ) public void shouldReindexContentInClusterIncrementally() throws Exception { JcrRepository repository1 = null; JcrRepository repository2 = null; try { // Start the first process completely ... repository1 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-incremental-indexes.json", node1Id); // Start the second process completely ... repository2 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-incremental-indexes.json", node2Id); RepositoryConfiguration repository2Config = repository2.getConfiguration(); // make 1 change which should be propagated in the cluster Session session1 = repository1.login(); Node node = session1.getRootNode().addNode("repo1_node1"); node.addMixin("mix:title"); node.setProperty("jcr:title", "title1"); session1.save(); Thread.sleep(300); // shut the second repo down assertTrue("Second repository has not shutdown in the expected amount of time", repository2.shutdown().get(3, TimeUnit.SECONDS)); // add a new node in the first repo node = session1.getRootNode().addNode("repo1_node2"); node.addMixin("mix:title"); node.setProperty("jcr:title", "title2"); session1.save(); // start the 2nd repo back up - at the end of this the journals should be up-to-date repository2 = new JcrRepository(repository2Config); repository2.start(); Thread.sleep(300); // run a query to check that the index are not yet up-to-date Session session2 = repository2.login(); org.modeshape.jcr.api.Workspace workspace2 = (org.modeshape.jcr.api.Workspace)session2.getWorkspace(); // run queries to check that reindexing has worked Query query = workspace2.getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node2").useIndex("titleIndex").validate(query, query.execute()); // shut the second repo down assertTrue("Second repository has not shutdown in the expected amount of time", repository2.shutdown().get(3, TimeUnit.SECONDS)); // remove a node from the first repo and change a value for the other node session1.getNode("/repo1_node2").remove(); session1.getNode("/repo1_node1").setProperty("jcr:title", "title1_edited"); session1.save(); // start the 2nd repo back up - at the end of this the journals should be up-to-date repository2 = new JcrRepository(repository2Config); repository2.start(); Thread.sleep(300); // run a query to check that the indexes are synced session2 = repository2.login(); workspace2 = (org.modeshape.jcr.api.Workspace)session2.getWorkspace(); query = workspace2.getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().rowCount(0).useIndex("titleIndex").validate(query, query.execute()); query = workspace2.getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title1'", Query.JCR_SQL2); validateQuery().rowCount(0).useIndex("titleIndex").validate(query, query.execute()); query = workspace2.getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title1_edited'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node1").useIndex("titleIndex").validate(query, query.execute()); } finally { TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor("MODE-2517") public void shouldPersistReindexedContentInCluster() throws Exception { JcrRepository repository1 = null; JcrRepository repository2 = null; try { // Start the first process completely ... repository1 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-indexes.json", node1Id); // Start the second process completely ... repository2 = TestingUtil.startClusteredRepositoryWithConfig( "config/cluster/repo-config-clustered-journal-indexes.json", node2Id); RepositoryConfiguration repository2Config = repository2.getConfiguration(); // make 1 change which should be propagated in the cluster Session session1 = repository1.login(); Node node = session1.getRootNode().addNode("repo1_node1"); node.addMixin("mix:title"); node.setProperty("jcr:title", "title1"); session1.save(); Thread.sleep(300); // check the indexes have been updated on the 2nd node Session session2 = repository2.login(); Query query = session2.getWorkspace().getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title1'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node1").useIndex("titleIndex").validate(query, query.execute()); // shut the second repo down TestingUtil.killRepository(repository2); // add a new node in the first repo node = session1.getRootNode().addNode("repo1_node2"); node.addMixin("mix:title"); node.setProperty("jcr:title", "title2"); session1.save(); // start the 2nd repo back up and force a reindexing repository2 = new JcrRepository(repository2Config); repository2.start(); Thread.sleep(300); // run a query to check that the index are not yet up-to-date session2 = repository2.login(); query = session2.getWorkspace().getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().rowCount(0).useIndex("titleIndex").validate(query, query.execute()); ((org.modeshape.jcr.api.Workspace)session2.getWorkspace()).reindex("/"); // run queries to check that reindexing has worked query = session2.getWorkspace().getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node2").useIndex("titleIndex").validate(query, query.execute()); // shut the second repo down TestingUtil.killRepository(repository2); // start the 2nd repo back up and check that reindexed data is still there repository2 = new JcrRepository(repository2Config); repository2.start(); Thread.sleep(300); session2 = repository2.login(); query = session2.getWorkspace().getQueryManager().createQuery( "select node.[jcr:path] from [mix:title] as node where node.[jcr:title] = 'title2'", Query.JCR_SQL2); validateQuery().hasNodesAtPaths("/repo1_node2").useIndex("titleIndex").validate(query, query.execute()); } finally { TestingUtil.killRepositories(repository1, repository2); } } @Test @FixFor({"MODE-1701", "MODE-2542"}) public void shouldNotStartRepositoryWithInvalidJGroupsConfiguration() throws Exception { try { TestingUtil.startRepositoryWithConfig("config/cluster/repo-config-invalid-clustering.json"); fail("Should reject invalid JGroups file..."); } catch (RuntimeException e) { //expected } } @Test public void shouldLockNodesCorrectlyInCluster() throws Exception { JcrRepository repository1 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered.json", node1Id); JcrSession session1 = repository1.login(); JcrRepository repository2 = TestingUtil.startClusteredRepositoryWithConfig("config/cluster/repo-config-clustered.json", node2Id); JcrSession session2 = repository2.login(); session1.getRootNode().addNode("folder", "nt:folder"); session1.save(); assertNotNull(session2.getNode("/folder")); session1.logout(); session2.logout(); CyclicBarrier cyclicBarrier = new CyclicBarrier(3); // add a number of file names to a list List<String> fileNames = IntStream.range(1, 100).mapToObj(i -> "file" + i).collect(Collectors.toList()); ExecutorService executors = Executors.newFixedThreadPool(2); try { // run a tasks on cluster node 1 which attempts to create each file from that list under the parent folder CompletableFuture<Boolean> taskOnClusterNode1 = CompletableFuture.supplyAsync(addFilesToFolder(repository1, cyclicBarrier, "node1", fileNames), executors); List<String> revertedList = new ArrayList<>(fileNames); Collections.reverse(revertedList); // run a tasks on cluster node 2 which attempts to create each file from that list in reverse under the parent folder CompletableFuture<Boolean> taskOnClusterNode2 = CompletableFuture.supplyAsync(addFilesToFolder(repository2, cyclicBarrier, "node2", revertedList), executors); cyclicBarrier.await(10, TimeUnit.SECONDS); boolean resultFromNode1 = taskOnClusterNode1.get(10, TimeUnit.SECONDS); boolean resultFromNode2 = taskOnClusterNode2.get(10, TimeUnit.SECONDS); // nt:folder does not allow SNS, so if locking working correctly only one of the 2 cluster nodes should've managed // to add all the files if (resultFromNode1 && resultFromNode2) { fail("Only one of the cluster nodes should've succeeded "); } String expectedClusterNode = resultFromNode1 ? "node1" : "node2"; JcrSession session = repository1.login(); Node folder = session.getNode("/folder"); for (NodeIterator nodeIterator = folder.getNodes(); nodeIterator.hasNext();) { Node file = nodeIterator.nextNode(); InputStream is = file.getNode("jcr:content").getProperty("jcr:data").getBinary().getStream(); String content = IoUtil.read(is); assertEquals(expectedClusterNode, content); } session.logout(); } finally { TestingUtil.killRepositories(repository1, repository2); executors.shutdownNow(); } } private Supplier<Boolean> addFilesToFolder(JcrRepository repository, CyclicBarrier cyclicBarrier, String clusterNodeId, List<String> fileNames) throws RepositoryException { JcrTools tools = new JcrTools(); JcrSession session = repository.login(); return () -> { try { fileNames.forEach(fileName -> { try { tools.uploadFile(session, "/folder/" + fileName, new ByteArrayInputStream(clusterNodeId.getBytes())); } catch (RepositoryException | IOException e) { throw new RuntimeException(e); } }); cyclicBarrier.await(); session.save(); return true; } catch (ItemExistsException ies) { return false; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } finally { if (session != null) { session.logout(); } } }; } private void assertChangesArePropagatedInCluster( Session process1Session, Session process2Session, String nodeName ) throws Exception { String nodeAbsPath = "/" + nodeName; String pathQuery = "select * from [nt:unstructured] as n where n.[jcr:path]='" + nodeAbsPath + "'"; // Add a jcr node in the 1st process and check it can be queried Node nodeProcess1 = process1Session.getRootNode().addNode(nodeName); process1Session.save(); queryAndExpectResults(process1Session, pathQuery, 1); Thread.sleep(300); // check that the custom jcr node created on the other process, was sent to this one assertNotNull(process2Session.getNode(nodeAbsPath)); queryAndExpectResults(process2Session, pathQuery, 1); // set a property of that node and check it's send through the cluster int minBinarySize = 4096; byte[] binaryData = new byte[minBinarySize + 2]; RANDOM.nextBytes(binaryData); nodeProcess1 = process1Session.getNode(nodeAbsPath); //create a normal string property nodeProcess1.setProperty("testProp", "test value"); //create a binary property nodeProcess1.setProperty("binaryProp", process1Session.getValueFactory().createBinary(new ByteArrayInputStream(binaryData))); //create a large string which should be stored as a binary String largeString = StringUtil.createString('a', minBinarySize + 2); nodeProcess1.setProperty("largeString", largeString); process1Session.save(); String propertyQuery = "select * from [nt:unstructured] as n where n.[testProp]='test value'"; queryAndExpectResults(process1Session, propertyQuery, 1); // wait a bit for state transfer to complete Thread.sleep(300); // check the property change was made in the indexes on the second node queryAndExpectResults(process2Session, propertyQuery, 1); //check the properties were sent across the cluster Node nodeProcess2 = process2Session.getNode(nodeAbsPath); assertEquals("test value", nodeProcess2.getProperty("testProp").getString()); Binary binary = nodeProcess2.getProperty("binaryProp").getBinary(); byte[] process2Data = IoUtil.readBytes(binary.getStream()); assertArrayEquals("Binary data not propagated in cluster", binaryData, process2Data); String process2LargeString = nodeProcess2.getProperty("largeString").getString(); assertEquals(largeString, process2LargeString); // Remove the node in the first process and check it's removed from the indexes across the cluster nodeProcess1 = process1Session.getNode(nodeAbsPath); nodeProcess1.remove(); process1Session.save(); queryAndExpectResults(process1Session, pathQuery, 0); Thread.sleep(300); // check the node was removed from the indexes in the second cluster node queryAndExpectResults(process2Session, pathQuery, 0); try { process2Session.getNode(nodeAbsPath); fail(nodeAbsPath + " not removed from other node in the cluster"); } catch (PathNotFoundException e) { //expected } } private void queryAndExpectResults(Session session, String queryString, int howMany) throws RepositoryException{ QueryManager queryManager = session.getWorkspace().getQueryManager(); Query query = queryManager.createQuery(queryString, Query.JCR_SQL2); NodeIterator nodes = query.execute().getNodes(); assertEquals(howMany, nodes.getSize()); } protected ValidateQuery.ValidationBuilder validateQuery() { return ValidateQuery.validateQuery().printDetail(false); } protected class ClusteringEventListener implements EventListener { private final List<String> paths; private final CountDownLatch eventsLatch; protected ClusteringEventListener( int expectedEventsCount ) { this.paths = new ArrayList<>(); this.eventsLatch = new CountDownLatch(expectedEventsCount); } @Override public void onEvent( EventIterator events ) { while (events.hasNext()) { Event event = (Event)events.nextEvent(); try { paths.add(event.getPath()); } catch (RepositoryException e) { throw new RuntimeException(e); } eventsLatch.countDown(); } } void waitForEvents() throws InterruptedException { assertTrue(eventsLatch.await(2, TimeUnit.SECONDS)); } public List<String> getPaths() { return paths; } } }