blob: 182c2fa6e2951500b5851e65e5d7b169d61a068b [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.mongo;
import com.mongodb.MongoClient;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.document.AbstractMongoConnectionTest;
import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
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.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ClusterConflictTest extends AbstractMongoConnectionTest {
private DocumentNodeStore ns2;
@Override
public void setUpConnection() throws Exception {
super.setUpConnection();
MongoConnection connection = connectionFactory.getConnection();
ns2 = newBuilder(connection.getMongoClient(), connection.getDBName())
.setClusterId(2).getNodeStore();
}
@Override
protected DocumentMK.Builder newBuilder(MongoClient client, String dbName) throws Exception {
return super.newBuilder(client, dbName).setAsyncDelay(0).setLeaseCheckMode(LeaseCheckMode.DISABLED);
}
@Override
public void tearDownConnection() throws Exception {
ns2.dispose();
super.tearDownConnection();
}
// OAK-3433
@Test
public void mergeRetryWhileBackgroundRead() throws Exception {
DocumentNodeStore ns1 = mk.getNodeStore();
NodeBuilder b1 = ns1.getRoot().builder();
b1.child("a").child("b").child("c").child("foo");
merge(ns1, b1);
ns1.runBackgroundOperations();
ns2.runBackgroundOperations();
NodeBuilder b2 = ns2.getRoot().builder();
// force cache fill
assertNodeExists(b2, "/a/b/c/foo");
// remove /a/b/c on ns1
b1 = ns1.getRoot().builder();
b1.child("a").child("b").child("c").remove();
merge(ns1, b1);
// perform some change on ns2
b2.child("z");
merge(ns2, b2);
runBackgroundUpdate(ns2);
// this will pickup changes done by ns2 and update
// the head revision
runBackgroundRead(ns1);
// the next step is where the issue described
// in OAK-3433 occurs.
// the journal entry with changes done on ns1 is pushed
// with the current head revision, which is newer
// than the most recent change in the journal entry
runBackgroundUpdate(ns1);
// perform a background read after the rebase
// the first merge attempt will fail with a conflict
// because /a/b/c is seen as changed in the future
// without the fix for OAK-3433:
// the second merge attempt succeeds because now the
// /a/b/c change revision is visible and happened before the commit
// revision but before the base revision
b2 = ns2.getRoot().builder();
b2.child("z").setProperty("q", "v");
try {
ns2.merge(b2, new CommitHook() {
@NotNull
@Override
public NodeState processCommit(NodeState before,
NodeState after,
CommitInfo info)
throws CommitFailedException {
runBackgroundRead(ns2);
NodeBuilder builder = after.builder();
if (builder.getChildNode("a").getChildNode("b").hasChildNode("c")) {
builder.child("a").child("b").child("c").child("bar");
} else {
throw new CommitFailedException(
CommitFailedException.OAK, 0,
"/a/b/c does not exist anymore");
}
return builder.getNodeState();
}
}, CommitInfo.EMPTY);
fail("Merge must fail with CommitFailedException");
} catch (CommitFailedException e) {
// expected
}
}
private void assertNodeExists(NodeBuilder builder, String path) {
for (String name : PathUtils.elements(path)) {
builder = builder.getChildNode(name);
assertTrue("node '" + name + "' does not exist", builder.exists());
}
}
private static void merge(NodeStore store,
NodeBuilder builder) throws Exception {
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
}
}