| /* |
| * 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 org.jetbrains.annotations.NotNull; |
| |
| 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); |
| } |
| |
| @NotNull |
| @Override |
| public NodeState getRoot() { |
| return getNodeStore().getRoot(); |
| } |
| |
| @NotNull |
| @Override |
| public NodeState merge(@NotNull NodeBuilder builder, @NotNull CommitHook commitHook, @NotNull CommitInfo info) throws CommitFailedException { |
| return getNodeStore(builder).merge(builder, commitHook, info); |
| } |
| |
| @NotNull |
| @Override |
| public NodeState rebase(@NotNull NodeBuilder builder) { |
| return getNodeStore(builder).rebase(builder); |
| } |
| |
| @Override |
| public NodeState reset(@NotNull NodeBuilder builder) { |
| return getNodeStore(builder).reset(builder); |
| } |
| |
| @NotNull |
| @Override |
| public Blob createBlob(InputStream inputStream) throws IOException { |
| return getNodeStore().createBlob(inputStream); |
| } |
| |
| @Override |
| public Blob getBlob(@NotNull String reference) { |
| return getNodeStore().getBlob(reference); |
| } |
| |
| @NotNull |
| @Override |
| public String checkpoint(long lifetime, @NotNull Map<String, String> properties) { |
| return getNodeStore().checkpoint(lifetime, properties); |
| } |
| |
| @NotNull |
| @Override |
| public String checkpoint(long lifetime) { |
| return getNodeStore().checkpoint(lifetime); |
| } |
| |
| @NotNull |
| @Override |
| public Map<String, String> checkpointInfo(@NotNull String checkpoint) { |
| return getNodeStore().checkpointInfo(checkpoint); |
| } |
| |
| @NotNull |
| @Override |
| public Iterable<String> checkpoints() { |
| return getNodeStore().checkpoints(); |
| } |
| |
| @Override |
| public NodeState retrieve(@NotNull String checkpoint) { |
| return getNodeStore().retrieve(checkpoint); |
| } |
| |
| @Override |
| public boolean release(@NotNull 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"; |
| } |
| } |
| |
| |
| } |