blob: c2cc5356decdf2dca7b0632ff8283fee11993bd8 [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 static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.VersionGCStats;
import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
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.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.stats.Clock;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class VersionGCDeletionTest {
private Clock clock;
private DocumentNodeStore store;
@Before
public void setUp() throws InterruptedException {
clock = new Clock.Virtual();
// baseline the clock
clock.waitUntil(System.currentTimeMillis());
Revision.setClock(clock);
}
@After
public void tearDown() {
if (store != null) {
store.dispose();
}
Revision.resetClockToDefault();
}
@Test
public void deleteParentLast() throws Exception{
TestDocumentStore ts = new TestDocumentStore();
store = new DocumentMK.Builder()
.clock(clock)
.setLeaseCheckMode(LeaseCheckMode.LENIENT)
.setDocumentStore(ts)
.setAsyncDelay(0)
.getNodeStore();
NodeBuilder b1 = store.getRoot().builder();
b1.child("x").child("y");
store.merge(b1, EmptyHook.INSTANCE, CommitInfo.EMPTY);
long maxAge = 1; //hours
long delta = TimeUnit.MINUTES.toMillis(10);
//Remove x/y
NodeBuilder b2 = store.getRoot().builder();
b2.child("x").remove();
store.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
//3. Check that deleted doc does get collected post maxAge
clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
VersionGarbageCollector gc = store.getVersionGarbageCollector();
//4. Ensure that while GC is being run /x gets removed but failure occurs
//for /x/y. At least attempt that! Once issue is fixed the list would be
//sorted again by VersionGC and then /x would always come after /x/y
try {
ts.throwException = true;
gc.gc(maxAge * 2, HOURS);
fail("Exception should be thrown");
} catch (AssertionError ignore) {
}
ts.throwException = false;
gc.gc(maxAge * 2, HOURS);
assertNull(ts.find(Collection.NODES, "2:/x/y"));
assertNull(ts.find(Collection.NODES, "1:/x"));
}
@Test
public void leaveResurrectedNodesAlone() throws Exception{
TestDocumentStore ts = new TestDocumentStore();
store = new DocumentMK.Builder()
.clock(clock)
.setLeaseCheckMode(LeaseCheckMode.LENIENT)
.setDocumentStore(ts)
.setAsyncDelay(0)
.getNodeStore();
String id = Utils.getIdFromPath("/x");
NodeBuilder b1 = store.getRoot().builder();
b1.child("x");
store.merge(b1, EmptyHook.INSTANCE, CommitInfo.EMPTY);
// Remove x
NodeBuilder b2 = store.getRoot().builder();
b2.child("x").remove();
store.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
NodeDocument d2 = ts.find(Collection.NODES, id, 0);
assertTrue(d2.wasDeletedOnce());
// Re-add x
NodeBuilder b3 = store.getRoot().builder();
b3.child("x");
store.merge(b3, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
NodeDocument d3 = ts.find(Collection.NODES, id, 0);
assertTrue(d3.wasDeletedOnce());
long maxAge = 1; //hours
long delta = TimeUnit.MINUTES.toMillis(10);
// 3. Check that resurrected doc does not get collected post maxAge
clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge * 2) + delta);
VersionGarbageCollector gc = store.getVersionGarbageCollector();
VersionGCStats stats = gc.gc(maxAge * 2, HOURS);
assertEquals(1, stats.updateResurrectedGCCount);
NodeDocument d4 = ts.find(Collection.NODES, id, 0);
assertNotNull(d4);
assertFalse(d4.wasDeletedOnce());
}
@Test
public void deleteLargeNumber() throws Exception{
int noOfDocsToDelete = 10000;
DocumentStore ts = new MemoryDocumentStore();
store = new DocumentMK.Builder()
.clock(clock)
.setLeaseCheckMode(LeaseCheckMode.LENIENT)
.setDocumentStore(new MemoryDocumentStore())
.setAsyncDelay(0)
.getNodeStore();
NodeBuilder b1 = store.getRoot().builder();
NodeBuilder xb = b1.child("x");
for (int i = 0; i < noOfDocsToDelete; i++){
xb.child("a"+i).child("b"+i);
}
store.merge(b1, EmptyHook.INSTANCE, CommitInfo.EMPTY);
long maxAge = 1; //hours
long delta = TimeUnit.MINUTES.toMillis(10);
//Remove x/y
NodeBuilder b2 = store.getRoot().builder();
b2.child("x").remove();
store.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
//3. Check that deleted doc does get collected post maxAge
clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
VersionGarbageCollector gc = store.getVersionGarbageCollector();
gc.setOptions(gc.getOptions().withOverflowToDiskThreshold(100));
VersionGCStats stats = gc.gc(maxAge * 2, HOURS);
assertEquals(noOfDocsToDelete * 2 + 1, stats.deletedDocGCCount);
assertEquals(noOfDocsToDelete, stats.deletedLeafDocGCCount);
assertNull(ts.find(Collection.NODES, "1:/x"));
for (int i = 0; i < noOfDocsToDelete; i++){
assertNull(ts.find(Collection.NODES, "2:/a"+i+"/b"+i));
assertNull(ts.find(Collection.NODES, "1:/a"+i));
}
}
@Test
public void gcWithPathsHavingNewLine() throws Exception{
int noOfDocsToDelete = 200;
DocumentStore ts = new MemoryDocumentStore();
store = new DocumentMK.Builder()
.clock(clock)
.setLeaseCheckMode(LeaseCheckMode.LENIENT)
.setDocumentStore(ts)
.setAsyncDelay(0)
.getNodeStore();
NodeBuilder b1 = store.getRoot().builder();
NodeBuilder xb = b1.child("x");
for (int i = 0; i < noOfDocsToDelete - 1; i++){
xb.child("a"+i).child("b"+i);
}
xb.child("a-1").child("b\r");
store.merge(b1, EmptyHook.INSTANCE, CommitInfo.EMPTY);
long maxAge = 1; //hours
long delta = TimeUnit.MINUTES.toMillis(10);
//Remove x/y
NodeBuilder b2 = store.getRoot().builder();
b2.child("x").remove();
store.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
//3. Check that deleted doc does get collected post maxAge
clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge*2) + delta);
VersionGarbageCollector gc = store.getVersionGarbageCollector();
gc.setOptions(gc.getOptions().withOverflowToDiskThreshold(100));
VersionGCStats stats = gc.gc(maxAge * 2, HOURS);
assertEquals(noOfDocsToDelete * 2 + 1, stats.deletedDocGCCount);
assertEquals(noOfDocsToDelete, stats.deletedLeafDocGCCount);
}
@Test
public void gcForPreviousDocs() throws Exception{
DocumentStore ts = new MemoryDocumentStore();
store = new DocumentMK.Builder()
.clock(clock)
.setLeaseCheckMode(LeaseCheckMode.LENIENT)
.setDocumentStore(ts)
.setAsyncDelay(0)
.getNodeStore();
NodeBuilder b1;
NodeBuilder xb;
//Create/remove "/x/split" sufficient times to split it
boolean create = true;
for (int i = 0; create || i < NodeDocument.NUM_REVS_THRESHOLD ; i++) {
b1 = store.getRoot().builder();
xb = b1.child("x").child("split");
if (!create) {
xb.remove();
}
store.merge(b1, EmptyHook.INSTANCE, CommitInfo.EMPTY);
create = !create;
}
store.runBackgroundOperations();
//Count split docs
NodeDocument doc = ts.find(Collection.NODES, "2:/x/split");
int splitDocCount = Iterators.size(doc.getAllPreviousDocs());
long maxAge = 1; //hours
long delta = TimeUnit.MINUTES.toMillis(10);
//Remove "/x"
NodeBuilder b2 = store.getRoot().builder();
b2.child("x").remove();
store.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
store.runBackgroundOperations();
//Pass some time and run GC
clock.waitUntil(clock.getTime() + HOURS.toMillis(maxAge * 2) + delta);
VersionGarbageCollector gc = store.getVersionGarbageCollector();
VersionGCStats stats = gc.gc(maxAge * 2, HOURS);
//Asset GC stats
assertEquals(2, stats.deletedDocGCCount);
assertEquals(splitDocCount, stats.splitDocGCCount);
//check if the deleted docs are really gone after GC
assertNull(ts.find(Collection.NODES, "1:/x"));
assertNull(ts.find(Collection.NODES, "2:/x/split"));
}
// OAK-2420
@Test
public void queryWhileDocsAreRemoved() throws Exception {
final Thread currentThread = Thread.currentThread();
final Semaphore queries = new Semaphore(0);
final CountDownLatch ready = new CountDownLatch(1);
MemoryDocumentStore ms = new MemoryDocumentStore() {
@NotNull
@Override
public <T extends Document> List<T> query(Collection<T> collection,
String fromKey,
String toKey,
int limit) {
if (collection == Collection.NODES
&& Thread.currentThread() != currentThread) {
ready.countDown();
queries.acquireUninterruptibly();
}
return super.query(collection, fromKey, toKey, limit);
}
};
store = new DocumentMK.Builder().clock(clock)
.setLeaseCheckMode(LeaseCheckMode.LENIENT)
.setDocumentStore(ms).setAsyncDelay(0).getNodeStore();
// create nodes
NodeBuilder builder = store.getRoot().builder();
NodeBuilder node = builder.child("node");
for (int i = 0; i < 200; i++) {
node.child("c-" + i);
}
merge(store, builder);
clock.waitUntil(clock.getTime() + HOURS.toMillis(1));
// remove nodes
builder = store.getRoot().builder();
node = builder.child("node");
for (int i = 0; i < 90; i++) {
node.getChildNode("c-" + i).remove();
}
merge(store, builder);
store.runBackgroundOperations();
clock.waitUntil(clock.getTime() + HOURS.toMillis(1));
List<String> expected = Lists.newArrayList();
// fill caches
NodeState n = store.getRoot().getChildNode("node");
for (ChildNodeEntry entry : n.getChildNodeEntries()) {
expected.add(entry.getName());
}
assertEquals(110, expected.size());
// invalidate the nodeChildren cache only
store.invalidateNodeChildrenCache();
Future<List<String>> f = newSingleThreadExecutor().submit(
new Callable<List<String>>() {
@Override
public List<String> call() {
List<String> names = Lists.newArrayList();
NodeState n = store.getRoot().getChildNode("node");
for (ChildNodeEntry entry : n.getChildNodeEntries()) {
names.add(entry.getName());
}
return names;
}
});
// run GC once the reader thread is collecting documents
ready.await();
VersionGarbageCollector gc = store.getVersionGarbageCollector();
VersionGCStats stats = gc.gc(30, MINUTES);
assertEquals(90, stats.deletedDocGCCount);
assertEquals(90, stats.deletedLeafDocGCCount);
queries.release(2);
List<String> names = f.get();
assertEquals(expected, names);
}
private void merge(DocumentNodeStore store, NodeBuilder builder)
throws CommitFailedException {
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
}
private static class TestDocumentStore extends MemoryDocumentStore {
boolean throwException;
@Override
public <T extends Document> void remove(Collection<T> collection, String key) {
if (throwException && "2:/x/y".equals(key)){
throw new AssertionError();
}
super.remove(collection, key);
}
@SuppressWarnings("unchecked")
@NotNull
@Override
public <T extends Document> List<T> query(Collection<T> collection, String fromKey,
String toKey, String indexedProperty, long startValue, int limit) {
List<T> result = super.query(collection, fromKey, toKey, indexedProperty, startValue, limit);
//Ensure that /x comes before /x/y
if (NodeDocument.DELETED_ONCE.equals(indexedProperty)){
Collections.sort((List<NodeDocument>)result, new NodeDocComparator());
}
return result;
}
}
/**
* Ensures that NodeDocument with path /x/y /x/y/z /x get sorted to
* /x /x/y /x/y/z
*/
private static class NodeDocComparator implements Comparator<NodeDocument> {
private static Comparator<String> reverse = Collections.reverseOrder(PathComparator.INSTANCE);
@Override
public int compare(NodeDocument o1, NodeDocument o2) {
return reverse.compare(o1.getPath(), o2.getPath());
}
}
}