Author: tomekr
Date: Tue Jun  6 11:30:38 2017
New Revision: 1797777

URL: http://svn.apache.org/viewvc?rev=1797777&view=rev
Log:
OAK-6220: Copy on write node store implementation

Added:
    
jackrabbit/oak/trunk/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/BranchNodeStore.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStore.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreService.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/cow/
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreTest.java
    jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/
    
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/COWStoreFixture.java
Modified:
    
jackrabbit/oak/trunk/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java
    
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java
    
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java
    
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java
    
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java

Added: 
jackrabbit/oak/trunk/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java?rev=1797777&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java
 Tue Jun  6 11:30:38 2017
@@ -0,0 +1,50 @@
+/*
+ * 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.api.jmx;
+
+import aQute.bnd.annotation.ProviderType;
+
+import javax.management.openmbean.TabularData;
+
+/**
+ * MBean for managing the copy-on-write node store
+ */
+@ProviderType
+public interface CopyOnWriteStoreMBean {
+    String TYPE = "CopyOnWriteStoreManager";
+
+    /**
+     * Enabled the temporary, copy-on-write store
+     * @return the operation status
+     */
+    String enableCopyOnWrite();
+
+    /**
+     * Disables the temporary store and switched the repository back to the 
"normal" mode.
+     * @return the operation status
+     */
+    String disableCopyOnWrite();
+
+    /**
+     * Returns the copy-on-write status
+     * @return status of the copy-on-write mode
+     */
+    String getStatus();
+}

Modified: 
jackrabbit/oak/trunk/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java?rev=1797777&r1=1797776&r2=1797777&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java
 Tue Jun  6 11:30:38 2017
@@ -43,7 +43,7 @@ public final class FixturesHelper {
      * default fixtures when no {@code nsfixtures} is provided
      */
     public enum Fixture {
-       DOCUMENT_NS, @Deprecated SEGMENT_MK, DOCUMENT_RDB, MEMORY_NS, 
DOCUMENT_MEM, SEGMENT_TAR, COMPOSITE_SEGMENT, COMPOSITE_MEM
+       DOCUMENT_NS, @Deprecated SEGMENT_MK, DOCUMENT_RDB, MEMORY_NS, 
DOCUMENT_MEM, SEGMENT_TAR, COMPOSITE_SEGMENT, COMPOSITE_MEM, COW_DOCUMENT
     }
 
     private static final Set<Fixture> FIXTURES;

Added: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/BranchNodeStore.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/BranchNodeStore.java?rev=1797777&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/BranchNodeStore.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/BranchNodeStore.java
 Tue Jun  6 11:30:38 2017
@@ -0,0 +1,184 @@
+/*
+ * 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.cow;
+
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitHook;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.Observable;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+
+import javax.annotation.Nonnull;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static com.google.common.collect.Iterables.addAll;
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Lists.newCopyOnWriteArrayList;
+import static com.google.common.collect.Maps.newConcurrentMap;
+import static com.google.common.collect.Maps.newHashMap;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.StreamSupport.stream;
+
+public class BranchNodeStore implements NodeStore, Observable {
+
+    private static final long CHECKPOINT_LIFETIME = 
TimeUnit.HOURS.toMillis(24);
+
+    private final NodeStore nodeStore;
+
+    private final MemoryNodeStore memoryNodeStore;
+
+    private final Collection<String> inheritedCheckpoints;
+
+    private final Map<String, String> checkpointMapping;
+
+    public BranchNodeStore(NodeStore nodeStore) throws CommitFailedException {
+        this.nodeStore = nodeStore;
+        this.inheritedCheckpoints = newArrayList(nodeStore.checkpoints());
+        this.checkpointMapping = newConcurrentMap();
+
+        String cp = nodeStore.checkpoint(CHECKPOINT_LIFETIME, 
singletonMap("type", "copy-on-write"));
+        memoryNodeStore = new MemoryNodeStore(nodeStore.retrieve(cp));
+    }
+
+    public void dispose() {
+        for (String cp : nodeStore.checkpoints()) {
+            if 
("copy-on-write".equals(nodeStore.checkpointInfo(cp).get("type"))) {
+                nodeStore.release(cp);
+            }
+        }
+    }
+
+    @Nonnull
+    @Override
+    public NodeState getRoot() {
+        return memoryNodeStore.getRoot();
+    }
+
+    @Nonnull
+    @Override
+    public synchronized NodeState merge(@Nonnull NodeBuilder builder, @Nonnull 
CommitHook commitHook, @Nonnull CommitInfo info) throws CommitFailedException {
+        return memoryNodeStore.merge(builder, commitHook, info);
+    }
+
+    @Nonnull
+    @Override
+    public NodeState rebase(@Nonnull NodeBuilder builder) {
+        return memoryNodeStore.rebase(builder);
+    }
+
+    @Override
+    public NodeState reset(@Nonnull NodeBuilder builder) {
+        return memoryNodeStore.reset(builder);
+    }
+
+    @Nonnull
+    @Override
+    public Blob createBlob(InputStream inputStream) throws IOException {
+        return memoryNodeStore.createBlob(inputStream);
+    }
+
+    @Override
+    public Blob getBlob(@Nonnull String reference) {
+        return memoryNodeStore.getBlob(reference);
+    }
+
+    @Nonnull
+    @Override
+    public String checkpoint(long lifetime, @Nonnull Map<String, String> 
properties) {
+        String checkpoint = memoryNodeStore.checkpoint(lifetime, properties);
+        String uuid = UUID.randomUUID().toString();
+        checkpointMapping.put(uuid, checkpoint);
+        return uuid;
+    }
+
+    @Nonnull
+    @Override
+    public String checkpoint(long lifetime) {
+        return checkpoint(lifetime, emptyMap());
+    }
+
+
+    @Nonnull
+    @Override
+    public Iterable<String> checkpoints() {
+        List<String> result = newArrayList(inheritedCheckpoints);
+        result.retainAll(newArrayList(nodeStore.checkpoints()));
+
+        checkpointMapping.entrySet().stream()
+                .filter(e -> 
memoryNodeStore.listCheckpoints().contains(e.getValue()))
+                .map(Map.Entry::getKey)
+                .forEach(result::add);
+
+        return result;
+    }
+
+    @Nonnull
+    @Override
+    public Map<String, String> checkpointInfo(@Nonnull String checkpoint) {
+        if (inheritedCheckpoints.contains(checkpoint)) {
+            return nodeStore.checkpointInfo(checkpoint);
+        } else if (checkpointMapping.containsKey(checkpoint)) {
+            return 
memoryNodeStore.checkpointInfo(checkpointMapping.get(checkpoint));
+        } else {
+            return emptyMap();
+        }
+    }
+
+    @Override
+    public NodeState retrieve(@Nonnull String checkpoint) {
+        if (inheritedCheckpoints.contains(checkpoint)) {
+            return nodeStore.retrieve(checkpoint);
+        } else if (checkpointMapping.containsKey(checkpoint)) {
+            return memoryNodeStore.retrieve(checkpointMapping.get(checkpoint));
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean release(@Nonnull String checkpoint) {
+        if (inheritedCheckpoints.contains(checkpoint)) {
+            return nodeStore.release(checkpoint);
+        } else if (checkpointMapping.containsKey(checkpoint)) {
+            return 
memoryNodeStore.release(checkpointMapping.remove(checkpoint));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public Closeable addObserver(Observer observer) {
+        return memoryNodeStore.addObserver(observer);
+    }
+}

Added: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStore.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStore.java?rev=1797777&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStore.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStore.java
 Tue Jun  6 11:30:38 2017
@@ -0,0 +1,216 @@
+/*
+ * 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.cow;
+
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.jmx.CopyOnWriteStoreMBean;
+import org.apache.jackrabbit.oak.spi.commit.CommitHook;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.commit.Observable;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+
+import javax.annotation.Nonnull;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * <p>The copy-on-write (COW) node store implementation allows to temporarily
+ * switch the repository into the "testing" mode, in which all the changes are
+ * stored in a volatile storage, namely the MemoryNodeStore. After switching
+ * back to the "production" mode, the test changes should be dropped.</p>
+ *
+ * <p>If the CoW is enabled, a special :cow=true property will be set on the
+ * root node returned by getRoot(). It's being used in the merge() to decide
+ * which store be modified. Removing this property will result in merging
+ * changes to the main node store, even in the CoW mode.</p>
+ *
+ * <p>The checkpoint support is provided by the {@link BranchNodeStore} class.
+ * All the existing checkpoints are still available in the CoW mode (until they
+ * expire). New checkpoints are only created in the MemoryNodeStore.</p>
+ *
+ * <p>Known limitations:</p>
+ *
+ * <ul>
+ *     <li>turning the CoW mode on and off requires cleaning up the
+ *     <a 
href="https://jackrabbit.apache.org/oak/docs/query/lucene.html#copy-on-read";>lucene
+ *     indexing cache</a>,</li>
+ *     <li>switching the CoW mode may result in repository inconsistencies
+ *     (eg. if two merges belongs to the same logical commit sequence),</li>
+ *     <li>in the CoW mode the changes are stored in MemoryNodeStore, so it
+ *     shouldn't be enabled for too long (otherwise it may exhaust the 
heap).</li>
+ * </ul>
+ */
+public class COWNodeStore implements NodeStore, Observable {
+
+    private final List<Observer> observers = new CopyOnWriteArrayList<>();
+
+    private final NodeStore store;
+
+    private volatile BranchNodeStore branchStore;
+
+    public COWNodeStore(NodeStore store) {
+        this.store = store;
+    }
+
+    public void enableCopyOnWrite() throws CommitFailedException {
+        BranchNodeStore branchStore = new BranchNodeStore(store);
+
+        NodeBuilder b = branchStore.getRoot().builder();
+        b.setProperty(":cow", true);
+        branchStore.merge(b, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        branchStore.addObserver((root, info) -> observers.stream().forEach(o 
-> o.contentChanged(root, info)));
+        this.branchStore = branchStore;
+    }
+
+    public void disableCopyOnWrite() {
+        BranchNodeStore branchStore = this.branchStore;
+        this.branchStore = null;
+        branchStore.dispose();
+    }
+
+    private NodeStore getNodeStore() {
+        NodeStore s = branchStore;
+        if (s == null) {
+            s = store;
+        }
+        return s;
+    }
+
+    private NodeStore getNodeStore(NodeBuilder builder) {
+        if (builder.hasProperty(":cow")) {
+            NodeStore s = branchStore;
+            if (s == null) {
+                throw new IllegalStateException("Node store for this builder 
is no longer available");
+            } else {
+                return s;
+            }
+        } else {
+            return store;
+        }
+    }
+
+    @Override
+    public Closeable addObserver(Observer observer) {
+        observer.contentChanged(getRoot(), CommitInfo.EMPTY_EXTERNAL);
+        observers.add(observer);
+        return () -> observers.remove(observer);
+    }
+
+    @Nonnull
+    @Override
+    public NodeState getRoot() {
+        return getNodeStore().getRoot();
+    }
+
+    @Nonnull
+    @Override
+    public NodeState merge(@Nonnull NodeBuilder builder, @Nonnull CommitHook 
commitHook, @Nonnull CommitInfo info) throws CommitFailedException {
+        return getNodeStore(builder).merge(builder, commitHook, info);
+    }
+
+    @Nonnull
+    @Override
+    public NodeState rebase(@Nonnull NodeBuilder builder) {
+        return getNodeStore(builder).rebase(builder);
+    }
+
+    @Override
+    public NodeState reset(@Nonnull NodeBuilder builder) {
+        return getNodeStore(builder).reset(builder);
+    }
+
+    @Nonnull
+    @Override
+    public Blob createBlob(InputStream inputStream) throws IOException {
+        return getNodeStore().createBlob(inputStream);
+    }
+
+    @Override
+    public Blob getBlob(@Nonnull String reference) {
+        return getNodeStore().getBlob(reference);
+    }
+
+    @Nonnull
+    @Override
+    public String checkpoint(long lifetime, @Nonnull Map<String, String> 
properties) {
+        return getNodeStore().checkpoint(lifetime, properties);
+    }
+
+    @Nonnull
+    @Override
+    public String checkpoint(long lifetime) {
+        return getNodeStore().checkpoint(lifetime);
+    }
+
+    @Nonnull
+    @Override
+    public Map<String, String> checkpointInfo(@Nonnull String checkpoint) {
+        return getNodeStore().checkpointInfo(checkpoint);
+    }
+
+    @Nonnull
+    @Override
+    public Iterable<String> checkpoints() {
+        return getNodeStore().checkpoints();
+    }
+
+    @Override
+    public NodeState retrieve(@Nonnull String checkpoint) {
+        return getNodeStore().retrieve(checkpoint);
+    }
+
+    @Override
+    public boolean release(@Nonnull String checkpoint) {
+        return getNodeStore().release(checkpoint);
+    }
+
+    class MBeanImpl implements CopyOnWriteStoreMBean {
+
+        @Override
+        public String enableCopyOnWrite() {
+            try {
+                COWNodeStore.this.enableCopyOnWrite();
+            } catch (CommitFailedException e) {
+                return "can't enable the copy on write: " + e.getMessage();
+            }
+            return "success";
+        }
+
+        @Override
+        public String disableCopyOnWrite() {
+            COWNodeStore.this.disableCopyOnWrite();
+            return "success";
+        }
+
+        @Override
+        public String getStatus() {
+            return branchStore == null ? "disabled" : "enabled";
+        }
+    }
+
+
+}

Added: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreService.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreService.java?rev=1797777&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreService.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreService.java
 Tue Jun  6 11:30:38 2017
@@ -0,0 +1,176 @@
+/*
+ * 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.cow;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.jackrabbit.oak.api.jmx.CopyOnWriteStoreMBean;
+import org.apache.jackrabbit.oak.commons.PropertiesUtil;
+import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
+import org.apache.jackrabbit.oak.spi.commit.ObserverTracker;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.state.NodeStoreProvider;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardExecutor;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+
+import static 
org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
+
+@Component(policy = ConfigurationPolicy.REQUIRE)
+public class COWNodeStoreService {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(COWNodeStoreService.class);
+
+    @Property(
+            label = "NodeStoreProvider role",
+            description = "Property indicating that this component will not 
register as a NodeStore but as a NodeStoreProvider with given role"
+    )
+    public static final String PROP_ROLE = "role";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = 
ReferencePolicy.DYNAMIC, target = "(role=copy-on-write)", bind = 
"bindNodeStoreProvider", unbind = "unbindNodeStoreProvider")
+    private NodeStoreProvider nodeStoreProvider;
+
+    private String nodeStoreDescription;
+
+    private ComponentContext context;
+
+    private ServiceRegistration nsReg;
+
+    private Registration mbeanReg;
+
+    private ObserverTracker observerTracker;
+
+    private Whiteboard whiteboard;
+
+    private WhiteboardExecutor executor;
+
+    private String role;
+
+    @Activate
+    protected void activate(ComponentContext context, Map<String, ?> config) {
+        this.role = PropertiesUtil.toString(config.get(PROP_ROLE), null);
+        this.context = context;
+        registerNodeStore();
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        unregisterNodeStore();
+    }
+
+    private void registerNodeStore() {
+        if (nsReg != null) {
+            return;
+        }
+        if (nodeStoreProvider == null) {
+            LOG.info("Waiting for the NodeStoreProvider with 
role=copy-on-write");
+            return;
+        }
+        if (context == null) {
+            LOG.info("Waiting for the component activation");
+            return;
+        }
+        COWNodeStore store = new 
COWNodeStore(nodeStoreProvider.getNodeStore());
+
+        whiteboard = new OsgiWhiteboard(context.getBundleContext());
+        executor = new WhiteboardExecutor();
+        executor.start(whiteboard);
+
+        mbeanReg = registerMBean(whiteboard,
+                CopyOnWriteStoreMBean.class,
+                store.new MBeanImpl(),
+                CopyOnWriteStoreMBean.TYPE,
+                "Copy-on-write: " + nodeStoreDescription);
+
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(Constants.SERVICE_PID, COWNodeStore.class.getName());
+        props.put("oak.nodestore.description", new 
String[]{"nodeStoreType=cowStore"});
+
+        if (role == null) {
+            LOG.info("Registering the COW node store");
+
+            observerTracker = new ObserverTracker(store);
+            observerTracker.start(context.getBundleContext());
+
+            nsReg = context.getBundleContext().registerService(
+                    new String[]{NodeStore.class.getName()},
+                    store,
+                    props
+            );
+        } else {
+            LOG.info("Registering the COW node store provider");
+
+            props.put("role", role);
+
+            nsReg = context.getBundleContext().registerService(
+                    new String[]{NodeStoreProvider.class.getName()},
+                    (NodeStoreProvider) () -> store,
+                    props
+            );
+        }
+    }
+
+    private void unregisterNodeStore() {
+        if (mbeanReg != null) {
+            mbeanReg.unregister();
+            mbeanReg = null;
+        }
+
+        if (executor != null) {
+            executor.stop();
+            executor = null;
+        }
+
+        if (observerTracker != null) {
+            observerTracker.stop();
+            observerTracker = null;
+        }
+
+        if (nsReg != null) {
+            LOG.info("Unregistering the COW node store");
+            nsReg.unregister();
+            nsReg = null;
+        }
+    }
+
+    protected void bindNodeStoreProvider(NodeStoreProvider ns, Map<String, ?> 
config) {
+        this.nodeStoreProvider = ns;
+        this.nodeStoreDescription = 
PropertiesUtil.toString(config.get("oak.nodestore.description"), 
ns.getClass().getName());
+        registerNodeStore();
+    }
+
+    protected void unbindNodeStoreProvider(NodeStoreProvider ns) {
+        this.nodeStoreProvider = null;
+        this.nodeStoreDescription = null;
+        unregisterNodeStore();
+    }
+}
\ No newline at end of file

Added: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreTest.java?rev=1797777&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreTest.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/cow/COWNodeStoreTest.java
 Tue Jun  6 11:30:38 2017
@@ -0,0 +1,107 @@
+/*
+ * 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.cow;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+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.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class COWNodeStoreTest {
+
+    private NodeStore store;
+
+    private COWNodeStore cowNodeStore;
+
+    @Before
+    public void createCowNodeStore() {
+        store = new MemoryNodeStore();
+        cowNodeStore = new COWNodeStore(store);
+    }
+
+    @Test
+    public void changesInCowMode() throws CommitFailedException {
+        NodeState root = cowNodeStore.getRoot();
+        NodeBuilder builder = root.builder();
+        builder.child("abc");
+        cowNodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        cowNodeStore.enableCopyOnWrite();
+
+        root = cowNodeStore.getRoot();
+        builder = root.builder();
+        builder.child("foo");
+        cowNodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        root = store.getRoot();
+        builder = root.builder();
+        builder.child("bar");
+        store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        assertTrue("Change introduced before enabling the CoW mode is not 
available", cowNodeStore.getRoot().hasChildNode("abc"));
+        assertTrue("Change introduced after enabling the CoW mode is not 
available", cowNodeStore.getRoot().hasChildNode("foo"));
+        assertFalse("Changed introduced to the main store after enabling the 
CoW mode shouldn't be visible", cowNodeStore.getRoot().hasChildNode("bar"));
+
+        assertTrue("Change introduced before enabling the CoW mode should be 
visible is the main store", store.getRoot().hasChildNode("abc"));
+        assertFalse("Change introduced after enabling the CoW mode shouldn't 
be visible in the main store", store.getRoot().hasChildNode("foo"));
+        assertTrue("Change introduced to the main store should be visible", 
store.getRoot().hasChildNode("bar"));
+
+        cowNodeStore.disableCopyOnWrite();
+
+        assertTrue("Change introduced before enabling the CoW mode is not 
available", cowNodeStore.getRoot().hasChildNode("abc"));
+        assertFalse("Change introduced in the CoW mode should be dropped after 
disabling it", cowNodeStore.getRoot().hasChildNode("foo"));
+        assertTrue("Change introduced to the main store should be visible", 
cowNodeStore.getRoot().hasChildNode("bar"));
+    }
+
+    @Test
+    public void checkpointsInCowMode() throws CommitFailedException {
+        String checkpoint1 = cowNodeStore.checkpoint(Long.MAX_VALUE, of("k", 
"v"));
+        cowNodeStore.enableCopyOnWrite();
+
+        Map<String, String> info = cowNodeStore.checkpointInfo(checkpoint1);
+        assertEquals("The checkpoint is not inherited", of("k", "v"), info);
+
+        NodeState root = cowNodeStore.getRoot();
+        NodeBuilder builder = root.builder();
+        builder.child("foo");
+        cowNodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        String checkpoint2 = cowNodeStore.checkpoint(Long.MAX_VALUE, of("k", 
"v2"));
+        info = cowNodeStore.checkpointInfo(checkpoint2);
+        assertEquals("The new checkpoint is not available", of("k", "v2"), 
info);
+        assertTrue("The retrieve() doesn't work for the new checkpoint", 
cowNodeStore.retrieve(checkpoint2).hasChildNode("foo"));
+        assertEquals(ImmutableList.of(checkpoint1, checkpoint2), 
cowNodeStore.checkpoints());
+
+        assertTrue("The new checkpoint shouldn't be stored in the main store", 
store.checkpointInfo(checkpoint2).isEmpty());
+
+        cowNodeStore.disableCopyOnWrite();
+        assertTrue("The new checkpoint should be dropped after disabling the 
CoW mode", cowNodeStore.checkpointInfo(checkpoint2).isEmpty());
+    }
+}

Modified: 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java?rev=1797777&r1=1797776&r2=1797777&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java
 Tue Jun  6 11:30:38 2017
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Set;
 
 import org.apache.jackrabbit.oak.commons.FixturesHelper;
+import org.apache.jackrabbit.oak.cow.COWStoreFixture;
 import org.apache.jackrabbit.oak.fixture.DocumentMemoryFixture;
 import org.apache.jackrabbit.oak.fixture.DocumentMongoFixture;
 import org.apache.jackrabbit.oak.fixture.DocumentRdbFixture;
@@ -48,6 +49,8 @@ public class NodeStoreFixtures {
 
     public static final NodeStoreFixture COMPOSITE_MEM = new 
CompositeMemoryStoreFixture();
 
+    public static final NodeStoreFixture COW_DOCUMENT = new COWStoreFixture();
+
     public static Collection<Object[]> 
asJunitParameters(Set<FixturesHelper.Fixture> fixtures) {
         List<NodeStoreFixture> configuredFixtures = new 
ArrayList<NodeStoreFixture>();
         if (fixtures.contains(FixturesHelper.Fixture.DOCUMENT_NS)) {
@@ -71,6 +74,9 @@ public class NodeStoreFixtures {
         if (fixtures.contains(FixturesHelper.Fixture.COMPOSITE_MEM)) {
             configuredFixtures.add(COMPOSITE_MEM);
         }
+        if (fixtures.contains(FixturesHelper.Fixture.COW_DOCUMENT)) {
+            configuredFixtures.add(COW_DOCUMENT);
+        }
 
         Collection<Object[]> result = new ArrayList<Object[]>();
         for (NodeStoreFixture f : configuredFixtures) {

Added: 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/COWStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/COWStoreFixture.java?rev=1797777&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/COWStoreFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/COWStoreFixture.java
 Tue Jun  6 11:30:38 2017
@@ -0,0 +1,45 @@
+/*
+ * 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.cow;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.plugins.cow.COWNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+
+public class COWStoreFixture extends NodeStoreFixture {
+
+    @Override
+    public NodeStore createNodeStore() {
+        COWNodeStore COWNodeStore = new COWNodeStore(new 
DocumentMK.Builder().getNodeStore());
+        try {
+            COWNodeStore.enableCopyOnWrite();
+        } catch (CommitFailedException e) {
+            throw new RuntimeException(e);
+        }
+        return COWNodeStore;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName();
+    }
+
+}
\ No newline at end of file

Modified: 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java?rev=1797777&r1=1797776&r2=1797777&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java
 Tue Jun  6 11:30:38 2017
@@ -42,12 +42,17 @@ import org.junit.Test;
 public class NodeStateTest extends OakBaseTest {
     private NodeState state;
 
+    private long initialCount;
+
     public NodeStateTest(NodeStoreFixture fixture) {
         super(fixture);
     }
 
     @Before
     public void setUp() throws CommitFailedException {
+        NodeState root = store.getRoot();
+        initialCount = root.getPropertyCount();
+
         NodeBuilder builder = store.getRoot().builder();
         builder.setProperty("a", 1);
         builder.setProperty("b", 2);
@@ -67,7 +72,7 @@ public class NodeStateTest extends OakBa
 
     @Test
     public void testGetPropertyCount() {
-        assertEquals(3, state.getPropertyCount());
+        assertEquals(3 + initialCount, state.getPropertyCount());
     }
 
     @Test
@@ -87,6 +92,9 @@ public class NodeStateTest extends OakBa
         List<String> names = new ArrayList<String>();
         List<Long> values = new ArrayList<Long>();
         for (PropertyState property : state.getProperties()) {
+            if (property.getName().startsWith(":")) {
+                continue;
+            }
             names.add(property.getName());
             values.add(property.getValue(LONG));
         }

Modified: 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java?rev=1797777&r1=1797776&r2=1797777&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java
 Tue Jun  6 11:30:38 2017
@@ -457,7 +457,8 @@ public class NodeStoreTest extends OakBa
         NodeBuilder test = store.getRoot().builder().getChildNode("test");
         NodeBuilder x = test.getChildNode("x");
         if (fixture == NodeStoreFixtures.SEGMENT_TAR || fixture == 
NodeStoreFixtures.MEMORY_NS 
-                || fixture == NodeStoreFixtures.COMPOSITE_MEM || fixture == 
NodeStoreFixtures.COMPOSITE_SEGMENT) {
+                || fixture == NodeStoreFixtures.COMPOSITE_MEM || fixture == 
NodeStoreFixtures.COMPOSITE_SEGMENT
+                || fixture == NodeStoreFixtures.COW_DOCUMENT) {
             assertTrue(x.moveTo(x, "xx"));
             assertFalse(x.exists());
             assertFalse(test.hasChildNode("x"));

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java?rev=1797777&r1=1797776&r2=1797777&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java
 Tue Jun  6 11:30:38 2017
@@ -58,7 +58,7 @@ public class CompositeNodeStoreService {
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = 
ReferencePolicy.STATIC)
     private MountInfoProvider mountInfoProvider;
 
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, policy = 
ReferencePolicy.DYNAMIC, bind = "bindNodeStore", unbind = "unbindNodeStore", 
referenceInterface = NodeStoreProvider.class)
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, policy = 
ReferencePolicy.DYNAMIC, bind = "bindNodeStore", unbind = "unbindNodeStore", 
referenceInterface = NodeStoreProvider.class, 
target="(!(service.pid=org.apache.jackrabbit.oak.composite.CompositeNodeStore))")
     private List<NodeStoreWithProps> nodeStores = new ArrayList<>();
 
     @Property(label = "Ignore read only writes",
@@ -158,6 +158,9 @@ public class CompositeNodeStoreService {
 
     private String getMountName(NodeStoreWithProps ns) {
         String role = ns.getRole();
+        if (role == null) {
+            return null;
+        }
         if (!role.startsWith(MOUNT_ROLE_PREFIX)) {
             return null;
         }


Reply via email to