blob: 815cb3c08313a50dcc55cb7ab59d85e10652c6c5 [file] [log] [blame]
/*
* 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";
}
}
}