blob: ac37674ddff93fba29b38abbb95264ee53407064 [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.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.oak.commons.json.JsopReader;
import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
import org.apache.jackrabbit.oak.commons.sort.StringSort;
import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
import org.junit.Test;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.apache.jackrabbit.oak.plugins.document.Collection.JOURNAL;
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;
/**
* Tests for {@link JournalEntry}.
*/
public class JournalEntryTest {
@Test
public void applyTo() throws Exception {
DiffCache cache = new MemoryDiffCache(new DocumentMK.Builder());
List<Path> paths = Lists.newArrayList();
addRandomPaths(paths);
StringSort sort = JournalEntry.newSorter();
add(sort, paths);
RevisionVector from = new RevisionVector(new Revision(1, 0, 1));
RevisionVector to = new RevisionVector(new Revision(2, 0, 1));
sort.sort();
JournalEntry.applyTo(sort, cache, Path.ROOT, from, to);
for (Path p : paths) {
String changes = cache.getChanges(from, to, p, null);
assertNotNull("missing changes for " + p, changes);
for (String c : getChildren(changes)) {
assertTrue(paths.contains(new Path(p, c)));
}
}
sort.close();
}
@Test
public void applyToWithPath() throws Exception {
DiffCache cache = new MemoryDiffCache(new DocumentMK.Builder());
StringSort sort = JournalEntry.newSorter();
sort.add("/");
sort.add("/foo");
sort.add("/foo/a");
sort.add("/foo/b");
sort.add("/bar");
sort.add("/bar/a");
sort.add("/bar/b");
RevisionVector from = new RevisionVector(Revision.newRevision(1));
RevisionVector to = new RevisionVector(Revision.newRevision(1));
sort.sort();
JournalEntry.applyTo(sort, cache, p("/foo"), from, to);
assertNotNull(cache.getChanges(from, to, p("/foo"), null));
assertNotNull(cache.getChanges(from, to, p("/foo/a"), null));
assertNotNull(cache.getChanges(from, to, p("/foo/b"), null));
assertNull(cache.getChanges(from, to, p("/bar"), null));
assertNull(cache.getChanges(from, to, p("/bar/a"), null));
assertNull(cache.getChanges(from, to, p("/bar/b"), null));
}
//OAK-3494
@Test
public void useParentDiff() throws Exception {
DiffCache cache = new MemoryDiffCache(new DocumentMK.Builder());
RevisionVector from = new RevisionVector(new Revision(1, 0, 1));
RevisionVector to = new RevisionVector(new Revision(2, 0, 1));
RevisionVector unjournalled = new RevisionVector(new Revision(3, 0, 1));
//Put one entry for (from, to, "/a/b")->["c1", "c2"] manually
DiffCache.Entry entry = cache.newEntry(from, to, false);
entry.append(p("/a/b"), "^\"c1\":{}^\"c2\":{}");
entry.done();
//NOTE: calling validateCacheUsage fills the cache with an empty diff for the path being validated.
//So, we need to make sure that each validation is done on a separate path.
//Cases that cache can answer (only c1 and c2 sub-trees are possibly changed)
validateCacheUsage(cache, from, to, "/a/b/c3", true);
validateCacheUsage(cache, from, to, "/a/b/c4/e/f/g", true);
//Cases that cache can't answer
validateCacheUsage(cache, from, to, "/a/b/c1", false); //cached entry says that c1 sub-tree is changed
validateCacheUsage(cache, from, to, "/a/b/c2/d", false); //cached entry says that c2 sub-tree is changed
validateCacheUsage(cache, from, to, "/c", false);//there is no cache entry for the whole hierarchy
//Fill cache using journal
List<Path> paths = Lists.newArrayList(
p("/content/changed"),
p("/content/changed1/child1")
);
StringSort sort = JournalEntry.newSorter();
add(sort, paths);
sort.sort();
JournalEntry.applyTo(sort, cache, Path.ROOT, from, to);
validateCacheUsage(cache, from, to, "/topUnchanged", true);
validateCacheUsage(cache, from, to, "/content/changed/unchangedLeaf", true);
validateCacheUsage(cache, from, to, "/content/changed1/child2", true);
//check against an unjournalled revision (essentially empty cache)
validateCacheUsage(cache, from, unjournalled, "/unjournalledPath", false);
}
@Test
public void fillExternalChanges() throws Exception {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
Set<Path> paths = Sets.newHashSet();
addRandomPaths(paths);
entry.modified(paths);
Revision r1 = new Revision(1, 0, 1);
Revision r2 = new Revision(2, 0, 1);
Revision r3 = new Revision(3, 0, 1);
UpdateOp op = entry.asUpdateOp(r2);
assertTrue(store.create(JOURNAL, Collections.singletonList(op)));
StringSort sort = JournalEntry.newSorter();
StringSort inv = JournalEntry.newSorter();
JournalEntry.fillExternalChanges(sort, inv, r2, r3, store);
assertEquals(0, sort.getSize());
assertEquals(0, inv.getSize());
JournalEntry.fillExternalChanges(sort, inv, r1, r2, store);
assertEquals(paths.size(), sort.getSize());
assertEquals(0, inv.getSize());
sort.close();
sort = JournalEntry.newSorter();
JournalEntry.fillExternalChanges(sort, inv, r1, r3, store);
assertEquals(paths.size(), sort.getSize());
assertEquals(0, inv.getSize());
sort.close();
inv.close();
}
@Test
public void invalidateOnly() throws Exception {
DocumentStore store = new MemoryDocumentStore();
JournalEntry invalidateEntry = JOURNAL.newDocument(store);
Set<Path> paths = Sets.newHashSet();
addRandomPaths(paths);
invalidateEntry.modified(paths);
Revision r1 = new Revision(1, 0, 1);
Revision r2 = new Revision(2, 0, 1);
Revision r3 = new Revision(3, 0, 1);
UpdateOp op = invalidateEntry.asUpdateOp(r1.asBranchRevision());
assertTrue(store.create(JOURNAL, singletonList(op)));
JournalEntry entry = JOURNAL.newDocument(store);
entry.invalidate(singleton(r1));
op = entry.asUpdateOp(r2);
assertTrue(store.create(JOURNAL, singletonList(op)));
StringSort sort = JournalEntry.newSorter();
StringSort inv = JournalEntry.newSorter();
JournalEntry.fillExternalChanges(sort, inv, r2, r3, store);
assertEquals(0, sort.getSize());
assertEquals(0, inv.getSize());
JournalEntry.fillExternalChanges(sort, inv, r1, r2, store);
assertEquals(1, sort.getSize());
assertEquals(paths.size(), inv.getSize());
inv.close();
sort.close();
sort = JournalEntry.newSorter();
inv = JournalEntry.newSorter();
JournalEntry.fillExternalChanges(sort, inv, r1, r3, store);
assertEquals(1, sort.getSize());
assertEquals(paths.size(), inv.getSize());
sort.close();
inv.close();
}
@Test
public void fillExternalChanges2() throws Exception {
Revision r1 = new Revision(1, 0, 1);
Revision r2 = new Revision(2, 0, 1);
Revision r3 = new Revision(3, 0, 1);
Revision r4 = new Revision(4, 0, 1);
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
entry.modified(p("/"));
entry.modified(p("/foo"));
UpdateOp op = entry.asUpdateOp(r2);
assertTrue(store.create(JOURNAL, Collections.singletonList(op)));
entry = JOURNAL.newDocument(store);
entry.modified(p("/"));
entry.modified(p("/bar"));
op = entry.asUpdateOp(r4);
assertTrue(store.create(JOURNAL, Collections.singletonList(op)));
StringSort sort = externalChanges(r1, r1, store);
assertEquals(0, sort.getSize());
sort.close();
sort = externalChanges(r1, r2, store);
assertEquals(Sets.newHashSet("/", "/foo"), Sets.newHashSet(sort));
sort.close();
sort = externalChanges(r1, r3, store);
assertEquals(Sets.newHashSet("/", "/foo", "/bar"), Sets.newHashSet(sort));
sort.close();
sort = externalChanges(r1, r4, store);
assertEquals(Sets.newHashSet("/", "/foo", "/bar"), Sets.newHashSet(sort));
sort.close();
sort = externalChanges(r2, r2, store);
assertEquals(0, sort.getSize());
sort.close();
sort = externalChanges(r2, r3, store);
assertEquals(Sets.newHashSet("/", "/bar"), Sets.newHashSet(sort));
sort.close();
sort = externalChanges(r2, r4, store);
assertEquals(Sets.newHashSet("/", "/bar"), Sets.newHashSet(sort));
sort.close();
sort = externalChanges(r3, r3, store);
assertEquals(0, sort.getSize());
sort.close();
sort = externalChanges(r3, r4, store);
assertEquals(Sets.newHashSet("/", "/bar"), Sets.newHashSet(sort));
sort.close();
sort = externalChanges(r4, r4, store);
assertEquals(0, sort.getSize());
sort.close();
}
@Test
public void fillExternalChangesWithPath() throws Exception {
Revision r1 = new Revision(1, 0, 1);
Revision r2 = new Revision(2, 0, 1);
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
entry.modified(p("/"));
entry.modified(p("/foo"));
entry.modified(p("/foo/a"));
entry.modified(p("/foo/b"));
entry.modified(p("/foo/c"));
entry.modified(p("/bar"));
entry.modified(p("/bar/a"));
entry.modified(p("/bar/b"));
entry.modified(p("/bar/c"));
UpdateOp op = entry.asUpdateOp(r2);
assertTrue(store.create(JOURNAL, Collections.singletonList(op)));
StringSort sort = JournalEntry.newSorter();
StringSort inv = JournalEntry.newSorter();
JournalEntry.fillExternalChanges(sort, inv, p("/foo"), r1, r2, store, e -> {}, null, null);
assertEquals(4, sort.getSize());
assertEquals(0, inv.getSize());
sort.close();
inv.close();
}
@Test
public void getRevisionTimestamp() throws Exception {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
entry.modified(p("/foo"));
Revision r = Revision.newRevision(1);
assertTrue(store.create(JOURNAL,
Collections.singletonList(entry.asUpdateOp(r))));
entry = store.find(JOURNAL, JournalEntry.asId(r));
assertEquals(r.getTimestamp(), entry.getRevisionTimestamp());
}
// OAK-4682
@Test
public void concurrentModification() throws Exception {
DocumentNodeStore store = new DocumentMK.Builder().getNodeStore();
try {
final JournalEntry entry = store.getCurrentJournalEntry();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
entry.modified(p("/node-" + i));
}
}
});
t.start();
StringSort sort = JournalEntry.newSorter();
try {
entry.addTo(sort, Path.ROOT);
} finally {
sort.close();
}
t.join();
} finally {
store.dispose();
}
}
@Test
public void addToWithPath() throws Exception {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
entry.modified(p("/"));
entry.modified(p("/foo"));
entry.modified(p("/foo/a"));
entry.modified(p("/foo/b"));
entry.modified(p("/foo/c"));
entry.modified(p("/bar"));
entry.modified(p("/bar/a"));
entry.modified(p("/bar/b"));
entry.modified(p("/bar/c"));
StringSort sort = JournalEntry.newSorter();
entry.addTo(sort, p("/foo"));
assertEquals(4, sort.getSize());
sort.close();
}
@Test
public void countUpdatedPaths() {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
assertEquals("Incorrect number of initial paths", 0, entry.getNumChangedNodes());
assertFalse("Incorrect hasChanges", entry.hasChanges());
entry.modified(p("/foo"));
entry.modified(p("/bar"));
assertEquals("Incorrect number of paths", 2, entry.getNumChangedNodes());
assertTrue("Incorrect hasChanges", entry.hasChanges());
entry.modified(Arrays.asList(p("/foo1"), p("/bar1")));
assertEquals("Incorrect number of paths", 4, entry.getNumChangedNodes());
assertTrue("Incorrect hasChanges", entry.hasChanges());
entry.modified(p("/foo/bar2"));
assertEquals("Incorrect number of paths", 5, entry.getNumChangedNodes());
assertTrue("Incorrect hasChanges", entry.hasChanges());
entry.modified(p("/foo3/bar3"));
assertEquals("Incorrect number of paths", 7, entry.getNumChangedNodes());
assertTrue("Incorrect hasChanges", entry.hasChanges());
entry.modified(Arrays.asList(p("/foo/bar4"), p("/foo5/bar5")));
assertEquals("Incorrect number of paths", 10, entry.getNumChangedNodes());
assertTrue("Incorrect hasChanges", entry.hasChanges());
}
@Test
public void branchAdditionMarksChanges() {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
assertFalse("Incorrect hasChanges", entry.hasChanges());
entry.branchCommit(Collections.<Revision>emptyList());
assertFalse("Incorrect hasChanges", entry.hasChanges());
entry.branchCommit(Collections.singleton(Revision.fromString("r123-0-1")));
assertTrue("Incorrect hasChanges", entry.hasChanges());
}
@Test
public void invalidationPathsMarksChanges() {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
assertFalse("Incorrect hasChanges", entry.hasChanges());
entry.invalidate(Collections.emptyList());
assertFalse("Incorrect hasChanges", entry.hasChanges());
entry.invalidate(Collections.singleton(Revision.fromString("r123-0-1")));
assertTrue("Incorrect hasChanges", entry.hasChanges());
}
@Test
public void emptyBranchCommit() {
DocumentStore store = new MemoryDocumentStore();
JournalEntry entry = JOURNAL.newDocument(store);
entry.branchCommit(Collections.<Revision>emptyList());
assertFalse(entry.getBranchCommits().iterator().hasNext());
assertNull(entry.get(JournalEntry.BRANCH_COMMITS));
}
private static void addRandomPaths(java.util.Collection<Path> paths) throws IOException {
paths.add(Path.ROOT);
Random random = new Random(42);
for (int i = 0; i < 1000; i++) {
Path path = Path.ROOT;
int depth = random.nextInt(6);
for (int j = 0; j < depth; j++) {
char name = (char) ('a' + random.nextInt(26));
path = new Path(path, String.valueOf(name));
paths.add(path);
}
}
}
private static void add(StringSort sort, List<Path> paths)
throws IOException {
for (Path p : paths) {
sort.add(p.toString());
}
}
private static List<String> getChildren(String diff) {
List<String> children = Lists.newArrayList();
JsopTokenizer t = new JsopTokenizer(diff);
for (;;) {
int r = t.read();
switch (r) {
case '^': {
children.add(t.readString());
t.read(':');
t.read('{');
t.read('}');
break;
}
case JsopReader.END: {
return children;
}
default:
fail("Unexpected token: " + r);
}
}
}
private void validateCacheUsage(DiffCache cache,
RevisionVector from,
RevisionVector to,
String path,
boolean cacheExpected) {
Path p = p(path);
String nonLoaderDiff = cache.getChanges(from, to, p, null);
final AtomicBoolean loaderCalled = new AtomicBoolean(false);
cache.getChanges(from, to, p, new DiffCache.Loader() {
@Override
public String call() {
loaderCalled.set(true);
return "";
}
});
if (cacheExpected) {
assertNotNull(nonLoaderDiff);
assertFalse(loaderCalled.get());
} else {
assertNull(nonLoaderDiff);
assertTrue(loaderCalled.get());
}
}
private static StringSort externalChanges(Revision from,
Revision to,
DocumentStore store)
throws IOException {
StringSort changes = JournalEntry.newSorter();
StringSort invalidate = JournalEntry.newSorter();
try {
JournalEntry.fillExternalChanges(changes, invalidate, from, to, store);
} finally {
invalidate.close();
} return changes;
}
private static Path p(String path) {
return Path.fromString(path);
}
}