blob: 2cef9f93f102fa4584b60d073e72cc74f5d641fd [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.document;
import java.util.List;
import java.util.Map;
import java.util.Random;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.DefaultNodeStateDiff;
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.apache.jackrabbit.oak.stats.Clock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.oak.spi.state.AbstractNodeState.comparePropertiesAgainstBaseState;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* A randomized test for _lastRev recovery.
*/
public class LastRevRecoveryRandomizedIT {
private static final Logger LOG = LoggerFactory.getLogger(LastRevRecoveryRandomizedIT.class);
private static final int SEED = Integer.getInteger(
LastRevRecoveryRandomizedIT.class.getSimpleName() + "-seed",
new Random().nextInt());
private Random random = new Random();
private MemoryDocumentStore store;
private DocumentNodeStore ns;
private Map<String, NodeState> currentState = Maps.newHashMap();
private DocumentRootBuilder builder;
private Map<String, NodeState> pending = Maps.newHashMap();
private int counter = 0;
private List<String> ops = Lists.newArrayList();
private Clock clock;
@Before
public void setUp() throws Exception {
LOG.info("Running " + getClass().getSimpleName() + " with seed " + SEED);
clock = new Clock.Virtual();
Revision.setClock(clock);
ClusterNodeInfo.setClock(clock);
random.setSeed(SEED);
store = new MemoryDocumentStore();
ns = new DocumentMK.Builder().setDocumentStore(store)
.setLeaseCheckMode(LeaseCheckMode.DISABLED).clock(clock)
.setAsyncDelay(0).getNodeStore();
builder = newBuilder(ns);
builder.child("root");
merge(ns, builder);
currentState.put("/root", ns.getRoot().getChildNode("root"));
builder = newBuilder(ns);
}
@After
public void tearDown() throws Exception {
Revision.resetClockToDefault();
ClusterNodeInfo.resetClockToDefault();
ns.dispose();
}
@Test
public void randomized() throws Exception {
for (int i = 0; i < 1000; i++) {
boolean success = false;
try {
switch (random.nextInt(10)) {
case 0:
case 1:
case 2:
addNode();
break;
case 3:
addLeafNode();
break;
case 4:
removeNode();
break;
case 5:
case 6:
setProperty();
break;
case 7:
merge();
break;
case 8:
purge();
break;
case 9:
bgOp();
break;
}
checkStore();
success = true;
} finally {
if (!success) {
int num = 0;
for (String line : ops) {
System.out.println("Seed: " + SEED);
System.out.println(num++ + ": " + line);
}
}
}
}
}
private void bgOp() {
ops.add("runBackgroundOperations()");
ns.runBackgroundOperations();
}
private void purge() {
ops.add("purge()");
builder.persist();
}
private void merge() throws CommitFailedException {
ops.add("merge()");
merge(ns, builder);
for (Map.Entry<String, NodeState> entry : pending.entrySet()) {
if (entry.getValue() == null) {
currentState.remove(entry.getKey());
} else {
currentState.put(entry.getKey(), entry.getValue());
}
}
pending.clear();
builder = newBuilder(ns);
}
private void setProperty() {
String p = choosePath();
String name = "p-" + counter++;
ops.add("setProperty() " + PathUtils.concat(p, name));
NodeBuilder ns = getNode(p);
ns.setProperty(name, "v");
pending.put(p, ns.getNodeState());
}
private void removeNode() {
String p = choosePath();
if (p.equals("/root")) {
return;
}
ops.add("removeNode() " + p);
getNode(p).remove();
pending.put(p, null);
}
private void addNode() {
String p = choosePath();
List<String> elements = Lists.newArrayList(PathUtils.elements(p));
if (elements.size() > 2) {
elements = elements.subList(1, elements.size() - 1);
elements = elements.subList(0, random.nextInt(elements.size() + 1));
p = PathUtils.concat("/root", elements.toArray(new String[elements.size()]));
}
String name = "n-" + counter++;
ops.add("addNode() " + PathUtils.concat(p, name));
NodeBuilder ns = getNode(p);
pending.put(PathUtils.concat(p, name), ns.child(name).getNodeState());
}
private void addLeafNode() {
String p = choosePath();
String name = "n-" + counter++;
ops.add("addLeafNode() " + PathUtils.concat(p, name));
NodeBuilder ns = getNode(p);
pending.put(PathUtils.concat(p, name), ns.child(name).getNodeState());
}
private NodeBuilder getNode(String path) {
NodeBuilder node = builder;
for (String name : PathUtils.elements(path)) {
node = node.getChildNode(name);
}
if (!node.exists()) {
throw new IllegalStateException("node does not exist: " + path);
}
return node;
}
private String choosePath() {
String path = "/root";
String next;
while ((next = chooseNode(path)) != null) {
path = next;
}
return path;
}
private String chooseNode(String parentPath) {
NodeBuilder node = getNode(parentPath);
int numChildren = (int) node.getChildNodeCount(Long.MAX_VALUE);
if (numChildren == 0) {
return null;
}
int k = random.nextInt(numChildren);
int c = 0;
for (String name : node.getChildNodeNames()) {
if (c++ == k) {
return PathUtils.concat(parentPath, name);
}
}
return null;
}
private void checkStore() {
MemoryDocumentStore s = store.copy();
// force lease expire
UpdateOp op = new UpdateOp(String.valueOf(ns.getClusterId()), false);
op.set(ClusterNodeInfo.LEASE_END_KEY, clock.getTime() - 1000);
if (s.findAndUpdate(Collection.CLUSTER_NODES, op) == null) {
fail("failed to set lease end");
}
// will trigger recovery on startup
DocumentNodeStore dns = new DocumentMK.Builder()
.setClusterId(ns.getClusterId())
.clock(clock).setLeaseCheckMode(LeaseCheckMode.DISABLED)
.setDocumentStore(s).setAsyncDelay(0).getNodeStore();
Map<String, NodeState> states = Maps.newHashMap(currentState);
NodeState root = dns.getRoot().getChildNode("root");
compareAndTraverse(root, "/root", states);
assertTrue("missing nodes: " + states.keySet() + " (seed=" + SEED + ")",
states.isEmpty());
dns.dispose();
}
private void compareAndTraverse(NodeState state,
final String path,
Map<String, NodeState> states) {
NodeState expected = states.remove(path);
if (expected == null) {
fail("unexpected node at " + path + " (seed=" + SEED + ")");
return;
}
comparePropertiesAgainstBaseState(state, expected, new DefaultNodeStateDiff() {
@Override
public boolean propertyAdded(PropertyState after) {
fail("unexpected property: " + path + "/" + after + " (seed=" + SEED + ")");
return super.propertyAdded(after);
}
@Override
public boolean propertyChanged(PropertyState before,
PropertyState after) {
assertEquals("property mismatch on node " + path + " (seed=" + SEED + ")",
before, after);
return super.propertyChanged(before, after);
}
@Override
public boolean propertyDeleted(PropertyState before) {
fail("missing property: " + path + "/" + before + " (seed=" + SEED + ")");
return super.propertyDeleted(before);
}
});
for (ChildNodeEntry entry : state.getChildNodeEntries()) {
String p = PathUtils.concat(path, entry.getName());
compareAndTraverse(entry.getNodeState(), p, states);
}
}
private static DocumentRootBuilder newBuilder(DocumentNodeStore store) {
return (DocumentRootBuilder) store.getRoot().builder();
}
private static void merge(NodeStore ns, NodeBuilder builder)
throws CommitFailedException {
ns.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
}
}