blob: ccc537557d936d425a48820a8de520aa2f0d551c [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.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.apache.commons.codec.binary.Hex;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.junit.LogCustomizer;
import org.apache.jackrabbit.oak.plugins.document.ClusterNodeInfo;
import org.apache.jackrabbit.oak.plugins.document.ClusterNodeInfoDocument;
import org.apache.jackrabbit.oak.plugins.document.Collection;
import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
import org.apache.jackrabbit.oak.plugins.document.DocumentStore;
import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
import org.apache.jackrabbit.oak.plugins.document.NodeDocument;
import org.apache.jackrabbit.oak.plugins.document.Path;
import org.apache.jackrabbit.oak.plugins.document.Revision;
import org.apache.jackrabbit.oak.plugins.document.RevisionVector;
import org.apache.jackrabbit.oak.plugins.document.UpdateOp;
import org.apache.jackrabbit.oak.plugins.document.UpdateUtils;
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.NodeBuilder;
import org.apache.jackrabbit.oak.stats.Clock;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import org.slf4j.event.Level;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
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.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for {@link Utils}.
*/
public class UtilsTest {
private static final long TIME_DIFF_WARN_THRESHOLD_MILLIS = 2000L;
@Test
public void getPreviousIdFor() {
Revision r = new Revision(System.currentTimeMillis(), 0, 0);
assertEquals("2:p/" + r.toString() + "/0",
Utils.getPreviousIdFor(Path.ROOT, r, 0));
assertEquals("3:p/test/" + r.toString() + "/1",
Utils.getPreviousIdFor(Path.fromString("/test"), r, 1));
assertEquals("15:p/a/b/c/d/e/f/g/h/i/j/k/l/m/" + r.toString() + "/3",
Utils.getPreviousIdFor(Path.fromString("/a/b/c/d/e/f/g/h/i/j/k/l/m"), r, 3));
}
@Test
public void previousDoc() throws Exception{
Revision r = new Revision(System.currentTimeMillis(), 0, 0);
assertTrue(Utils.isPreviousDocId(Utils.getPreviousIdFor(Path.ROOT, r, 0)));
assertTrue(Utils.isPreviousDocId(Utils.getPreviousIdFor(Path.fromString("/a/b/c/d/e/f/g/h/i/j/k/l/m"), r, 3)));
assertFalse(Utils.isPreviousDocId(Utils.getIdFromPath("/a/b")));
assertFalse(Utils.isPreviousDocId("foo"));
assertFalse(Utils.isPreviousDocId("0:"));
}
@Test
public void leafPreviousDoc() throws Exception {
Revision r = new Revision(System.currentTimeMillis(), 0, 0);
assertTrue(Utils.isLeafPreviousDocId(Utils.getPreviousIdFor(Path.ROOT, r, 0)));
assertTrue(Utils.isLeafPreviousDocId(Utils.getPreviousIdFor(Path.fromString("/a/b/c/d/e/f/g/h/i/j/k/l/m"), r, 0)));
assertFalse(Utils.isLeafPreviousDocId(Utils.getPreviousIdFor(Path.fromString("/a/b/c/d/e/f/g/h/i/j/k/l/m"), r, 3)));
assertFalse(Utils.isLeafPreviousDocId(Utils.getIdFromPath("/a/b")));
assertFalse(Utils.isLeafPreviousDocId("foo"));
assertFalse(Utils.isLeafPreviousDocId("0:"));
assertFalse(Utils.isLeafPreviousDocId(":/0"));
}
@Test
public void getParentIdFromLowerLimit() throws Exception{
assertEquals("1:/foo",Utils.getParentIdFromLowerLimit(Utils.getKeyLowerLimit(Path.fromString("/foo"))));
assertEquals("1:/foo",Utils.getParentIdFromLowerLimit("2:/foo/bar"));
}
@Test
public void getParentId() throws Exception{
Path longPath = Path.fromString(PathUtils.concat("/"+Strings.repeat("p", Utils.PATH_LONG + 1), "foo"));
assertTrue(Utils.isLongPath(longPath));
assertNull(Utils.getParentId(Utils.getIdFromPath(longPath)));
assertNull(Utils.getParentId(Utils.getIdFromPath(Path.ROOT)));
assertEquals("1:/foo", Utils.getParentId("2:/foo/bar"));
}
@Test
public void getDepthFromId() throws Exception{
assertEquals(1, Utils.getDepthFromId("1:/x"));
assertEquals(2, Utils.getDepthFromId("2:/x"));
assertEquals(10, Utils.getDepthFromId("10:/x"));
}
@Ignore("Performance test")
@Test
public void performance_getPreviousIdFor() {
Revision r = new Revision(System.currentTimeMillis(), 0, 0);
Path path = Path.fromString("/some/test/path/foo");
// warm up
for (int i = 0; i < 1 * 1000 * 1000; i++) {
Utils.getPreviousIdFor(path, r, 0);
}
long time = System.currentTimeMillis();
for (int i = 0; i < 10 * 1000 * 1000; i++) {
Utils.getPreviousIdFor(path, r, 0);
}
time = System.currentTimeMillis() - time;
System.out.println(time);
}
@Ignore("Performance test")
@Test
public void performance_revisionToString() {
for (int i = 0; i < 4; i++) {
performance_revisionToStringOne();
}
}
private static void performance_revisionToStringOne() {
Revision r = new Revision(System.currentTimeMillis(), 0, 0);
int dummy = 0;
long time = System.currentTimeMillis();
for (int i = 0; i < 30 * 1000 * 1000; i++) {
dummy += r.toString().length();
}
time = System.currentTimeMillis() - time;
System.out.println("time: " + time + " dummy " + dummy);
}
@Test
public void max() {
Revision a = new Revision(42, 0, 1);
Revision b = new Revision(43, 0, 1);
assertSame(b, Utils.max(a, b));
Revision a1 = new Revision(42, 1, 1);
assertSame(a1, Utils.max(a, a1));
assertSame(a, Utils.max(a, null));
assertSame(a, Utils.max(null, a));
assertNull(Utils.max(null, null));
}
@Test
public void min() {
Revision a = new Revision(42, 1, 1);
Revision b = new Revision(43, 0, 1);
assertSame(a, Utils.min(a, b));
Revision a1 = new Revision(42, 0, 1);
assertSame(a1, Utils.min(a, a1));
assertSame(a, Utils.min(a, null));
assertSame(a, Utils.min(null, a));
assertNull(Utils.max(null, null));
}
@Test
public void getAllDocuments() throws CommitFailedException {
DocumentNodeStore store = new DocumentMK.Builder().getNodeStore();
try {
NodeBuilder builder = store.getRoot().builder();
for (int i = 0; i < 1000; i++) {
builder.child("test-" + i);
}
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
assertEquals(1001 /* root + 1000 children */, Iterables.size(
Utils.getAllDocuments(store.getDocumentStore())));
} finally {
store.dispose();
}
}
@Test
public void getMaxExternalRevisionTime() {
int localClusterId = 1;
List<Revision> revs = ImmutableList.of();
long revTime = Utils.getMaxExternalTimestamp(revs, localClusterId);
assertEquals(Long.MIN_VALUE, revTime);
revs = ImmutableList.of(Revision.fromString("r1-0-1"));
revTime = Utils.getMaxExternalTimestamp(revs, localClusterId);
assertEquals(Long.MIN_VALUE, revTime);
revs = ImmutableList.of(
Revision.fromString("r1-0-1"),
Revision.fromString("r2-0-2"));
revTime = Utils.getMaxExternalTimestamp(revs, localClusterId);
assertEquals(2, revTime);
revs = ImmutableList.of(
Revision.fromString("r3-0-1"),
Revision.fromString("r2-0-2"));
revTime = Utils.getMaxExternalTimestamp(revs, localClusterId);
assertEquals(2, revTime);
revs = ImmutableList.of(
Revision.fromString("r1-0-1"),
Revision.fromString("r2-0-2"),
Revision.fromString("r2-0-3"));
revTime = Utils.getMaxExternalTimestamp(revs, localClusterId);
assertEquals(2, revTime);
revs = ImmutableList.of(
Revision.fromString("r1-0-1"),
Revision.fromString("r3-0-2"),
Revision.fromString("r2-0-3"));
revTime = Utils.getMaxExternalTimestamp(revs, localClusterId);
assertEquals(3, revTime);
}
@Test
public void getMinTimestampForDiff() {
RevisionVector from = new RevisionVector(new Revision(17, 0, 1));
RevisionVector to = new RevisionVector(new Revision(19, 0, 1));
assertEquals(17, Utils.getMinTimestampForDiff(from, to, new RevisionVector()));
assertEquals(17, Utils.getMinTimestampForDiff(to, from, new RevisionVector()));
RevisionVector minRevs = new RevisionVector(
new Revision(7, 0, 1),
new Revision(4, 0, 2));
assertEquals(17, Utils.getMinTimestampForDiff(from, to, minRevs));
assertEquals(17, Utils.getMinTimestampForDiff(to, from, minRevs));
to = to.update(new Revision(15, 0, 2));
// must return min revision of clusterId 2
assertEquals(4, Utils.getMinTimestampForDiff(from, to, minRevs));
assertEquals(4, Utils.getMinTimestampForDiff(to, from, minRevs));
}
@Test(expected = IllegalArgumentException.class)
public void getDepthFromIdIllegalArgumentException1() {
Utils.getDepthFromId("a:/foo");
}
@Test(expected = IllegalArgumentException.class)
public void getDepthFromIdIllegalArgumentException2() {
Utils.getDepthFromId("42");
}
@Test
public void alignWithExternalRevisions() throws Exception {
Clock c = new Clock.Virtual();
c.waitUntil(System.currentTimeMillis());
// past
Revision lastRev1 = new Revision(c.getTime() - 1000, 0, 1);
// future
Revision lastRev2 = new Revision(c.getTime() + 1000, 0, 2);
// create a root document
NodeDocument doc = new NodeDocument(new MemoryDocumentStore(), c.getTime());
UpdateOp op = new UpdateOp(Utils.getIdFromPath("/"), true);
NodeDocument.setLastRev(op, lastRev1);
NodeDocument.setLastRev(op, lastRev2);
UpdateUtils.applyChanges(doc, op);
// must not wait even if revision is in the future
Utils.alignWithExternalRevisions(doc, c, 2, TIME_DIFF_WARN_THRESHOLD_MILLIS);
assertThat(c.getTime(), is(lessThan(lastRev2.getTimestamp())));
// must wait until after lastRev2 timestamp
Utils.alignWithExternalRevisions(doc, c, 1, TIME_DIFF_WARN_THRESHOLD_MILLIS);
assertThat(c.getTime(), is(greaterThan(lastRev2.getTimestamp())));
}
@Test
public void warnOnClockDifferences() throws Exception {
Clock c = new Clock.Virtual();
c.waitUntil(System.currentTimeMillis());
// local
Revision lastRev1 = new Revision(c.getTime(), 0, 1);
// other in the future
Revision lastRev2 = new Revision(c.getTime() + 5000, 0, 2);
// create a root document
NodeDocument doc = new NodeDocument(new MemoryDocumentStore(), c.getTime());
UpdateOp op = new UpdateOp(Utils.getIdFromPath("/"), true);
NodeDocument.setLastRev(op, lastRev1);
NodeDocument.setLastRev(op, lastRev2);
UpdateUtils.applyChanges(doc, op);
LogCustomizer customizer = LogCustomizer.forLogger(Utils.class).enable(Level.WARN).create();
customizer.starting();
try {
// must wait until after lastRev2 timestamp
Utils.alignWithExternalRevisions(doc, c, 1, TIME_DIFF_WARN_THRESHOLD_MILLIS);
assertThat(c.getTime(), is(greaterThan(lastRev2.getTimestamp())));
assertFalse(customizer.getLogs().isEmpty());
assertThat(customizer.getLogs().iterator().next(), containsString("Detected clock differences"));
} finally {
customizer.finished();
}
}
@Test
public void noWarnOnMinorClockDifferences() throws Exception {
Clock c = new Clock.Virtual();
c.waitUntil(System.currentTimeMillis());
// local
Revision lastRev1 = new Revision(c.getTime(), 0, 1);
// other slightly in the future
Revision lastRev2 = new Revision(c.getTime() + 100, 0, 2);
// create a root document
NodeDocument doc = new NodeDocument(new MemoryDocumentStore(), c.getTime());
UpdateOp op = new UpdateOp(Utils.getIdFromPath("/"), true);
NodeDocument.setLastRev(op, lastRev1);
NodeDocument.setLastRev(op, lastRev2);
UpdateUtils.applyChanges(doc, op);
LogCustomizer customizer = LogCustomizer.forLogger(Utils.class).enable(Level.WARN).create();
customizer.starting();
try {
// must wait until after lastRev2 timestamp
Utils.alignWithExternalRevisions(doc, c, 1, TIME_DIFF_WARN_THRESHOLD_MILLIS);
assertThat(c.getTime(), is(greaterThan(lastRev2.getTimestamp())));
assertThat(customizer.getLogs(), empty());
} finally {
customizer.finished();
}
}
@Test
public void noWarnWithSingleClusterId() throws Exception {
Clock c = new Clock.Virtual();
c.waitUntil(System.currentTimeMillis());
// local
Revision lastRev1 = new Revision(c.getTime(), 0, 1);
// create a root document
NodeDocument doc = new NodeDocument(new MemoryDocumentStore(), c.getTime());
UpdateOp op = new UpdateOp(Utils.getIdFromPath("/"), true);
NodeDocument.setLastRev(op, lastRev1);
UpdateUtils.applyChanges(doc, op);
LogCustomizer customizer = LogCustomizer.forLogger(Utils.class).enable(Level.WARN).create();
customizer.starting();
try {
Utils.alignWithExternalRevisions(doc, c, 1, TIME_DIFF_WARN_THRESHOLD_MILLIS);
assertThat(customizer.getLogs(), empty());
} finally {
customizer.finished();
}
}
@Test
public void isIdFromLongPath() {
Path path = Path.fromString("/test");
while (!Utils.isLongPath(path)) {
path = new Path(path, path.getName());
}
String idFromLongPath = Utils.getIdFromPath(path);
assertTrue(Utils.isIdFromLongPath(idFromLongPath));
assertFalse(Utils.isIdFromLongPath("foo"));
assertFalse(Utils.isIdFromLongPath(NodeDocument.MIN_ID_VALUE));
assertFalse(Utils.isIdFromLongPath(NodeDocument.MAX_ID_VALUE));
assertFalse(Utils.isIdFromLongPath(":"));
}
@Test
public void idDepth() {
assertEquals(0, Utils.getIdDepth(Path.ROOT));
assertEquals(0, Utils.getIdDepth(Path.fromString("a")));
assertEquals(1, Utils.getIdDepth(Path.fromString("/a")));
assertEquals(2, Utils.getIdDepth(Path.fromString("/a/b")));
assertEquals(3, Utils.getIdDepth(Path.fromString("/a/b/c")));
assertEquals(2, Utils.getIdDepth(Path.fromString("a/b/c")));
}
@Test
public void encodeHexString() {
Random r = new Random(42);
for (int i = 0; i < 1000; i++) {
int len = r.nextInt(100);
byte[] data = new byte[len];
r.nextBytes(data);
// compare against commons codec implementation
assertEquals(Hex.encodeHexString(data),
Utils.encodeHexString(data, new StringBuilder()).toString());
}
}
@Test
public void isLocalChange() {
RevisionVector empty = new RevisionVector();
Revision r11 = Revision.fromString("r1-0-1");
Revision r21 = Revision.fromString("r2-0-1");
Revision r12 = Revision.fromString("r1-0-2");
Revision r22 = Revision.fromString("r2-0-2");
assertFalse(Utils.isLocalChange(empty, empty, 1));
assertTrue(Utils.isLocalChange(empty, new RevisionVector(r11), 1));
assertFalse(Utils.isLocalChange(empty, new RevisionVector(r11), 0));
assertFalse(Utils.isLocalChange(new RevisionVector(r11), new RevisionVector(r11), 1));
assertTrue(Utils.isLocalChange(new RevisionVector(r11), new RevisionVector(r21), 1));
assertFalse(Utils.isLocalChange(new RevisionVector(r11), new RevisionVector(r11, r12), 1));
assertFalse(Utils.isLocalChange(new RevisionVector(r11, r12), new RevisionVector(r11, r12), 1));
assertFalse(Utils.isLocalChange(new RevisionVector(r11, r12), new RevisionVector(r11, r22), 1));
assertFalse(Utils.isLocalChange(new RevisionVector(r11, r12), new RevisionVector(r21, r22), 1));
assertTrue(Utils.isLocalChange(new RevisionVector(r11, r12), new RevisionVector(r21, r12), 1));
}
@Test
public void abortingIterableIsCloseable() throws Exception {
AtomicBoolean closed = new AtomicBoolean(false);
Iterable<String> iterable = CloseableIterable.wrap(
Collections.emptyList(), () -> closed.set(true));
Utils.closeIfCloseable(Utils.abortingIterable(iterable, s -> true));
assertTrue(closed.get());
}
@Test
public void checkRevisionAge() throws Exception {
DocumentStore store = new MemoryDocumentStore();
ClusterNodeInfo info = mock(ClusterNodeInfo.class);
when(info.getId()).thenReturn(2);
Clock clock = new Clock.Virtual();
clock.waitUntil(System.currentTimeMillis());
// store is empty -> fine
Utils.checkRevisionAge(store, info, clock);
UpdateOp op = new UpdateOp(Utils.getIdFromPath("/"), true);
NodeDocument.setLastRev(op, new Revision(clock.getTime(), 0, 1));
assertTrue(store.create(Collection.NODES, Collections.singletonList(op)));
// root document does not have a lastRev entry for clusterId 2
Utils.checkRevisionAge(store, info, clock);
long lastRevTime = clock.getTime();
op = new UpdateOp(Utils.getIdFromPath("/"), false);
NodeDocument.setLastRev(op, new Revision(lastRevTime, 0, 2));
assertNotNull(store.findAndUpdate(Collection.NODES, op));
// lastRev entry for clusterId 2 is older than current time
Utils.checkRevisionAge(store, info, clock);
// rewind time
clock = new Clock.Virtual();
clock.waitUntil(lastRevTime - 1000);
try {
// now the check must fail
Utils.checkRevisionAge(store, info, clock);
fail("must fail with DocumentStoreException");
} catch (DocumentStoreException e) {
assertThat(e.getMessage(), containsString("newer than current time"));
}
}
@Test
public void getStartRevisionsEmpty() {
RevisionVector rv = Utils.getStartRevisions(Collections.emptyList());
assertEquals(0, rv.getDimensions());
}
@Test
public void getStartRevisionsSingleNode() {
int clusterId = 1;
long now = System.currentTimeMillis();
ClusterNodeInfoDocument info = mockedClusterNodeInfo(clusterId, now);
RevisionVector rv = Utils.getStartRevisions(Collections.singleton(info));
assertEquals(1, rv.getDimensions());
Revision r = rv.getRevision(clusterId);
assertNotNull(r);
assertEquals(now, r.getTimestamp());
}
@Test
public void getStartRevisionsMultipleNodes() {
int clusterId1 = 1;
int clusterId2 = 2;
long startTime1 = System.currentTimeMillis();
long startTime2 = startTime1 + 1000;
ClusterNodeInfoDocument info1 = mockedClusterNodeInfo(clusterId1, startTime1);
ClusterNodeInfoDocument info2 = mockedClusterNodeInfo(clusterId2, startTime2);
RevisionVector rv = Utils.getStartRevisions(Arrays.asList(info1, info2));
assertEquals(2, rv.getDimensions());
Revision r1 = rv.getRevision(clusterId1);
assertNotNull(r1);
Revision r2 = rv.getRevision(clusterId2);
assertNotNull(r2);
assertEquals(startTime1, r1.getTimestamp());
assertEquals(startTime2, r2.getTimestamp());
}
@Test
public void sum() {
assertEquals(0, Utils.sum());
assertEquals(42, Utils.sum(7, 15, 20));
assertEquals(-12, Utils.sum(-7, 15, -20));
assertEquals(42, Utils.sum(Long.MAX_VALUE, 43, Long.MIN_VALUE));
assertEquals(Long.MAX_VALUE, Utils.sum(Long.MAX_VALUE));
assertEquals(Long.MAX_VALUE - 1, Utils.sum(Long.MAX_VALUE, -1));
assertEquals(Long.MAX_VALUE, Utils.sum(Long.MAX_VALUE, 1));
assertEquals(Long.MIN_VALUE, Utils.sum(Long.MIN_VALUE));
assertEquals(Long.MIN_VALUE + 1, Utils.sum(Long.MIN_VALUE, 1));
assertEquals(Long.MIN_VALUE, Utils.sum(Long.MIN_VALUE, -1));
}
private static ClusterNodeInfoDocument mockedClusterNodeInfo(int clusterId,
long startTime) {
ClusterNodeInfoDocument info = Mockito.mock(ClusterNodeInfoDocument.class);
Mockito.when(info.getClusterId()).thenReturn(clusterId);
Mockito.when(info.getStartTime()).thenReturn(startTime);
return info;
}
}