diff --git a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java new file mode 100644 index 0000000..00716d9 --- /dev/null +++ b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/CopyOnWriteStoreMBean.java @@ -0,0 +1,51 @@ +/* + * 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, persisted under the given path + * @param rootPath + * @return the operation status + */ + String enableCopyOnWrite(String rootPath); + + /** + * Disables the temporary store and switched the repository back to the "normal" mode. + * @return the operation status + */ + String disableCopyOnWrite(); + + /** + * Returns the copy-on-write path or null if the feature is disabled + * @return repository root path or null if COW is disabled + */ + String getCopyOnWritePath(); +} diff --git a/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java index c54c18c..7835402 100644 --- a/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java +++ b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/FixturesHelper.java @@ -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_SEGMENT } private static final Set FIXTURES; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/CowNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/CowNodeStore.java new file mode 100644 index 0000000..f1e0a5c --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/CowNodeStore.java @@ -0,0 +1,398 @@ +/* + * 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.Sets; +import org.apache.jackrabbit.oak.api.Blob; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.api.jmx.CopyOnWriteStoreMBean; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; +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.ApplyDiff; +import org.apache.jackrabbit.oak.spi.state.ConflictAnnotatingRebaseDiff; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Lists.newLinkedList; +import static java.lang.System.currentTimeMillis; +import static org.apache.jackrabbit.oak.spi.state.NodeStateUtils.getNode; + +public class CowNodeStore implements NodeStore, Observable { + + private static final Logger LOG = LoggerFactory.getLogger(CowNodeStoreService.class); + + private static final String COW_PATH_PROPERTY = ":cow-repo-path"; + + private final List observers = new CopyOnWriteArrayList<>(); + + private final NodeStore store; + + private volatile String localRepoPath; + + public CowNodeStore(NodeStore store) { + this.store = store; + } + + public void setLocalRepoPath(String newLocalRepoPath, boolean dropPreviousRepo) throws CommitFailedException { + NodeState root = store.getRoot(); + NodeBuilder rootBuilder = root.builder(); + if (dropPreviousRepo && localRepoPath != null) { + getBuilder(rootBuilder, localRepoPath).remove(); + } + if (newLocalRepoPath == null || newLocalRepoPath.isEmpty()) { + localRepoPath = null; + return; + } + NodeBuilder newRepo = rootBuilder; + for (String segment : PathUtils.elements(newLocalRepoPath)) { + newRepo = newRepo.child(segment); + } + newRepo.setChildNode("root", root).setProperty(COW_PATH_PROPERTY, newLocalRepoPath, Type.STRING); + NodeBuilder checkpoints = newRepo.child("checkpoints"); + checkpoints.setProperty("inherited", store.checkpoints(), Type.STRINGS); + store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY); + localRepoPath = newLocalRepoPath; + } + + @Nonnull + @Override + public NodeState getRoot() { + String localRepoPath = this.localRepoPath; + if (localRepoPath == null) { + return store.getRoot(); + } else { + return getNode(store.getRoot(), localRepoPath).getChildNode("root"); + } + } + + @Nonnull + @Override + public NodeState merge(@Nonnull NodeBuilder builder, @Nonnull CommitHook commitHook, @Nonnull CommitInfo info) throws CommitFailedException { + String localRepoPath = builder.getString(COW_PATH_PROPERTY); + NodeState newNodeState; + if (localRepoPath == null) { + newNodeState = store.merge(builder, commitHook, info); + } else if (localRepoPath.equals(this.localRepoPath)) { + NodeState storeRoot = store.getRoot(); + + NodeState localRoot = getNode(storeRoot, localRepoPath).getChildNode("root"); + NodeState processedLocalRoot = commitHook.processCommit(localRoot, rebase(storeRoot, builder), info); + processedLocalRoot.compareAgainstBaseState(builder.getNodeState(), new ApplyDiff(builder)); + + NodeBuilder storeBuilder = storeRoot.builder(); + newNodeState = builder.getNodeState(); + getBuilder(storeBuilder, localRepoPath).setChildNode("root", newNodeState); + + store.merge(storeBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY); + + if (builder instanceof MemoryNodeBuilder) { + ((MemoryNodeBuilder) builder).reset(newNodeState); + } + } else { + throw new CommitFailedException("CowNodeStore", 31, "This COW repo path is not valid anymore: " + localRepoPath); + } + for (Observer o : observers) { + o.contentChanged(newNodeState, info); + } + return newNodeState; + } + + private NodeState rebase(@Nonnull NodeState storeRoot, @Nonnull NodeBuilder builder) { + if (builder instanceof MemoryNodeBuilder) { + String localRepoPath = builder.getString(COW_PATH_PROPERTY); + + NodeState baseState = builder.getBaseState(); + NodeState nodeState = builder.getNodeState(); + + if (baseState != storeRoot) { + ((MemoryNodeBuilder) builder).reset(getNode(storeRoot, localRepoPath).getChildNode("root")); + nodeState.compareAgainstBaseState(baseState, new ConflictAnnotatingRebaseDiff(builder)); + } + return builder.getNodeState(); + } else { + throw new IllegalArgumentException("The NodeBuilder should extend the MemoryNodeBuilder"); + } + } + + @Nonnull + @Override + public NodeState rebase(@Nonnull NodeBuilder builder) { + String repoPath = builder.getString(COW_PATH_PROPERTY); + if (repoPath == null) { + return store.rebase(builder); + } else { + return rebase(store.getRoot(), builder); + } + } + + @Override + public NodeState reset(@Nonnull NodeBuilder builder) { + String repoPath = builder.getString(COW_PATH_PROPERTY); + if (repoPath == null) { + return store.reset(builder); + } else if (builder instanceof MemoryNodeBuilder) { + NodeState localRoot = getNode(store.getRoot(), repoPath).getChildNode("root"); + ((MemoryNodeBuilder) builder).reset(localRoot); + return localRoot; + } else { + throw new IllegalArgumentException("The NodeBuilder should extend the MemoryNodeBuilder"); + } + } + + @Nonnull + @Override + public Blob createBlob(InputStream inputStream) throws IOException { + return store.createBlob(inputStream); + } + + @Override + public Blob getBlob(@Nonnull String reference) { + return store.getBlob(reference); + } + + @Nonnull + @Override + public String checkpoint(long lifetime, @Nonnull Map properties) { + String localRepoPath = this.localRepoPath; + if (localRepoPath == null) { + return store.checkpoint(lifetime, properties); + } else { + NodeState storeRoot = store.getRoot(); + NodeBuilder storeBuilder = storeRoot.builder(); + + NodeState localRoot = getNode(storeRoot, localRepoPath).getChildNode("root"); + NodeBuilder localCheckpoints = getBuilder(storeBuilder, localRepoPath).getChildNode("checkpoints"); + + String checkpointUuid = UUID.randomUUID().toString(); + checkpoint(localCheckpoints, checkpointUuid, localRoot, lifetime, properties); + try { + store.merge(storeBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY); + return checkpointUuid; + } catch (CommitFailedException e) { + return null; + } + } + } + + @Nonnull + @Override + public String checkpoint(long lifetime) { + return checkpoint(lifetime, Collections.emptyMap()); + } + + // TODO support lifetime + private void checkpoint(NodeBuilder checkpoints, String checkpoint, NodeState root, long lifetime, Map info) { + NodeBuilder newCheckpoint = checkpoints.child(checkpoint); + newCheckpoint.setChildNode("root", root); + newCheckpoint.setProperty("lifetime", lifetime); + newCheckpoint.setProperty("created", currentTimeMillis()); + + NodeBuilder infoBuilder = newCheckpoint.child("info"); + for (Map.Entry e : info.entrySet()) { + infoBuilder.setProperty(e.getKey(), e.getValue()); + } + } + + @Nonnull + @Override + public Map checkpointInfo(@Nonnull String checkpoint) { + String localRepoPath = this.localRepoPath; + if (localRepoPath == null) { + return store.checkpointInfo(checkpoint); + } else { + NodeState storeRoot = store.getRoot(); + if (isInherited(storeRoot, localRepoPath, checkpoint)) { + return store.checkpointInfo(checkpoint); + } + NodeState checkpoints = getNode(storeRoot, localRepoPath).getChildNode("checkpoints"); + if (!checkpointAvailable(checkpoints, checkpoint)) { + return Collections.emptyMap(); + } + NodeState checkpointInfo = checkpoints.getChildNode(checkpoint).getChildNode("info"); + Map infoMap = new LinkedHashMap<>(); + for (PropertyState s : checkpointInfo.getProperties()) { + infoMap.put(s.getName(), s.getValue(Type.STRING)); + } + return infoMap; + } + } + + @Nonnull + @Override + public Iterable checkpoints() { + String localRepoPath = this.localRepoPath; + if (localRepoPath == null) { + return store.checkpoints(); + } else { + NodeState storeRoot = store.getRoot(); + NodeState checkpoints = getNode(storeRoot, localRepoPath).getChildNode("checkpoints"); + + List checkpointList = newArrayList(store.checkpoints()); + checkpointList.retainAll(newArrayList(checkpoints.getStrings("inherited"))); + for (String c : checkpoints.getChildNodeNames()) { + if (checkpointAvailable(checkpoints, c)) { + checkpointList.add(c); + } + } + return checkpointList; + } + } + + @Override + public NodeState retrieve(@Nonnull String checkpoint) { + String localRepoPath = this.localRepoPath; + if (localRepoPath == null) { + return store.retrieve(checkpoint); + } else { + NodeState storeRoot = store.getRoot(); + if (isInherited(storeRoot, localRepoPath, checkpoint)) { + return store.retrieve(checkpoint); + } + + NodeState checkpoints = getNode(storeRoot, localRepoPath).getChildNode("checkpoints"); + if (!checkpointAvailable(checkpoints, checkpoint)) { + return null; + } + + NodeState checkpointRoot = checkpoints.getChildNode(checkpoint).getChildNode("root"); + if (checkpointRoot.exists()) { + return checkpointRoot; + } else { + return null; + } + } + } + + @Override + public boolean release(@Nonnull String checkpoint) { + String localRepoPath = this.localRepoPath; + if (localRepoPath == null) { + return store.release(checkpoint); + } else { + NodeState storeRoot = store.getRoot(); + NodeBuilder storeBuilder = storeRoot.builder(); + NodeBuilder checkpoints = getBuilder(storeBuilder, localRepoPath).getChildNode("checkpoints"); + + PropertyState ps = checkpoints.getProperty("inherited"); + List inherited = newLinkedList(ps.getValue(Type.STRINGS)); + boolean released = false; + if (inherited.contains(checkpoint)) { + inherited.remove(checkpoint); + checkpoints.setProperty("inherited", inherited, Type.STRINGS); + } else if (checkpoints.hasChildNode(checkpoint)) { + released = checkpointAvailable(checkpoints.getNodeState(), checkpoint); + checkpoints.getChildNode(checkpoint).remove(); + } else { + return false; + } + try { + store.merge(storeBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY); + return released; + } catch (CommitFailedException e) { + LOG.info("Can't release the checkpoint {}", checkpoint, e); + return false; + } + } + } + + @Override + public Closeable addObserver(Observer observer) { + observer.contentChanged(getRoot(), CommitInfo.EMPTY_EXTERNAL); + observers.add(observer); + return () -> observers.remove(observer); + } + + private NodeBuilder getBuilder(NodeBuilder root, String path) { + NodeBuilder builder = root; + for (String segment : PathUtils.elements(path)) { + builder = builder.getChildNode(segment); + } + return builder; + } + + private boolean isInherited(NodeState storeRoot, String localRepoPath, String checkpoint) { + NodeState checkpoints = getNode(storeRoot, localRepoPath).getChildNode("checkpoints"); + Set inherited = Sets.newHashSet(checkpoints.getStrings("inherited")); + Set storeCheckpoints = Sets.newHashSet(store.checkpoints()); + inherited.retainAll(storeCheckpoints); + return inherited.contains(checkpoint); + } + + private boolean checkpointAvailable(NodeState checkpoints, String checkpoint) { + NodeState cp = checkpoints.getChildNode(checkpoint); + if (!cp.exists()) { + return false; + } + long now = System.currentTimeMillis(); + long lifetime = cp.getProperty("lifetime").getValue(Type.LONG); + long created = cp.getProperty("created").getValue(Type.LONG); + return now - created < lifetime; + } + + class MBeanImpl implements CopyOnWriteStoreMBean { + + @Override + public String enableCopyOnWrite(String rootPath) { + try { + setLocalRepoPath(rootPath, true); + return "Success"; + } catch (CommitFailedException e) { + LOG.error("Can't enable the COW", e); + return "Failed: " + e.getMessage(); + } + } + + @Override + public String disableCopyOnWrite() { + try { + setLocalRepoPath(null, true); + return "Success"; + } catch (CommitFailedException e) { + LOG.error("Can't disable the COW", e); + return "Failed: " + e.getMessage(); + } + } + + @Override + public String getCopyOnWritePath() { + return localRepoPath; + } + } + +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/CowNodeStoreService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/CowNodeStoreService.java new file mode 100644 index 0000000..e395184 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/cow/CowNodeStoreService.java @@ -0,0 +1,177 @@ +/* + * 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.CacheStatsMBean; +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 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 props = new Hashtable(); + 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 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 diff --git a/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java b/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java index 59a7edc..9ac11ac 100644 --- a/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java +++ b/oak-it/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixtures.java @@ -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.CowSegmentStoreFixture; 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_SEGMENT = new CowSegmentStoreFixture(); + public static Collection asJunitParameters(Set fixtures) { List configuredFixtures = new ArrayList(); 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_SEGMENT)) { + configuredFixtures.add(COW_SEGMENT); + } Collection result = new ArrayList(); for (NodeStoreFixture f : configuredFixtures) { diff --git a/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/CowSegmentStoreFixture.java b/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/CowSegmentStoreFixture.java new file mode 100644 index 0000000..8b4aff4 --- /dev/null +++ b/oak-it/src/test/java/org/apache/jackrabbit/oak/cow/CowSegmentStoreFixture.java @@ -0,0 +1,49 @@ +/* + * 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.segment.SegmentNodeStoreBuilders; +import org.apache.jackrabbit.oak.segment.memory.MemoryStore; +import org.apache.jackrabbit.oak.spi.state.NodeStore; + +import java.io.IOException; + +public class CowSegmentStoreFixture extends NodeStoreFixture { + + @Override + public NodeStore createNodeStore() { + try { + NodeStore segmentNodeStore = SegmentNodeStoreBuilders.builder(new MemoryStore()).build(); + CowNodeStore cowNodeStore = new CowNodeStore(segmentNodeStore); + //cowNodeStore.setLocalRepoPath("/:cow", false); + return cowNodeStore; + } catch (IOException /*| CommitFailedException */e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} \ No newline at end of file diff --git a/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java b/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java index 9bae26d..c25e6d0 100644 --- a/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java +++ b/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStateTest.java @@ -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 OakBaseTest { @Test public void testGetPropertyCount() { - assertEquals(3, state.getPropertyCount()); + assertEquals(3 + initialCount, state.getPropertyCount()); } @Test @@ -87,6 +92,9 @@ public class NodeStateTest extends OakBaseTest { List names = new ArrayList(); List values = new ArrayList(); for (PropertyState property : state.getProperties()) { + if (property.getName().startsWith(":")) { + continue; + } names.add(property.getName()); values.add(property.getValue(LONG)); } diff --git a/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java b/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java index 0cbf0cc..500cab9 100644 --- a/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java +++ b/oak-it/src/test/java/org/apache/jackrabbit/oak/spi/state/NodeStoreTest.java @@ -457,7 +457,8 @@ public class NodeStoreTest extends OakBaseTest { 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_SEGMENT) { assertTrue(x.moveTo(x, "xx")); assertFalse(x.exists()); assertFalse(test.hasChildNode("x")); diff --git a/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java b/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java index 0a3783a..2360bde 100644 --- a/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java +++ b/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreService.java @@ -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 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; }