You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by st...@apache.org on 2015/08/24 13:25:49 UTC

svn commit: r1697355 [2/2] - in /jackrabbit/oak/trunk/oak-core: ./ src/main/java/org/apache/jackrabbit/oak/plugins/document/ src/test/java/org/apache/jackrabbit/oak/plugins/document/

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewDocumentTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewDocumentTest.java?rev=1697355&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewDocumentTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewDocumentTest.java Mon Aug 24 11:25:48 2015
@@ -0,0 +1,239 @@
+/*
+ * 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.oak.plugins.document;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
+import org.junit.Test;
+
+import junitx.util.PrivateAccessor;
+
+public class ClusterViewDocumentTest {
+
+    @Test
+    public void testSetToCsv() throws Throwable {
+        // test null and empty first
+        assertNull(callSetToCsv(null));
+        assertNull(callSetToCsv(new HashSet<Integer>()));
+
+        // test one element
+        Set<Integer> input = new TreeSet<Integer>();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 100; i < 199; i++) {
+            input.add(i);
+            if (sb.length() != 0) {
+                sb.append(",");
+            }
+            sb.append(String.valueOf(i));
+            assertEquals(sb.toString(), callSetToCsv(input));
+        }
+    }
+
+    private String callSetToCsv(Set<Integer> input) throws Throwable {
+        return (String) PrivateAccessor.invoke(ClusterViewDocument.class, "setToCsv", new Class[] { Set.class },
+                new Object[] { input });
+    }
+
+    @Test
+    public void testArrayToCsv() throws Throwable {
+        // nulls first
+        assertNull(callArrayToCsv(null));
+        assertNull(callArrayToCsv(new Integer[0]));
+
+        // one element only
+        assertEquals("101", callArrayToCsv(new Integer[] { 101 }));
+        assertEquals("101,102", callArrayToCsv(new Integer[] { 101, 102 }));
+        assertEquals("101,102,103", callArrayToCsv(new Integer[] { 101, 102, 103 }));
+        assertEquals("101,102,103,104", callArrayToCsv(new Integer[] { 101, 102, 103, 104 }));
+    }
+
+    private String callArrayToCsv(Integer[] input) throws Throwable {
+        return (String) PrivateAccessor.invoke(ClusterViewDocument.class, "arrayToCsv", new Class[] { Integer[].class },
+                new Object[] { input });
+    }
+
+    @Test
+    public void testConstructor() {
+        ClusterViewDocument doc = new ClusterViewBuilder(1, "2", 3).active(3, 4).asDoc();
+        assertNotNull(doc);
+        assertEquals("2", doc.getClusterViewId());
+        assertEquals(0, doc.getRecoveringIds().size());
+        assertEquals(0, doc.getInactiveIds().size());
+        assertEquals(2, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(3));
+        assertTrue(doc.getActiveIds().contains(4));
+
+        doc = new ClusterViewBuilder(1, "2", 3).active(3, 4).backlogs(5).inactive(5, 6).asDoc();
+        assertNotNull(doc);
+        assertEquals("2", doc.getClusterViewId());
+        assertEquals(0, doc.getRecoveringIds().size());
+        assertEquals(2, doc.getInactiveIds().size());
+        assertEquals(2, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(3));
+        assertTrue(doc.getActiveIds().contains(4));
+        assertTrue(doc.getInactiveIds().contains(5));
+        assertTrue(doc.getInactiveIds().contains(6));
+
+        doc = new ClusterViewBuilder(11, "x", 4).active(3, 4, 5).recovering(6).inactive(7, 8).asDoc();
+        assertNotNull(doc);
+        assertEquals(11, doc.getViewSeqNum());
+        assertEquals("x", doc.getClusterViewId());
+        assertEquals(1, doc.getRecoveringIds().size());
+        assertEquals(2, doc.getInactiveIds().size());
+        assertEquals(3, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(3));
+        assertTrue(doc.getActiveIds().contains(4));
+        assertTrue(doc.getActiveIds().contains(5));
+        assertTrue(doc.getRecoveringIds().contains(6));
+        assertTrue(doc.getInactiveIds().contains(7));
+        assertTrue(doc.getInactiveIds().contains(8));
+
+    }
+
+    @Test
+    public void testReadUpdate() throws Exception {
+        final int localClusterId = 11;
+        final DocumentNodeStore ns = createMK(localClusterId).nodeStore;
+
+        try {
+            ClusterViewDocument.readOrUpdate(ns, null, null, null);
+            fail("should complain");
+        } catch (Exception ok) {
+            // ok
+        }
+
+        try {
+            ClusterViewDocument.readOrUpdate(ns, new HashSet<Integer>(), null, null);
+            fail("should complain");
+        } catch (Exception ok) {
+            // ok
+        }
+
+        Set<Integer> activeIds = new HashSet<Integer>();
+        activeIds.add(2);
+        Set<Integer> recoveringIds = null;
+        Set<Integer> inactiveIds = null;
+        // first ever view:
+        ClusterViewDocument doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        final String id = doc.getClusterViewId();
+        assertTrue(id != null && id.length() > 0);
+        String createdAt = doc.getCreatedAt();
+        assertTrue(createdAt != null && createdAt.length() > 0);
+        int createdBy = doc.getCreatedBy();
+        assertEquals(localClusterId, createdBy);
+        assertEquals(1, doc.getViewSeqNum());
+        assertEquals(1, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(2));
+        assertEquals(0, doc.getRecoveringIds().size());
+        assertEquals(0, doc.getInactiveIds().size());
+
+        // now let's check if it doesn't change anything when we're not doing
+        // any update
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(1, doc.getViewSeqNum());
+
+        // and now add a new active id
+        activeIds.add(3);
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(id, doc.getClusterViewId());
+        createdAt = doc.getCreatedAt();
+        assertTrue(createdAt != null && createdAt.length() > 0);
+        createdBy = doc.getCreatedBy();
+        assertEquals(localClusterId, createdBy);
+        assertEquals(2, doc.getViewSeqNum());
+        assertEquals(2, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(2));
+        assertTrue(doc.getActiveIds().contains(3));
+        assertEquals(0, doc.getRecoveringIds().size());
+        assertEquals(0, doc.getInactiveIds().size());
+
+        // now let's check if it doesn't change anything when we're not doing
+        // any update
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(2, doc.getViewSeqNum());
+
+        // and now add a new recovering id
+        recoveringIds = new HashSet<Integer>();
+        recoveringIds.add(4);
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(id, doc.getClusterViewId());
+        createdAt = doc.getCreatedAt();
+        assertTrue(createdAt != null && createdAt.length() > 0);
+        createdBy = doc.getCreatedBy();
+        assertEquals(localClusterId, createdBy);
+        assertEquals(3, doc.getViewSeqNum());
+        assertEquals(2, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(2));
+        assertTrue(doc.getActiveIds().contains(3));
+        assertEquals(1, doc.getRecoveringIds().size());
+        assertTrue(doc.getRecoveringIds().contains(4));
+        assertEquals(0, doc.getInactiveIds().size());
+
+        // now let's check if it doesn't change anything when we're not doing
+        // any update
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(3, doc.getViewSeqNum());
+
+        // and now move that one to inactive
+        recoveringIds = new HashSet<Integer>();
+        inactiveIds = new HashSet<Integer>();
+        inactiveIds.add(4);
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(id, doc.getClusterViewId());
+        createdAt = doc.getCreatedAt();
+        assertTrue(createdAt != null && createdAt.length() > 0);
+        createdBy = doc.getCreatedBy();
+        assertEquals(localClusterId, createdBy);
+        assertEquals(4, doc.getViewSeqNum());
+        assertEquals(2, doc.getActiveIds().size());
+        assertTrue(doc.getActiveIds().contains(2));
+        assertTrue(doc.getActiveIds().contains(3));
+        assertEquals(0, doc.getRecoveringIds().size());
+        assertEquals(1, doc.getInactiveIds().size());
+        assertTrue(doc.getInactiveIds().contains(4));
+
+        // now let's check if it doesn't change anything when we're not doing
+        // any update
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(4, doc.getViewSeqNum());
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(4, doc.getViewSeqNum());
+        doc = ClusterViewDocument.readOrUpdate(ns, activeIds, recoveringIds, inactiveIds);
+        assertEquals(4, doc.getViewSeqNum());
+    }
+
+    private static DocumentMK createMK(int clusterId) {
+        return create(new MemoryDocumentStore(), clusterId);
+    }
+
+    private static DocumentMK create(DocumentStore ds, int clusterId) {
+        return new DocumentMK.Builder().setAsyncDelay(0).setDocumentStore(ds).setClusterId(clusterId)
+                .setPersistentCache("target/persistentCache,time").open();
+    }
+
+}

Propchange: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewDocumentTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewTest.java?rev=1697355&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewTest.java Mon Aug 24 11:25:48 2015
@@ -0,0 +1,260 @@
+/*
+ * 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.oak.plugins.document;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
+import org.junit.Test;
+
+/** Simple paranoia tests for constructor and getters of ClusterViewImpl **/
+public class ClusterViewTest {
+
+    @Test
+    public void testConstructor() throws Exception {
+        final Integer viewId = 3;
+        final String clusterViewId = UUID.randomUUID().toString();
+        final Integer instanceId = 2;
+        final Set<Integer> emptyInstanceIds = new HashSet<Integer>();
+        final Set<Integer> instanceIds = new HashSet<Integer>();
+        instanceIds.add(1);
+        final Set<Integer> deactivating = new HashSet<Integer>();
+        final Set<Integer> inactive = new HashSet<Integer>();
+        try {
+            new ClusterView(-1, true, clusterViewId, instanceId, instanceIds, deactivating, inactive);
+            fail("should complain");
+        } catch (IllegalStateException e) {
+            // ok
+        }
+        try {
+            new ClusterView(viewId, true, null, instanceId, instanceIds, deactivating, inactive);
+            fail("should complain");
+        } catch (IllegalStateException e) {
+            // ok
+        }
+        try {
+            new ClusterView(viewId, true, clusterViewId, -1, instanceIds, deactivating, inactive);
+            fail("should complain");
+        } catch (IllegalStateException e) {
+            // ok
+        }
+        try {
+            new ClusterView(viewId, true, clusterViewId, instanceId, emptyInstanceIds, deactivating, inactive);
+            fail("should complain");
+        } catch (IllegalStateException e) {
+            // ok
+        }
+        try {
+            new ClusterView(viewId, true, clusterViewId, instanceId, null, deactivating, inactive);
+            fail("should complain");
+        } catch (IllegalStateException e) {
+            // ok
+        }
+        try {
+            new ClusterView(viewId, true, clusterViewId, instanceId, instanceIds, null, inactive);
+            fail("should complain");
+        } catch (Exception e) {
+            // ok
+        }
+        try {
+            new ClusterView(viewId, true, clusterViewId, instanceId, instanceIds, deactivating, null);
+            fail("should complain");
+        } catch (Exception e) {
+            // ok
+        }
+        final Set<Integer> nonEmptyDeactivating = new HashSet<Integer>();
+        nonEmptyDeactivating.add(3);
+        new ClusterView(viewId, false, clusterViewId, instanceId, instanceIds, nonEmptyDeactivating, inactive);
+        new ClusterView(viewId, true, clusterViewId, instanceId, instanceIds, nonEmptyDeactivating, inactive);
+        // should not complain about:
+        new ClusterView(viewId, true, clusterViewId, instanceId, instanceIds, deactivating, inactive);
+    }
+
+    @Test
+    public void testGetters() throws Exception {
+        final Integer viewId = 3;
+        final String clusterViewId = UUID.randomUUID().toString();
+        final Set<Integer> instanceIds = new HashSet<Integer>();
+        instanceIds.add(1);
+        final Integer instanceId = 2;
+        final Set<Integer> deactivating = new HashSet<Integer>();
+        final Set<Integer> inactive = new HashSet<Integer>();
+        final ClusterView cv = new ClusterView(viewId, true, clusterViewId, instanceId, instanceIds, deactivating, inactive);
+        assertNotNull(cv);
+        assertTrue(cv.asDescriptorValue().length() > 0);
+        assertTrue(cv.toString().length() > 0);
+    }
+
+    @Test
+    public void testOneActiveOnly() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 21);
+        ClusterView view = builder.active(21).asView();
+
+        // {"seq":10,"id":"35f60ed3-508d-4a81-b812-89f07f57db20","me":2,"active":[2],"deactivating":[],"inactive":[3]}
+        JsonObject o = asJsonObject(view);
+        Map<String, String> props = o.getProperties();
+        assertEquals("10", props.get("seq"));
+        assertEquals(clusterViewId, unwrapString(props.get("id")));
+        assertEquals("21", props.get("me"));
+        assertEquals(asJsonArray(21), props.get("active"));
+        assertEquals(asJsonArray(), props.get("deactivating"));
+        assertEquals(asJsonArray(), props.get("inactive"));
+    }
+
+    @Test
+    public void testOneActiveOneInactive() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 2);
+        ClusterView view = builder.active(2).inactive(3).asView();
+
+        // {"seq":10,"id":"35f60ed3-508d-4a81-b812-89f07f57db20","me":2,"active":[2],"deactivating":[],"inactive":[3]}
+        JsonObject o = asJsonObject(view);
+        Map<String, String> props = o.getProperties();
+        assertEquals("10", props.get("seq"));
+        assertEquals(clusterViewId, unwrapString(props.get("id")));
+        assertEquals("2", props.get("me"));
+        assertEquals(asJsonArray(2), props.get("active"));
+        assertEquals(asJsonArray(), props.get("deactivating"));
+        assertEquals(asJsonArray(3), props.get("inactive"));
+    }
+
+    @Test
+    public void testSeveralActiveOneInactive() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 2);
+        ClusterView view = builder.active(2, 5, 6).inactive(3).asView();
+
+        // {"seq":10,"id":"35f60ed3-508d-4a81-b812-89f07f57db20","me":2,"active":[2],"deactivating":[],"inactive":[3]}
+        JsonObject o = asJsonObject(view);
+        Map<String, String> props = o.getProperties();
+        assertEquals("10", props.get("seq"));
+        assertEquals("true", props.get("final"));
+        assertEquals(clusterViewId, unwrapString(props.get("id")));
+        assertEquals("2", props.get("me"));
+        assertEquals(asJsonArray(2, 5, 6), props.get("active"));
+        assertEquals(asJsonArray(), props.get("deactivating"));
+        assertEquals(asJsonArray(3), props.get("inactive"));
+    }
+
+    @Test
+    public void testOneActiveSeveralInactive() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 2);
+        ClusterView view = builder.active(2).inactive(3, 4, 5, 6).asView();
+
+        // {"seq":10,"id":"35f60ed3-508d-4a81-b812-89f07f57db20","me":2,"active":[2],"deactivating":[],"inactive":[3]}
+        JsonObject o = asJsonObject(view);
+        Map<String, String> props = o.getProperties();
+        assertEquals("10", props.get("seq"));
+        assertEquals("true", props.get("final"));
+        assertEquals(clusterViewId, unwrapString(props.get("id")));
+        assertEquals("2", props.get("me"));
+        assertEquals(asJsonArray(2), props.get("active"));
+        assertEquals(asJsonArray(), props.get("deactivating"));
+        assertEquals(asJsonArray(3, 4, 5, 6), props.get("inactive"));
+    }
+
+    @Test
+    public void testWithRecoveringOnly() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 2);
+        ClusterView view = builder.active(2, 3).recovering(4).inactive(5, 6).asView();
+
+        JsonObject o = asJsonObject(view);
+        Map<String, String> props = o.getProperties();
+        assertEquals("10", props.get("seq"));
+        assertEquals("true", props.get("final"));
+        assertEquals(clusterViewId, unwrapString(props.get("id")));
+        assertEquals("2", props.get("me"));
+        assertEquals(asJsonArray(2, 3), props.get("active"));
+        assertEquals(asJsonArray(4), props.get("deactivating"));
+        assertEquals(asJsonArray(5, 6), props.get("inactive"));
+    }
+
+    @Test
+    public void testWithRecoveringAndBacklog() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 2);
+        ClusterView view = builder.active(2, 3).recovering(4).inactive(5, 6).backlogs(5).asView();
+
+        JsonObject o = asJsonObject(view);
+        Map<String, String> props = o.getProperties();
+        assertEquals("10", props.get("seq"));
+        assertEquals(clusterViewId, unwrapString(props.get("id")));
+        assertEquals("2", props.get("me"));
+        assertEquals("false", props.get("final"));
+        assertEquals(asJsonArray(2, 3), props.get("active"));
+        assertEquals(asJsonArray(4, 5), props.get("deactivating"));
+        assertEquals(asJsonArray(6), props.get("inactive"));
+    }
+
+    @Test
+    public void testBacklogButNotInactive() throws Exception {
+        String clusterViewId = UUID.randomUUID().toString();
+        ClusterViewBuilder builder = new ClusterViewBuilder(10, clusterViewId, 2);
+        try {
+            ClusterView view = builder.active(2, 3).backlogs(5).asView();
+            fail("should complain");
+        } catch (Exception ok) {
+            // ok
+        }
+    }
+
+    private JsonObject asJsonObject(final ClusterView view) {
+        final String json = view.asDescriptorValue();
+        System.out.println(json);
+        JsopTokenizer t = new JsopTokenizer(json);
+        t.read('{');
+        JsonObject o = JsonObject.create(t);
+        return o;
+    }
+
+    private String unwrapString(String stringWithQuotes) {
+        // TODO: I'm not really sure why the JsonObject parses this string
+        // including the "
+        // perhaps that's rather a bug ..
+        assertTrue(stringWithQuotes.startsWith("\""));
+        assertTrue(stringWithQuotes.endsWith("\""));
+        return stringWithQuotes.substring(1, stringWithQuotes.length() - 1);
+    }
+
+    static String asJsonArray(int... ids) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        for (int i = 0; i < ids.length; i++) {
+            int anId = ids[i];
+            if (i != 0) {
+                sb.append(",");
+            }
+            sb.append(String.valueOf(anId));
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ClusterViewTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentDiscoveryLiteServiceTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentDiscoveryLiteServiceTest.java?rev=1697355&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentDiscoveryLiteServiceTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentDiscoveryLiteServiceTest.java Mon Aug 24 11:25:48 2015
@@ -0,0 +1,1043 @@
+/*
+ * 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.oak.plugins.document;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.jcr.PropertyType;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+import junitx.util.PrivateAccessor;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.Descriptors;
+import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
+import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
+import org.apache.jackrabbit.oak.spi.blob.BlobStore;
+import org.apache.jackrabbit.oak.spi.blob.MemoryBlobStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Lists;
+import com.mongodb.DB;
+
+/**
+ * Tests for the DocumentDiscoveryLiteService
+ */
+public class DocumentDiscoveryLiteServiceTest {
+
+    /**
+     * container for what should represent an instance, but is not a complete
+     * one, hence 'simplified'. it contains most importantly the
+     * DocuemntNodeStore and the discoveryLite service
+     */
+    class SimplifiedInstance {
+
+        private DocumentDiscoveryLiteService service;
+        private DocumentNodeStore ns;
+        private final Descriptors descriptors;
+        private Map<String, Object> registeredServices;
+        private final long lastRevInterval;
+        private volatile boolean lastRevStopped = false;
+        private volatile boolean writeSimulationStopped = false;
+        private Thread lastRevThread;
+        private Thread writeSimulationThread;
+        public String workingDir;
+
+        SimplifiedInstance(DocumentDiscoveryLiteService service, DocumentNodeStore ns, Descriptors descriptors,
+                Map<String, Object> registeredServices, long lastRevInterval, String workingDir) {
+            this.service = service;
+            this.ns = ns;
+            this.workingDir = workingDir;
+            this.descriptors = descriptors;
+            this.registeredServices = registeredServices;
+            this.lastRevInterval = lastRevInterval;
+            if (lastRevInterval > 0) {
+                startLastRevThread();
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "SimplifiedInstance[cid=" + ns.getClusterId() + "]";
+        }
+
+        private void startLastRevThread() {
+            lastRevStopped = false;
+            lastRevThread = new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    while (!lastRevStopped) {
+                        SimplifiedInstance.this.ns.getLastRevRecoveryAgent().performRecoveryIfNeeded();
+                        try {
+                            Thread.sleep(SimplifiedInstance.this.lastRevInterval);
+                        } catch (InterruptedException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+
+            });
+            lastRevThread.setDaemon(true);
+            lastRevThread.setName("lastRevThread[cid=" + ns.getClusterId() + "]");
+            lastRevThread.start();
+        }
+
+        void stopLastRevThread() throws InterruptedException {
+            lastRevStopped = true;
+            lastRevThread.join();
+        }
+
+        boolean isFinal() throws Exception {
+            final JsonObject clusterViewObj = getClusterViewObj();
+            if (clusterViewObj == null) {
+                throw new IllegalStateException("should always have that final flag set");
+            }
+
+            String finalStr = clusterViewObj.getProperties().get("final");
+
+            return Boolean.valueOf(finalStr);
+        }
+
+        boolean hasActiveIds(String clusterViewStr, int... expected) throws Exception {
+            return hasIds(clusterViewStr, "active", expected);
+        }
+
+        boolean hasDeactivatingIds(String clusterViewStr, int... expected) throws Exception {
+            return hasIds(clusterViewStr, "deactivating", expected);
+        }
+
+        boolean hasInactiveIds(String clusterViewStr, int... expected) throws Exception {
+            return hasIds(clusterViewStr, "inactive", expected);
+        }
+
+        private boolean hasIds(final String clusterViewStr, final String key, int... expectedIds) throws Exception {
+            final JsonObject clusterViewObj = asJsonObject(clusterViewStr);
+            String actualIdsStr = clusterViewObj == null ? null : clusterViewObj.getProperties().get(key);
+
+            boolean actualEmpty = actualIdsStr == null || actualIdsStr.length() == 0 || actualIdsStr.equals("[]");
+            boolean expectedEmpty = expectedIds == null || expectedIds.length == 0;
+
+            if (actualEmpty && expectedEmpty) {
+                return true;
+            }
+            if (actualEmpty != expectedEmpty) {
+                return false;
+            }
+
+            final List<Integer> actualList = Arrays
+                    .asList(ClusterViewDocument.csvToIntegerArray(actualIdsStr.substring(1, actualIdsStr.length() - 1)));
+            if (expectedIds.length != actualList.size()) {
+                return false;
+            }
+            for (int i = 0; i < expectedIds.length; i++) {
+                int anExpectedId = expectedIds[i];
+                if (!actualList.contains(anExpectedId)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        JsonObject getClusterViewObj() throws Exception {
+            final String json = getClusterViewStr();
+            return asJsonObject(json);
+        }
+
+        private JsonObject asJsonObject(final String json) {
+            if (json == null) {
+                return null;
+            }
+            JsopTokenizer t = new JsopTokenizer(json);
+            t.read('{');
+            JsonObject o = JsonObject.create(t);
+            return o;
+        }
+
+        String getClusterViewStr() throws Exception {
+            return getDescriptor(DocumentDiscoveryLiteService.OAK_DISCOVERYLITE_CLUSTERVIEW);
+        }
+
+        String getDescriptor(String key) throws Exception {
+            final Value value = descriptors.getValue(key);
+            if (value == null) {
+                return null;
+            }
+            if (value.getType() != PropertyType.STRING) {
+                return null;
+            }
+            try {
+                return value.getString();
+            } catch (ValueFormatException vfe) {
+                return null;
+            }
+        }
+
+        public void dispose() {
+            logger.info("Disposing " + this);
+            try {
+                stopSimulatingWrites();
+            } catch (InterruptedException e) {
+                fail("interrupted");
+            }
+            if (lastRevThread != null) {
+                try {
+                    stopLastRevThread();
+                } catch (InterruptedException ok) {
+                    fail("interrupted");
+                }
+                lastRevThread = null;
+            }
+            if (service != null) {
+                service.deactivate();
+                service = null;
+            }
+            if (ns != null) {
+                ns.dispose();
+                ns = null;
+            }
+            if (registeredServices != null) {
+                registeredServices.clear();
+                registeredServices = null;
+            }
+        }
+
+        /**
+         * shutdown simulates the normal, graceful, shutdown
+         * 
+         * @throws InterruptedException
+         */
+        public void shutdown() throws InterruptedException {
+            stopSimulatingWrites();
+            stopLastRevThread();
+            ns.dispose();
+            service.deactivate();
+        }
+
+        /**
+         * crash simulates a kill -9, sort of
+         * 
+         * @throws Throwable
+         */
+        public void crash() throws Throwable {
+            logger.info("crash: stopping simulating writes...");
+            stopSimulatingWrites();
+            logger.info("crash: stopping lastrev thread...");
+            stopLastRevThread();
+            logger.info("crash: stopped lastrev thread, now setting least to end within 1 sec");
+
+            boolean renewed = setLeaseTime(1000 /* 1 sec */);
+            if (!renewed) {
+                logger.info("halt");
+                fail("did not renew clusterid lease");
+            }
+
+            logger.info("crash: now stopping background read/update");
+            stopAllBackgroundThreads();
+            // but don't do the following from DocumentNodeStore.dispose():
+            // * don't do the last internalRunBackgroundUpdateOperations - as
+            // we're trying to simulate a crash here
+            // * don't dispose clusterNodeInfo to leave the node in active state
+
+            // the DocumentDiscoveryLiteService currently can simply be
+            // deactivated, doesn't differ much from crashing
+            service.deactivate();
+            logger.info("crash: crash simulation done.");
+        }
+
+        /**
+         * very hacky way of doing the following: make sure this instance's
+         * clusterNodes entry is marked with a very short (1 sec off) lease end
+         * time so that the crash detection doesn't take a minute (as it would
+         * by default)
+         */
+        private boolean setLeaseTime(final int leaseTime) throws NoSuchFieldException {
+            ns.getClusterInfo().setLeaseTime(leaseTime);
+            PrivateAccessor.setField(ns.getClusterInfo(), "leaseEndTime", System.currentTimeMillis() + (leaseTime / 2));
+            boolean renewed = ns.renewClusterIdLease();
+            return renewed;
+        }
+
+        private AtomicBoolean getIsDisposed() throws NoSuchFieldException {
+            AtomicBoolean isDisposed = (AtomicBoolean) PrivateAccessor.getField(ns, "isDisposed");
+            return isDisposed;
+        }
+
+        private void stopAllBackgroundThreads() throws NoSuchFieldException {
+            // get all those background threads...
+            Thread backgroundReadThread = (Thread) PrivateAccessor.getField(ns, "backgroundReadThread");
+            assertNotNull(backgroundReadThread);
+            Thread backgroundUpdateThread = (Thread) PrivateAccessor.getField(ns, "backgroundUpdateThread");
+            assertNotNull(backgroundUpdateThread);
+            Thread leaseUpdateThread = (Thread) PrivateAccessor.getField(ns, "leaseUpdateThread");
+            assertNotNull(leaseUpdateThread);
+
+            // start doing what DocumentNodeStore.dispose() would do - except do
+            // it very fine controlled, basically:
+            // make sure to stop backgroundReadThread, backgroundUpdateThread
+            // and leaseUpdateThread
+            // but then nothing else.
+            final AtomicBoolean isDisposed = getIsDisposed();
+            assertFalse(isDisposed.getAndSet(true));
+            // notify background threads waiting on isDisposed
+            synchronized (isDisposed) {
+                isDisposed.notifyAll();
+            }
+            try {
+                backgroundReadThread.join(5000);
+                assertTrue(!backgroundReadThread.isAlive());
+            } catch (InterruptedException e) {
+                // ignore
+            }
+            try {
+                backgroundUpdateThread.join(5000);
+                assertTrue(!backgroundUpdateThread.isAlive());
+            } catch (InterruptedException e) {
+                // ignore
+            }
+            try {
+                leaseUpdateThread.join(5000);
+                assertTrue(!leaseUpdateThread.isAlive());
+            } catch (InterruptedException e) {
+                // ignore
+            }
+        }
+
+        public void stopBgReadThread() throws NoSuchFieldException {
+            final Thread backgroundReadThread = (Thread) PrivateAccessor.getField(ns, "backgroundReadThread");
+            assertNotNull(backgroundReadThread);
+            final Runnable bgReadRunnable = (Runnable) PrivateAccessor.getField(backgroundReadThread, "target");
+            assertNotNull(bgReadRunnable);
+            final AtomicBoolean bgReadIsDisposed = new AtomicBoolean(false);
+            PrivateAccessor.setField(bgReadRunnable, "isDisposed", bgReadIsDisposed);
+            assertFalse(bgReadIsDisposed.getAndSet(true));
+            try {
+                backgroundReadThread.join(5000);
+                assertTrue(!backgroundReadThread.isAlive());
+            } catch (InterruptedException e) {
+                // ignore
+            }
+            // big of heavy work, but now the backgroundReadThread is stopped
+            // and all the others are still running
+        }
+
+        public void addNode(String path) throws CommitFailedException {
+            NodeBuilder root = ns.getRoot().builder();
+            NodeBuilder child = root;
+            String[] split = path.split("/");
+            for (int i = 1; i < split.length; i++) {
+                child = child.child(split[i]);
+            }
+            logger.info("addNode: " + ns.getClusterId() + " is merging path " + path);
+            ns.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        }
+
+        public void setProperty(String path, String key, String value) throws CommitFailedException {
+            NodeBuilder root = ns.getRoot().builder();
+            NodeBuilder child = root;
+            String[] split = path.split("/");
+            for (int i = 1; i < split.length; i++) {
+                child = child.child(split[i]);
+            }
+            child.setProperty(key, value);
+            logger.info("setProperty: " + ns.getClusterId() + " is merging path/property " + path + ", key=" + key + ", value="
+                    + value);
+            ns.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        }
+
+        public void setLeastTimeout(long timeoutInMs) throws NoSuchFieldException {
+            ns.getClusterInfo().setLeaseTime(timeoutInMs);
+            PrivateAccessor.setField(ns.getClusterInfo(), "leaseEndTime", System.currentTimeMillis() - 1000);
+        }
+
+        private void startSimulatingWrites(final long writeInterval) {
+            writeSimulationStopped = false;
+            writeSimulationThread = new Thread(new Runnable() {
+
+                final Random random = new Random();
+
+                @Override
+                public void run() {
+                    while (!writeSimulationStopped) {
+                        try {
+                            writeSomething();
+                            Thread.sleep(SimplifiedInstance.this.lastRevInterval);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+
+                private void writeSomething() throws CommitFailedException {
+                    final String path = "/" + ns.getClusterId() + "/" + random.nextInt(100) + "/" + random.nextInt(100) + "/"
+                            + random.nextInt(100);
+                    logger.info("Writing [" + ns.getClusterId() + "]" + path);
+                    addNode(path);
+                }
+
+            });
+            writeSimulationThread.setDaemon(true);
+            writeSimulationThread.start();
+        }
+
+        void stopSimulatingWrites() throws InterruptedException {
+            writeSimulationStopped = true;
+            if (writeSimulationThread != null) {
+                writeSimulationThread.join();
+            }
+        }
+
+    }
+
+    interface Expectation {
+        /**
+         * check if the expectation is fulfilled, return true if it is, return a
+         * descriptive error msg if not
+         **/
+        String fulfilled() throws Exception;
+    }
+
+    class ViewExpectation implements Expectation {
+
+        private int[] activeIds;
+        private int[] deactivatingIds;
+        private int[] inactiveIds;
+        private final SimplifiedInstance discoveryLiteCombo;
+        private boolean isFinal = true;
+
+        ViewExpectation(SimplifiedInstance discoveryLiteCombo) {
+            this.discoveryLiteCombo = discoveryLiteCombo;
+        }
+
+        private int[] asIntArray(Integer[] arr) {
+            int[] result = new int[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                result[i] = arr[i];
+            }
+            return result;
+        }
+
+        void setActiveIds(Integer[] activeIds) {
+            this.activeIds = asIntArray(activeIds);
+        }
+
+        void setActiveIds(int... activeIds) {
+            this.activeIds = activeIds;
+        }
+
+        void setDeactivatingIds(int... deactivatingIds) {
+            this.deactivatingIds = deactivatingIds;
+        }
+
+        void setInactiveIds(Integer[] inactiveIds) {
+            this.inactiveIds = asIntArray(inactiveIds);
+        }
+
+        void setInactiveIds(int... inaactiveIds) {
+            this.inactiveIds = inaactiveIds;
+        }
+
+        @Override
+        public String fulfilled() throws Exception {
+            final String clusterViewStr = discoveryLiteCombo.getClusterViewStr();
+            if (clusterViewStr == null) {
+                if (activeIds.length != 0) {
+                    return "no clusterView, but expected activeIds: " + beautify(activeIds);
+                }
+                if (deactivatingIds.length != 0) {
+                    return "no clusterView, but expected deactivatingIds: " + beautify(deactivatingIds);
+                }
+                if (inactiveIds.length != 0) {
+                    return "no clusterView, but expected inactiveIds: " + beautify(inactiveIds);
+                }
+            }
+            if (!discoveryLiteCombo.hasActiveIds(clusterViewStr, activeIds)) {
+                return "activeIds dont match, expected: " + beautify(activeIds) + ", got clusterView: " + clusterViewStr;
+            }
+            if (!discoveryLiteCombo.hasDeactivatingIds(clusterViewStr, deactivatingIds)) {
+                return "deactivatingIds dont match, expected: " + beautify(deactivatingIds) + ", got clusterView: "
+                        + clusterViewStr;
+            }
+            if (!discoveryLiteCombo.hasInactiveIds(clusterViewStr, inactiveIds)) {
+                return "inactiveIds dont match, expected: " + beautify(inactiveIds) + ", got clusterView: " + clusterViewStr;
+            }
+            if (discoveryLiteCombo.isFinal() != isFinal) {
+                return "final flag does not match. expected: " + isFinal + ", but is: " + discoveryLiteCombo.isFinal();
+            }
+            return null;
+        }
+
+        private String beautify(int[] ids) {
+            if (ids == null) {
+                return "";
+            }
+            StringBuffer sb = new StringBuffer();
+            for (int i = 0; i < ids.length; i++) {
+                if (i != 0) {
+                    sb.append(",");
+                }
+                sb.append(ids[i]);
+            }
+            return sb.toString();
+        }
+
+        public void setFinal(boolean isFinal) {
+            this.isFinal = isFinal;
+        }
+
+    }
+
+    private static final boolean MONGO_DB = true;
+    // private static final boolean MONGO_DB = true;
+
+    private List<DocumentMK> mks = Lists.newArrayList();
+    private MemoryDocumentStore ds;
+    private MemoryBlobStore bs;
+
+    final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private List<SimplifiedInstance> allInstances = new LinkedList<SimplifiedInstance>();
+
+    @Test
+    public void testActivateDeactivate() throws Exception {
+        // then test normal start with a DocumentNodeStore
+        DocumentMK mk1 = createMK(1, 0);
+        DocumentDiscoveryLiteService discoveryLite = new DocumentDiscoveryLiteService();
+        PrivateAccessor.setField(discoveryLite, "nodeStore", mk1.nodeStore);
+        BundleContext bc = mock(BundleContext.class);
+        ComponentContext c = mock(ComponentContext.class);
+        when(c.getBundleContext()).thenReturn(bc);
+        discoveryLite.activate(c);
+        verify(c, times(0)).disableComponent(DocumentDiscoveryLiteService.COMPONENT_NAME);
+        discoveryLite.deactivate();
+    }
+
+    /**
+     * Borrowed from
+     * http://stackoverflow.com/questions/3301635/change-private-static-final-
+     * field-using-java-reflection
+     */
+    static void setFinalStatic(Field field, Object newValue) throws Exception {
+        field.setAccessible(true);
+
+        Field modifiersField = Field.class.getDeclaredField("modifiers");
+        modifiersField.setAccessible(true);
+        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+        field.set(null, newValue);
+    }
+
+    // subsequent tests should get a DocumentDiscoveryLiteService setup from the
+    // start
+    private DocumentNodeStore createNodeStore(String workingDir) throws SecurityException, Exception {
+        // ensure that we always get a fresh cluster[node]id
+        System.setProperty("user.dir", workingDir);
+        setFinalStatic(ClusterNodeInfo.class.getDeclaredField("WORKING_DIR"), workingDir);
+
+        // then create the DocumentNodeStore
+        DocumentMK mk1 = createMK(
+                0 /* to make sure the clusterNodes collection is used **/,
+                500 /* asyncDelay: background interval */);
+
+        logger.info("createNodeStore: created DocumentNodeStore with cid=" + mk1.nodeStore.getClusterId() + ", workingDir="
+                + workingDir);
+        return mk1.nodeStore;
+    }
+
+    private SimplifiedInstance createInstance() throws Exception {
+        final String workingDir = UUID.randomUUID().toString();
+        return createInstance(workingDir);
+    }
+
+    private SimplifiedInstance createInstance(String workingDir) throws SecurityException, Exception {
+        DocumentNodeStore ns = createNodeStore(workingDir);
+        return createInstance(ns, workingDir);
+    }
+
+    private SimplifiedInstance createInstance(DocumentNodeStore ns, String workingDir) throws NoSuchFieldException {
+        DocumentDiscoveryLiteService discoveryLite = new DocumentDiscoveryLiteService();
+        PrivateAccessor.setField(discoveryLite, "nodeStore", ns);
+        BundleContext bc = mock(BundleContext.class);
+        ComponentContext c = mock(ComponentContext.class);
+        when(c.getBundleContext()).thenReturn(bc);
+        final Map<String, Object> registeredServices = new HashMap<String, Object>();
+        when(bc.registerService(anyString(), anyObject(), (Properties) anyObject())).then(new Answer<ServiceRegistration>() {
+            @Override
+            public ServiceRegistration answer(InvocationOnMock invocation) {
+                registeredServices.put((String) invocation.getArguments()[0], invocation.getArguments()[1]);
+                return null;
+            }
+        });
+        discoveryLite.activate(c);
+        Descriptors d = (Descriptors) registeredServices.get(Descriptors.class.getName());
+        final SimplifiedInstance result = new SimplifiedInstance(discoveryLite, ns, d, registeredServices, 500, workingDir);
+        allInstances.add(result);
+        logger.info("Created " + result);
+        return result;
+    }
+
+    private void waitFor(Expectation expectation, int timeout, String msg) throws Exception {
+        final long tooLate = System.currentTimeMillis() + timeout;
+        while (true) {
+            final String fulfillmentResult = expectation.fulfilled();
+            if (fulfillmentResult == null) {
+                // everything's fine
+                return;
+            }
+            if (System.currentTimeMillis() > tooLate) {
+                fail("expectation not fulfilled within " + timeout + "ms: " + msg + ", fulfillment result: " + fulfillmentResult);
+            }
+            Thread.sleep(100);
+        }
+    }
+
+    @Test
+    public void testOneNode() throws Exception {
+        final SimplifiedInstance s1 = createInstance();
+        final ViewExpectation expectation = new ViewExpectation(s1);
+        expectation.setActiveIds(s1.ns.getClusterId());
+        waitFor(expectation, 2000, "see myself as active");
+    }
+
+    @Test
+    public void testTwoNodesWithCleanShutdown() throws Exception {
+        final SimplifiedInstance s1 = createInstance();
+        final SimplifiedInstance s2 = createInstance();
+        final ViewExpectation expectation1 = new ViewExpectation(s1);
+        final ViewExpectation expectation2 = new ViewExpectation(s2);
+        expectation1.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        expectation2.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        waitFor(expectation1, 2000, "first should see both as active");
+        waitFor(expectation2, 2000, "second should see both as active");
+
+        s2.shutdown();
+        final ViewExpectation expectation1AfterShutdown = new ViewExpectation(s1);
+        expectation1AfterShutdown.setActiveIds(s1.ns.getClusterId());
+        expectation1AfterShutdown.setInactiveIds(s2.ns.getClusterId());
+        waitFor(expectation1AfterShutdown, 2000, "first should only see itself after shutdown");
+    }
+
+    @Test
+    public void testTwoNodesWithCrash() throws Throwable {
+        final SimplifiedInstance s1 = createInstance();
+        final SimplifiedInstance s2 = createInstance();
+        final ViewExpectation expectation1 = new ViewExpectation(s1);
+        final ViewExpectation expectation2 = new ViewExpectation(s2);
+        expectation1.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        expectation2.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        waitFor(expectation1, 2000, "first should see both as active");
+        waitFor(expectation2, 2000, "second should see both as active");
+
+        s2.crash();
+
+        final ViewExpectation expectation1AfterShutdown = new ViewExpectation(s1);
+        expectation1AfterShutdown.setActiveIds(s1.ns.getClusterId());
+        expectation1AfterShutdown.setInactiveIds(s2.ns.getClusterId());
+        waitFor(expectation1AfterShutdown, 2000, "first should only see itself after shutdown");
+    }
+
+    @Test
+    public void testTwoNodesWithCrashAndLongduringRecovery() throws Throwable {
+        doTestTwoNodesWithCrashAndLongduringDeactivation(false);
+    }
+
+    @Test
+    public void testTwoNodesWithCrashAndLongduringRecoveryAndBacklog() throws Throwable {
+        doTestTwoNodesWithCrashAndLongduringDeactivation(true);
+    }
+
+    void doTestTwoNodesWithCrashAndLongduringDeactivation(boolean withBacklog) throws Throwable {
+        final int TEST_WAIT_TIMEOUT = 10000;
+        final SimplifiedInstance s1 = createInstance();
+        final SimplifiedInstance s2 = createInstance();
+        final ViewExpectation expectation1 = new ViewExpectation(s1);
+        final ViewExpectation expectation2 = new ViewExpectation(s2);
+        expectation1.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        expectation2.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        waitFor(expectation1, TEST_WAIT_TIMEOUT, "first should see both as active");
+        waitFor(expectation2, TEST_WAIT_TIMEOUT, "second should see both as active");
+
+        // before crashing s2, make sure that s1's lastRevRecovery thread
+        // doesn't run
+        s1.stopLastRevThread();
+        if (withBacklog) {
+            // plus also stop s1's backgroundReadThread - in case we want to
+            // test backlog handling
+            s1.stopBgReadThread();
+
+            // and then, if we want to do backlog testing, then s2 should write
+            // something
+            // before it crashes, so here it comes:
+            s2.addNode("/foo/bar");
+            s2.setProperty("/foo/bar", "prop", "value");
+        }
+
+        // then crash s2
+        s2.crash();
+
+        // then wait 2 sec
+        Thread.sleep(2000);
+
+        // at this stage, while s2 has crashed, we have stopped s1's
+        // lastRevRecoveryThread, so we should still see both as active
+        logger.info(s1.getClusterViewStr());
+        final ViewExpectation expectation1AfterCrashBeforeLastRevRecovery = new ViewExpectation(s1);
+        expectation1AfterCrashBeforeLastRevRecovery.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        waitFor(expectation1AfterCrashBeforeLastRevRecovery, TEST_WAIT_TIMEOUT, "first should still see both as active");
+
+        // the next part is a bit tricky: we want to fine-control the
+        // lastRevRecoveryThread's acquire/release locking.
+        // the chosen way to do this is to make heavy use of mockito and two
+        // semaphores:
+        // when acquireRecoveryLock is called, that thread should wait for the
+        // waitBeforeLocking semaphore to be released
+        final MissingLastRevSeeker missingLastRevUtil = (MissingLastRevSeeker) PrivateAccessor
+                .getField(s1.ns.getLastRevRecoveryAgent(), "missingLastRevUtil");
+        assertNotNull(missingLastRevUtil);
+        MissingLastRevSeeker mockedLongduringMissingLastRevUtil = mock(MissingLastRevSeeker.class, delegatesTo(missingLastRevUtil));
+        final Semaphore waitBeforeLocking = new Semaphore(0);
+        when(mockedLongduringMissingLastRevUtil.acquireRecoveryLock(anyInt())).then(new Answer<Boolean>() {
+
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                logger.info("going to waitBeforeLocking");
+                waitBeforeLocking.acquire();
+                logger.info("done with waitBeforeLocking");
+                return missingLastRevUtil.acquireRecoveryLock((Integer) invocation.getArguments()[0]);
+            }
+        });
+        PrivateAccessor.setField(s1.ns.getLastRevRecoveryAgent(), "missingLastRevUtil", mockedLongduringMissingLastRevUtil);
+
+        // so let's start the lastRevThread again and wait for that
+        // waitBeforeLocking semaphore to be hit
+        s1.startLastRevThread();
+        waitFor(new Expectation() {
+
+            @Override
+            public String fulfilled() throws Exception {
+                if (!waitBeforeLocking.hasQueuedThreads()) {
+                    return "no thread queued";
+                }
+                return null;
+            }
+
+        }, TEST_WAIT_TIMEOUT, "lastRevRecoveryThread should acquire a lock");
+
+        // at this stage the crashed s2 is still not in recovery mode, so let's
+        // check:
+        logger.info(s1.getClusterViewStr());
+        final ViewExpectation expectation1AfterCrashBeforeLastRevRecoveryLocking = new ViewExpectation(s1);
+        expectation1AfterCrashBeforeLastRevRecoveryLocking.setActiveIds(s1.ns.getClusterId(), s2.ns.getClusterId());
+        waitFor(expectation1AfterCrashBeforeLastRevRecoveryLocking, TEST_WAIT_TIMEOUT, "first should still see both as active");
+
+        // one thing, before we let the waitBeforeLocking go, setup the release
+        // semaphore/mock:
+        final Semaphore waitBeforeUnlocking = new Semaphore(0);
+        Mockito.doAnswer(new Answer<Void>() {
+            public Void answer(InvocationOnMock invocation) throws InterruptedException {
+                logger.info("Going to waitBeforeUnlocking");
+                waitBeforeUnlocking.acquire();
+                logger.info("Done with waitBeforeUnlocking");
+                missingLastRevUtil.releaseRecoveryLock((Integer) invocation.getArguments()[0]);
+                return null;
+            }
+        }).when(mockedLongduringMissingLastRevUtil).releaseRecoveryLock(anyInt());
+
+        // let go (or tschaedere loh)
+        waitBeforeLocking.release();
+
+        // then, right after we let the waitBeforeLocking semaphore go, we
+        // should see s2 in recovery mode
+        final ViewExpectation expectation1AfterCrashWhileLastRevRecoveryLocking = new ViewExpectation(s1);
+        expectation1AfterCrashWhileLastRevRecoveryLocking.setActiveIds(s1.ns.getClusterId());
+        expectation1AfterCrashWhileLastRevRecoveryLocking.setDeactivatingIds(s2.ns.getClusterId());
+        waitFor(expectation1AfterCrashWhileLastRevRecoveryLocking, TEST_WAIT_TIMEOUT, "first should still see s2 as recovering");
+
+        // ok, meanwhile, the lastRevRecoveryAgent should have hit the ot
+        waitFor(new Expectation() {
+
+            @Override
+            public String fulfilled() throws Exception {
+                if (!waitBeforeUnlocking.hasQueuedThreads()) {
+                    return "no thread queued";
+                }
+                return null;
+            }
+
+        }, TEST_WAIT_TIMEOUT, "lastRevRecoveryThread should want to release a lock");
+
+        // so then, we should still see the same state
+        waitFor(expectation1AfterCrashWhileLastRevRecoveryLocking, TEST_WAIT_TIMEOUT, "first should still see s2 as recovering");
+
+        logger.info("Waiting 1,5sec");
+        Thread.sleep(1500);
+        logger.info("Waiting done");
+
+        // first, lets check to see what the view looks like - should be
+        // unchanged:
+        waitFor(expectation1AfterCrashWhileLastRevRecoveryLocking, TEST_WAIT_TIMEOUT, "first should still see s2 as recovering");
+
+        // let waitBeforeUnlocking go
+        logger.info("releasing waitBeforeUnlocking, state: " + s1.getClusterViewStr());
+        waitBeforeUnlocking.release();
+        logger.info("released waitBeforeUnlocking");
+
+        if (!withBacklog) {
+            final ViewExpectation expectationWithoutBacklog = new ViewExpectation(s1);
+            expectationWithoutBacklog.setActiveIds(s1.ns.getClusterId());
+            expectationWithoutBacklog.setInactiveIds(s2.ns.getClusterId());
+            waitFor(expectationWithoutBacklog, TEST_WAIT_TIMEOUT, "finally we should see s2 as completely inactive");
+        } else {
+            // wait just 2 sec to see if the bgReadThread is really stopped
+            logger.info("sleeping 2 sec");
+            Thread.sleep(2000);
+            logger.info("sleeping 2 sec done, state: " + s1.getClusterViewStr());
+
+            // when that's the case, check the view - it should now be in a
+            // special 'final=false' mode
+            final ViewExpectation expectationBeforeBgRead = new ViewExpectation(s1);
+            expectationBeforeBgRead.setActiveIds(s1.ns.getClusterId());
+            expectationBeforeBgRead.setDeactivatingIds(s2.ns.getClusterId());
+            expectationBeforeBgRead.setFinal(false);
+            waitFor(expectationBeforeBgRead, TEST_WAIT_TIMEOUT, "first should only see itself after shutdown");
+
+            // ook, now we explicitly do a background read to get out of the
+            // backlog situation
+            s1.ns.runBackgroundReadOperations();
+
+            final ViewExpectation expectationAfterBgRead = new ViewExpectation(s1);
+            expectationAfterBgRead.setActiveIds(s1.ns.getClusterId());
+            expectationAfterBgRead.setInactiveIds(s2.ns.getClusterId());
+            waitFor(expectationAfterBgRead, TEST_WAIT_TIMEOUT, "finally we should see s2 as completely inactive");
+        }
+    }
+
+    /**
+     * This test creates a large number of documentnodestores which it starts,
+     * runs, stops in a random fashion, always testing to make sure the
+     * clusterView is correct
+     */
+    @Test
+    public void testLargeStartStopFiesta() throws Throwable {
+        final List<SimplifiedInstance> instances = new LinkedList<SimplifiedInstance>();
+        final Map<Integer, String> inactiveIds = new HashMap<Integer, String>();
+        final Random random = new Random();
+        final int LOOP_CNT = 50; // with too many loops have also seen mongo
+                                 // connections becoming starved thus test
+                                 // failed
+        final int CHECK_EVERY = 3;
+        final int MAX_NUM_INSTANCES = 8;
+        for (int i = 0; i < LOOP_CNT; i++) {
+            if (i % CHECK_EVERY == 0) {
+                checkFiestaState(instances, inactiveIds.keySet());
+            }
+            final int nextInt = random.nextInt(5);
+            // logger.info("testLargeStartStopFiesta: iteration "+i+" with case
+            // "+nextInt);
+            String workingDir = UUID.randomUUID().toString();
+            switch (nextInt) {
+                case 0: {
+                    // increase likelihood of creating instances..
+                    // but reuse an inactive one if possible
+                    if (inactiveIds.size() > 0) {
+                        logger.info("Case 0 - reactivating an instance...");
+                        final int n = random.nextInt(inactiveIds.size());
+                        final Integer cid = new LinkedList<Integer>(inactiveIds.keySet()).get(n);
+                        final String reactivatedWorkingDir = inactiveIds.remove(cid);
+                        if (reactivatedWorkingDir == null) {
+                            fail("reactivatedWorkingDir null for n=" + n + ", cid=" + cid + ", other inactives: " + inactiveIds);
+                        }
+                        assertNotNull(reactivatedWorkingDir);
+                        logger.info("Case 0 - reactivated instance " + cid + ", workingDir=" + reactivatedWorkingDir);
+                        workingDir = reactivatedWorkingDir;
+                        logger.info("Case 0: creating instance");
+                        final SimplifiedInstance newInstance = createInstance(workingDir);
+                        newInstance.setLeastTimeout(5000);
+                        newInstance.startSimulatingWrites(500);
+                        logger.info("Case 0: created instance: " + newInstance.ns.getClusterId());
+                        if (newInstance.ns.getClusterId() != cid) {
+                            logger.info(
+                                    "Case 0: reactivated instance did not take over cid - probably a testing artifact. expected cid: {}, actual cid: {}",
+                                    cid, newInstance.ns.getClusterId());
+                            inactiveIds.put(cid, reactivatedWorkingDir);
+                            // remove the newly reactivated from the inactives -
+                            // although it shouldn't be there, it might!
+                            inactiveIds.remove(newInstance.ns.getClusterId());
+                        }
+                        instances.add(newInstance);
+                    }
+                    break;
+                }
+                case 1: {
+                    // creates a new instance
+                    if (instances.size() < MAX_NUM_INSTANCES) {
+                        logger.info("Case 1: creating instance");
+                        final SimplifiedInstance newInstance = createInstance(workingDir);
+                        newInstance.setLeastTimeout(5000);
+                        newInstance.startSimulatingWrites(500);
+                        logger.info("Case 1: created instance: " + newInstance.ns.getClusterId());
+                        instances.add(newInstance);
+                    }
+                    break;
+                }
+                case 2: {
+                    // do nothing
+                    break;
+                }
+                case 3: {
+                    // shutdown instance
+                    if (instances.size() > 1) {
+                        // before shutting down: make sure we have a stable view
+                        // (we could otherwise not correctly startup too)
+                        checkFiestaState(instances, inactiveIds.keySet());
+                        final SimplifiedInstance instance = instances.remove(random.nextInt(instances.size()));
+                        assertNotNull(instance.workingDir);
+                        logger.info("Case 3: Shutdown instance: " + instance.ns.getClusterId());
+                        inactiveIds.put(instance.ns.getClusterId(), instance.workingDir);
+                        instance.shutdown();
+                    }
+                    break;
+                }
+                case 4: {
+                    // crash instance
+                    if (instances.size() > 1) {
+                        // before crashing make sure we have a stable view (we
+                        // could otherwise not correctly startup too)
+                        checkFiestaState(instances, inactiveIds.keySet());
+                        final SimplifiedInstance instance = instances.remove(random.nextInt(instances.size()));
+                        assertNotNull(instance.workingDir);
+                        logger.info("Case 4: Crashing instance: " + instance.ns.getClusterId());
+                        inactiveIds.put(instance.ns.getClusterId(), instance.workingDir);
+                        instance.addNode("/" + instance.ns.getClusterId() + "/stuffForRecovery/" + random.nextInt(10000));
+                        instance.crash();
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
+    private void dumpChildren(DocumentNodeState root) {
+        logger.info("testEmptyParentRecovery: root: " + root);
+        Iterator<String> it = root.getChildNodeNames().iterator();
+        while (it.hasNext()) {
+            String n = it.next();
+            logger.info("testEmptyParentRecovery: a child: '" + n + "'");
+        }
+    }
+
+    private void checkFiestaState(final List<SimplifiedInstance> instances, Set<Integer> inactiveIds) throws Exception {
+        final List<Integer> activeIds = new LinkedList<Integer>();
+        for (Iterator<SimplifiedInstance> it = instances.iterator(); it.hasNext();) {
+            SimplifiedInstance anInstance = it.next();
+            activeIds.add(anInstance.ns.getClusterId());
+        }
+        for (Iterator<SimplifiedInstance> it = instances.iterator(); it.hasNext();) {
+            SimplifiedInstance anInstance = it.next();
+
+            final ViewExpectation e = new ViewExpectation(anInstance);
+            e.setActiveIds(activeIds.toArray(new Integer[activeIds.size()]));
+            e.setInactiveIds(inactiveIds.toArray(new Integer[inactiveIds.size()]));
+            waitFor(e, 20000, "checkFiestaState failed for " + anInstance + ", with instances: " + instances + ", and inactiveIds: "
+                    + inactiveIds);
+        }
+    }
+
+    @Before
+    @After
+    public void clear() {
+        for (SimplifiedInstance i : allInstances) {
+            i.dispose();
+        }
+        for (DocumentMK mk : mks) {
+            mk.dispose();
+        }
+        mks.clear();
+        if (MONGO_DB) {
+            DB db = MongoUtils.getConnection().getDB();
+            MongoUtils.dropCollections(db);
+        }
+    }
+
+    private DocumentMK createMK(int clusterId, int asyncDelay) {
+        if (MONGO_DB) {
+            DB db = MongoUtils.getConnection().getDB();
+            return register(new DocumentMK.Builder().setMongoDB(db).setLeaseCheck(false).setClusterId(clusterId)
+                    .setAsyncDelay(asyncDelay).open());
+        } else {
+            if (ds == null) {
+                ds = new MemoryDocumentStore();
+            }
+            if (bs == null) {
+                bs = new MemoryBlobStore();
+            }
+            return createMK(clusterId, asyncDelay, ds, bs);
+        }
+    }
+
+    private DocumentMK createMK(int clusterId, int asyncDelay, DocumentStore ds, BlobStore bs) {
+        return register(new DocumentMK.Builder().setDocumentStore(ds).setBlobStore(bs).setClusterId(clusterId).setLeaseCheck(false)
+                .setAsyncDelay(asyncDelay).open());
+    }
+
+    private DocumentMK register(DocumentMK mk) {
+        mks.add(mk);
+        return mk;
+    }
+
+}

Propchange: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentDiscoveryLiteServiceTest.java
------------------------------------------------------------------------------
    svn:eol-style = native