/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.jackrabbit.core.data; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.jcr.RepositoryException; import org.apache.jackrabbit.core.cluster.ClusterException; import org.apache.jackrabbit.core.cluster.ClusterNode; import org.apache.jackrabbit.core.cluster.SimpleClusterContext; import org.apache.jackrabbit.core.cluster.UpdateEventChannel; import org.apache.jackrabbit.core.cluster.UpdateEventListener; import org.apache.jackrabbit.core.config.ClusterConfig; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.journal.Journal; import org.apache.jackrabbit.core.journal.JournalFactory; import org.apache.jackrabbit.core.journal.MemoryJournal; import org.apache.jackrabbit.core.journal.MemoryJournal.MemoryRecord; import org.apache.jackrabbit.core.observation.EventState; import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager; import org.apache.jackrabbit.core.persistence.bundle.ConsistencyCheckerImpl; import org.apache.jackrabbit.core.persistence.check.ReportItem; import org.apache.jackrabbit.core.persistence.util.BLOBStore; import org.apache.jackrabbit.core.persistence.util.NodePropBundle; import org.apache.jackrabbit.core.state.ChangeLog; import org.apache.jackrabbit.core.state.ItemStateException; import org.apache.jackrabbit.core.state.NoSuchItemStateException; import org.apache.jackrabbit.core.state.NodeReferences; import org.apache.jackrabbit.spi.NameFactory; import org.apache.jackrabbit.spi.commons.name.NameConstants; import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; import junit.framework.TestCase; public class ConsistencyCheckerImplTest extends TestCase { private static final NameFactory nameFactory = NameFactoryImpl.getInstance(); /** Default sync delay: 5 seconds. */ private static final long SYNC_DELAY = 5000; private List<MemoryRecord> records = new ArrayList<MemoryRecord>(); private ClusterNode master; private ClusterNode slave; @Override public void setUp() throws Exception { super.setUp(); master = createClusterNode("master"); master.start(); slave = createClusterNode("slave"); slave.start(); } // Abandoned nodes are nodes that have a link to a parent but that // parent does not have a link back to the child public void testFixAbandonedNode() throws RepositoryException, ClusterException { NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); // node2 has a reference to node 1 as its parent, but node 1 doesn't have // a corresponding child node entry bundle2.setParentId(bundle1.getId()); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, master.createUpdateChannel("default")); // set up cluster event update listener final TestUpdateEventListener listener = new TestUpdateEventListener(); final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); slaveEventChannel.setListener(listener); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.ABANDONED, reportItem.getType()); assertEquals(bundle2.getId().toString(), reportItem.getNodeId()); checker.repair(); // node1 should now have a child node entry for node2 bundle1 = pm.loadBundle(bundle1.getId()); assertEquals(1, bundle1.getChildNodeEntries().size()); assertEquals(bundle2.getId(), bundle1.getChildNodeEntries().get(0).getId()); slave.sync(); // verify events were correctly broadcast to cluster assertNotNull("Cluster node did not receive update event", listener.changes); assertTrue("Expected node1 to be modified", listener.changes.isModified(bundle1.getId())); } public void testDoubleCheckAbandonedNode() throws RepositoryException { NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); // node2 has a reference to node 1 as its parent, but node 1 doesn't have // a corresponding child node entry bundle2.setParentId(bundle1.getId()); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.ABANDONED, reportItem.getType()); assertEquals(bundle2.getId().toString(), reportItem.getNodeId()); checker.doubleCheckErrors(); assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); // fix the error bundle1.addChildNodeEntry(nameFactory.create("", "test"), bundle2.getId()); checker.doubleCheckErrors(); assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); } /* * There was a bug where when there were multiple abandoned nodes by the same parent * only one of them was fixed. Hence this separate test case for this scenario. */ public void testFixMultipleAbandonedNodesBySameParent() throws RepositoryException { NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); NodePropBundle bundle3 = new NodePropBundle(new NodeId(1, 0)); // node2 and node3 have a reference to node1 as its parent, but node1 doesn't have // corresponding child node entries bundle2.setParentId(bundle1.getId()); bundle3.setParentId(bundle1.getId()); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2, bundle3)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); checker.check(null, false); checker.repair(); // node1 should now have child node entries for node2 and node3 bundle1 = pm.loadBundle(bundle1.getId()); assertEquals(2, bundle1.getChildNodeEntries().size()); assertEquals(bundle2.getId(), bundle1.getChildNodeEntries().get(0).getId()); assertEquals(bundle3.getId(), bundle1.getChildNodeEntries().get(1).getId()); } // Orphaned nodes are those nodes who's parent does not exist public void testAddOrphanedNodeToLostAndFound() throws RepositoryException, ClusterException { final NodeId lostAndFoundId = new NodeId(0, 0); NodePropBundle lostAndFound = new NodePropBundle(lostAndFoundId); // lost and found must be of type nt:unstructured lostAndFound.setNodeTypeName(NameConstants.NT_UNSTRUCTURED); final NodeId orphanedId = new NodeId(0, 1); NodePropBundle orphaned = new NodePropBundle(orphanedId); // set non-existent parent node id orphaned.setParentId(new NodeId(1, 0)); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(lostAndFound, orphaned)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, lostAndFoundId.toString(), master.createUpdateChannel("default")); // set up cluster event update listener final TestUpdateEventListener listener = new TestUpdateEventListener(); final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); slaveEventChannel.setListener(listener); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.ORPHANED, reportItem.getType()); assertEquals(orphanedId.toString(), reportItem.getNodeId()); checker.repair(); // orphan should have been added to lost+found lostAndFound = pm.loadBundle(lostAndFoundId); assertEquals(1, lostAndFound.getChildNodeEntries().size()); assertEquals(orphanedId, lostAndFound.getChildNodeEntries().get(0).getId()); orphaned = pm.loadBundle(orphanedId); assertEquals(lostAndFoundId, orphaned.getParentId()); slave.sync(); // verify events were correctly broadcast to cluster assertNotNull("Cluster node did not receive update event", listener.changes); assertTrue("Expected lostAndFound to be modified", listener.changes.isModified(lostAndFoundId)); assertTrue("Expected orphan to be modified", listener.changes.isModified(orphanedId)); } public void testDoubleCheckOrphanedNode() throws RepositoryException { NodePropBundle orphaned = new NodePropBundle(new NodeId(0, 1)); orphaned.setParentId(new NodeId(1, 0)); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(orphaned)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.ORPHANED, reportItem.getType()); assertEquals(orphaned.getId().toString(), reportItem.getNodeId()); checker.doubleCheckErrors(); assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); // fix the error NodePropBundle parent = new NodePropBundle(orphaned.getParentId()); pm.bundles.put(parent.getId(), parent); checker.doubleCheckErrors(); assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); } // Disconnected nodes are those nodes for which there are nodes // that have the node as its child, but the node itself does not // have those nodes as its parent public void testFixDisconnectedNode() throws RepositoryException, ClusterException { NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); NodePropBundle bundle3 = new NodePropBundle(new NodeId(1, 0)); // node1 has child node3 bundle1.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); // node2 also has child node3 bundle2.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); // node3 has node2 as parent bundle3.setParentId(bundle2.getId()); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2, bundle3)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, master.createUpdateChannel("default")); // set up cluster event update listener final TestUpdateEventListener listener = new TestUpdateEventListener(); final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); slaveEventChannel.setListener(listener); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.DISCONNECTED, reportItem.getType()); assertEquals(bundle1.getId().toString(), reportItem.getNodeId()); checker.repair(); bundle1 = pm.loadBundle(bundle1.getId()); bundle2 = pm.loadBundle(bundle2.getId()); bundle3 = pm.loadBundle(bundle3.getId()); // node3 should have been removed as child node entry of node1 assertEquals(0, bundle1.getChildNodeEntries().size()); // node3 should still be a child of node2 assertEquals(1, bundle2.getChildNodeEntries().size()); assertEquals(bundle2.getId(), bundle3.getParentId()); slave.sync(); // verify events were correctly broadcast to cluster assertNotNull("Cluster node did not receive update event", listener.changes); assertTrue("Expected node1 to be modified", listener.changes.isModified(bundle1.getId())); } public void testDoubleCheckDisonnectedNode() throws RepositoryException { NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); NodePropBundle bundle3 = new NodePropBundle(new NodeId(1, 0)); // node1 has child node3 bundle1.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); // node2 also has child node3 bundle2.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); // node3 has node2 as parent bundle3.setParentId(bundle2.getId()); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2, bundle3)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.DISCONNECTED, reportItem.getType()); assertEquals(bundle1.getId().toString(), reportItem.getNodeId()); checker.doubleCheckErrors(); assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); // fix the error bundle1.getChildNodeEntries().remove(0); checker.doubleCheckErrors(); assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); } public void testFixMissingNode() throws RepositoryException, ClusterException { NodePropBundle bundle = new NodePropBundle(new NodeId(0, 0)); bundle.addChildNodeEntry(nameFactory.create("", "test"), new NodeId(0, 1)); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, master.createUpdateChannel("default")); // set up cluster event update listener final TestUpdateEventListener listener = new TestUpdateEventListener(); final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); slaveEventChannel.setListener(listener); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.MISSING, reportItem.getType()); assertEquals(bundle.getId().toString(), reportItem.getNodeId()); checker.repair(); // node should have no child no entries assertTrue(bundle.getChildNodeEntries().isEmpty()); slave.sync(); // verify events were correctly broadcast to cluster assertNotNull("Cluster node did not receive update event", listener.changes); assertTrue("Expected node to be modified", listener.changes.isModified(bundle.getId())); } public void testDoubleCheckMissingNode() throws RepositoryException { NodePropBundle bundle = new NodePropBundle(new NodeId(0, 0)); final NodeId childNodeId = new NodeId(0, 1); bundle.addChildNodeEntry(nameFactory.create("", "test"), childNodeId); MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle)); ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); checker.check(null, false); Set<ReportItem> reportItems = checker.getReport().getItems(); assertEquals(1, reportItems.size()); ReportItem reportItem = reportItems.iterator().next(); assertEquals(ReportItem.Type.MISSING, reportItem.getType()); assertEquals(bundle.getId().toString(), reportItem.getNodeId()); checker.doubleCheckErrors(); assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); // fix the error NodePropBundle child = new NodePropBundle(childNodeId); pm.bundles.put(childNodeId, child); checker.doubleCheckErrors(); assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); } private ClusterNode createClusterNode(String id) throws Exception { final MemoryJournal journal = new MemoryJournal() { protected boolean syncAgainOnNewRecords() { return true; } }; JournalFactory jf = new JournalFactory() { public Journal getJournal(NamespaceResolver resolver) throws RepositoryException { return journal; } }; ClusterConfig cc = new ClusterConfig(id, SYNC_DELAY, jf); SimpleClusterContext context = new SimpleClusterContext(cc); journal.setRepositoryHome(context.getRepositoryHome()); journal.init(id, context.getNamespaceResolver()); journal.setRecords(records); ClusterNode clusterNode = new ClusterNode(); clusterNode.init(context); return clusterNode; } private static class MockPersistenceManager extends AbstractBundlePersistenceManager { private Map<NodeId, NodePropBundle> bundles = new LinkedHashMap<NodeId, NodePropBundle>(); private MockPersistenceManager(List<NodePropBundle> bundles) { for (NodePropBundle bundle : bundles) { this.bundles.put(bundle.getId(), bundle); } } public List<NodeId> getAllNodeIds(final NodeId after, final int maxCount) throws ItemStateException, RepositoryException { List<NodeId> allNodeIds = new ArrayList<NodeId>(); boolean add = after == null; for (NodeId nodeId : bundles.keySet()) { if (add) { allNodeIds.add(nodeId); } if (!add) { add = nodeId.equals(after); } } return allNodeIds; } @Override protected NodePropBundle loadBundle(final NodeId id) { return bundles.get(id); } @Override protected void evictBundle(final NodeId id) { } @Override protected void storeBundle(final NodePropBundle bundle) throws ItemStateException { bundles.put(bundle.getId(), bundle); } @Override protected void destroyBundle(final NodePropBundle bundle) throws ItemStateException { bundles.remove(bundle.getId()); } @Override protected void destroy(final NodeReferences refs) throws ItemStateException { } @Override protected void store(final NodeReferences refs) throws ItemStateException { } @Override protected BLOBStore getBlobStore() { return null; } public NodeReferences loadReferencesTo(final NodeId id) throws NoSuchItemStateException, ItemStateException { return null; } public boolean existsReferencesTo(final NodeId targetId) throws ItemStateException { return false; } } private static class TestUpdateEventListener implements UpdateEventListener { private ChangeLog changes; @Override public void externalUpdate(final ChangeLog changes, final List<EventState> events, final long timestamp, final String userData) throws RepositoryException { this.changes = changes; } } }