blob: 4b08e745ea686352ce8eabb5d7be0984185a355c [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.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.Lists;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
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.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.COLLISIONS;
import static org.apache.jackrabbit.oak.plugins.document.TestUtils.persistToBranch;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Test for OAK-2421.
*/
@RunWith(Parameterized.class)
public class OrphanedBranchTest {
private static final Logger LOG = LoggerFactory.getLogger(OrphanedBranchTest.class);
private DocumentStoreFixture fixture;
private DocumentNodeStore store;
private VersionGarbageCollector gc;
public OrphanedBranchTest(DocumentStoreFixture fixture) {
this.fixture = fixture;
}
@Parameterized.Parameters(name="{0}")
public static java.util.Collection<Object[]> fixtures() throws IOException {
List<Object[]> fixtures = Lists.newArrayList();
fixtures.add(new Object[] {new DocumentStoreFixture.MemoryFixture()});
DocumentStoreFixture mongo = new DocumentStoreFixture.MongoFixture();
if(mongo.isAvailable()){
fixtures.add(new Object[] {mongo});
}
return fixtures;
}
@Before
public void setUp() throws InterruptedException {
store = new DocumentMK.Builder()
.setDocumentStore(fixture.createDocumentStore())
.setAsyncDelay(0)
.getNodeStore();
gc = store.getVersionGarbageCollector();
}
@After
public void tearDown() throws Exception {
store.dispose();
fixture.dispose();
}
@Test
public void orphanedBranches() throws Exception {
int numCreated = 0;
for (;;) {
NodeBuilder builder = store.getRoot().builder();
NodeBuilder child = builder.child("foo");
int numBranches = store.getBranches().size();
int count = 0;
// perform changes until a branch is created
while (store.getBranches().size() == numBranches) {
child.setProperty("prop", count++);
persistToBranch(builder);
}
// but do not merge!
numCreated++;
// commit a change to get new head revision.
// collisions will only be cleaned up if the head
// revision is newer.
builder = store.getRoot().builder();
builder.child("bar").setProperty("prop", numCreated);
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
numBranches = store.getBranches().size();
store.runBackgroundOperations();
// after background ops we must not see more collisions
// than active branches
String id = Utils.getIdFromPath("/");
NodeDocument doc = store.getDocumentStore().find(NODES, id);
assertNotNull(doc);
Map<Revision, String> collisions = doc.getLocalMap(COLLISIONS);
assertTrue("too many collisions: " + collisions.size(),
collisions.size() <= numBranches);
// split ops must remove orphaned changes
// limit to check is number of branches considered active
// plus NodeDocument.NUM_REVS_THRESHOLD
int limit = numBranches + NodeDocument.NUM_REVS_THRESHOLD;
id = Utils.getIdFromPath("/foo");
doc = store.getDocumentStore().find(NODES, id);
assertNotNull(doc);
Map<Revision, String> map = doc.getLocalMap("prop");
assertTrue("too many orphaned changes: " + map.size() + " > " + limit,
map.size() <= limit);
map = doc.getLocalCommitRoot();
assertTrue("too many orphaned commit root entries: " + map.size() + " > " + limit,
map.size() <= limit);
Set<Revision> branchCommits = doc.getLocalBranchCommits();
assertTrue("too many orphaned branch commit entries: " + branchCommits.size() + " > " + limit,
branchCommits.size() <= limit);
// run garbage collector once in a while for changes on /bar
if (numCreated % NodeDocument.NUM_REVS_THRESHOLD == 0) {
gc.gc(1, TimeUnit.SECONDS);
}
LOG.info("created {}, still considered active: {}",
numCreated, store.getBranches().size());
// create orphaned branches, until we were able to
// collect 500 of them (or 100 for MongoDB and other fixtures)
int collect = fixture instanceof DocumentStoreFixture.MemoryFixture ? 500 : 100;
if (numCreated - store.getBranches().size() >= collect) {
break;
}
}
}
// OAK-2442
@Test
public void removeUncommittedChange() throws Exception {
String id = Utils.getIdFromPath("/foo");
NodeBuilder builder = store.getRoot().builder();
builder.child("foo");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
// force previous document
int count = 0;
while (store.getDocumentStore().find(NODES, id).getPreviousRanges().isEmpty()) {
builder = store.getRoot().builder();
builder.child("foo").setProperty("test", count++);
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
}
int numBranches = store.getBranches().size();
count = 0;
builder = store.getRoot().builder();
NodeBuilder child = builder.child("foo");
// perform changes until a branch is created
while (store.getBranches().size() == numBranches) {
child.setProperty("prop", count++);
persistToBranch(builder);
}
// perform some other change to update head revision
builder = store.getRoot().builder();
builder.child("foo").setProperty("bar", 0);
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
// force remove branch
NodeDocument doc = store.getDocumentStore().find(NODES, id);
assertNotNull(doc);
assertFalse(doc.getLocalBranchCommits().isEmpty());
Map<Revision, String> valueMap = doc.getLocalMap("prop");
assertFalse(valueMap.isEmpty());
UnmergedBranches branches = store.getBranches();
Revision branchRev = doc.getLocalMap("prop").firstKey();
Branch b = branches.getBranch(new RevisionVector(branchRev.asBranchRevision()));
assertNotNull(b);
branches.remove(b);
// force another previous document to trigger split
// this will also remove unmerged changes
count = 0;
while (store.getDocumentStore().find(NODES, id).getPreviousRanges().size() == 1) {
builder = store.getRoot().builder();
builder.child("foo").setProperty("p", count++);
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
}
doc = store.getDocumentStore().find(NODES, id);
assertTrue(doc.getLocalBranchCommits().isEmpty());
doc.getNodeAtRevision(store, store.getHeadRevision(), null);
store.dispose();
}
}