| /* |
| * 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.secondary; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import org.apache.jackrabbit.oak.api.CommitFailedException; |
| import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState; |
| import org.apache.jackrabbit.oak.plugins.document.Collection; |
| import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider; |
| import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore; |
| import org.apache.jackrabbit.oak.plugins.document.NodeDocument; |
| import org.apache.jackrabbit.oak.plugins.document.Revision; |
| import org.apache.jackrabbit.oak.plugins.document.RevisionVector; |
| import org.apache.jackrabbit.oak.plugins.document.bundlor.BundledTypesRegistry; |
| import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigHandler; |
| import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigInitializer; |
| import org.apache.jackrabbit.oak.plugins.document.util.Utils; |
| import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore; |
| import org.apache.jackrabbit.oak.InitialContent; |
| import org.apache.jackrabbit.oak.spi.commit.CommitInfo; |
| import org.apache.jackrabbit.oak.spi.commit.EmptyHook; |
| import org.apache.jackrabbit.oak.spi.filter.PathFilter; |
| import org.apache.jackrabbit.oak.spi.state.EqualsDiff; |
| import org.apache.jackrabbit.oak.spi.state.NodeBuilder; |
| import org.apache.jackrabbit.oak.spi.state.NodeStateUtils; |
| import org.apache.jackrabbit.oak.spi.state.NodeStore; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| |
| import static com.google.common.collect.ImmutableList.of; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.oak.plugins.document.secondary.SecondaryStoreObserverTest.create; |
| import static org.apache.jackrabbit.oak.plugins.document.secondary.SecondaryStoreObserverTest.documentState; |
| import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE; |
| 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; |
| |
| public class SecondaryStoreCacheTest { |
| private final List<String> empty = Collections.emptyList(); |
| @Rule |
| public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider(); |
| |
| private DocumentNodeStore primary; |
| private NodeStore secondary; |
| |
| @Before |
| public void setUp() throws IOException { |
| primary = builderProvider.newBuilder().getNodeStore(); |
| secondary = new MemoryNodeStore(); |
| } |
| |
| @Test |
| public void basicTest() throws Exception{ |
| SecondaryStoreCache cache = createCache(new PathFilter(of("/a"), empty)); |
| |
| NodeBuilder nb = primary.getRoot().builder(); |
| create(nb, "/a/b", "/a/c", "/x/y/z"); |
| merge(nb); |
| |
| RevisionVector rv1 = new RevisionVector(new Revision(1,0,1)); |
| RevisionVector rv2 = new RevisionVector(new Revision(1,0,3)); |
| assertNull(cache.getDocumentNodeState("/a/b", rv1, rv2)); |
| assertNull(cache.getDocumentNodeState("/x", rv1, rv2)); |
| } |
| |
| @Test |
| public void updateAndReadAtReadRev() throws Exception{ |
| SecondaryStoreCache cache = createCache(new PathFilter(of("/a"), empty)); |
| |
| NodeBuilder nb = primary.getRoot().builder(); |
| create(nb, "/a/b", "/a/c", "/x/y/z"); |
| AbstractDocumentNodeState r1 = merge(nb); |
| |
| //Update some other part of tree i.e. which does not change lastRev for /a/c |
| nb = primary.getRoot().builder(); |
| create(nb, "/a/e/d"); |
| AbstractDocumentNodeState r2 = merge(nb); |
| |
| //Lookup should work fine |
| AbstractDocumentNodeState a_r2 = documentState(r2, "/a/c"); |
| AbstractDocumentNodeState result |
| = cache.getDocumentNodeState("/a/c", r2.getRootRevision(), a_r2.getLastRevision()); |
| assertTrue(EqualsDiff.equals(a_r2, result)); |
| |
| //Child docs should only have lastRev and not root rev |
| assertTrue(result.hasProperty(DelegatingDocumentNodeState.PROP_LAST_REV)); |
| assertFalse(result.hasProperty(DelegatingDocumentNodeState.PROP_REVISION)); |
| |
| //Root doc would have both meta props |
| assertTrue(secondary.getRoot().hasProperty(DelegatingDocumentNodeState.PROP_LAST_REV)); |
| assertTrue(secondary.getRoot().hasProperty(DelegatingDocumentNodeState.PROP_REVISION)); |
| |
| nb = primary.getRoot().builder(); |
| nb.child("a").child("c").remove(); |
| AbstractDocumentNodeState r3 = merge(nb); |
| |
| //Now look from older revision |
| result = cache.getDocumentNodeState("/a/c", r3.getRootRevision(), a_r2.getLastRevision()); |
| |
| //now as its not visible from head it would not be visible |
| assertNull(result); |
| } |
| |
| @Test |
| public void updateAndReadAtPrevRevision() throws Exception { |
| SecondaryStoreCache cache = createCache(new PathFilter(of("/a"), empty)); |
| |
| NodeBuilder nb = primary.getRoot().builder(); |
| create(nb, "/a/b", "/a/c"); |
| AbstractDocumentNodeState r0 = merge(nb); |
| AbstractDocumentNodeState a_c_0 = documentState(primary.getRoot(), "/a/c"); |
| |
| //Update some other part of tree i.e. which does not change lastRev for /a/c |
| nb = primary.getRoot().builder(); |
| create(nb, "/a/c/d"); |
| AbstractDocumentNodeState r1 = merge(nb); |
| AbstractDocumentNodeState a_c_1 = documentState(primary.getRoot(), "/a/c"); |
| |
| AbstractDocumentNodeState result |
| = cache.getDocumentNodeState("/a/c", r1.getRootRevision(), a_c_1.getLastRevision()); |
| assertTrue(EqualsDiff.equals(a_c_1, result)); |
| |
| //Read from older revision |
| result = cache.getDocumentNodeState("/a/c", r0.getRootRevision(), a_c_0.getLastRevision()); |
| assertTrue(EqualsDiff.equals(a_c_0, result)); |
| } |
| |
| @Test |
| public void binarySearch() throws Exception{ |
| SecondaryStoreCache cache = createCache(new PathFilter(of("/a"), empty)); |
| |
| List<AbstractDocumentNodeState> roots = Lists.newArrayList(); |
| List<RevisionVector> revs = Lists.newArrayList(); |
| for (int i = 0; i < 50; i++) { |
| NodeBuilder nb = primary.getRoot().builder(); |
| create(nb, "/a/b"+i); |
| AbstractDocumentNodeState r = merge(nb); |
| roots.add(r); |
| revs.add(r.getRootRevision()); |
| } |
| |
| AbstractDocumentNodeState[] rootsArr = Iterables.toArray(roots, AbstractDocumentNodeState.class); |
| |
| Collections.shuffle(revs); |
| for (RevisionVector rev : revs){ |
| AbstractDocumentNodeState result = SecondaryStoreCache.findMatchingRoot(rootsArr, rev); |
| assertNotNull(result); |
| assertEquals(rev, result.getRootRevision()); |
| } |
| |
| NodeBuilder nb = primary.getRoot().builder(); |
| create(nb, "/a/m"); |
| AbstractDocumentNodeState r = merge(nb); |
| AbstractDocumentNodeState result = SecondaryStoreCache.findMatchingRoot(rootsArr, r.getRootRevision()); |
| assertNull(result); |
| |
| } |
| |
| @Test |
| public void readWithSecondaryLagging() throws Exception{ |
| PathFilter pathFilter = new PathFilter(of("/a"), empty); |
| SecondaryStoreCache cache = createBuilder(pathFilter).buildCache(); |
| SecondaryStoreObserver observer = createBuilder(pathFilter).buildObserver(cache); |
| |
| NodeBuilder nb = primary.getRoot().builder(); |
| create(nb, "/a/b", "/a/c"); |
| AbstractDocumentNodeState r0 = merge(nb); |
| AbstractDocumentNodeState a_c_0 = documentState(primary.getRoot(), "/a/c"); |
| |
| observer.contentChanged(r0, CommitInfo.EMPTY); |
| |
| AbstractDocumentNodeState result = cache.getDocumentNodeState("/a/c", r0.getRootRevision(), a_c_0 |
| .getLastRevision()); |
| assertTrue(EqualsDiff.equals(a_c_0, result)); |
| |
| //Make change in some other part of tree i.e. /a/c is unmodified |
| nb = primary.getRoot().builder(); |
| create(nb, "/a/e"); |
| AbstractDocumentNodeState r1 = merge(nb); |
| |
| //Change is yet not pushed to secondary i.e. observer not invoked |
| //but lookup with latest root should still work fine if lastRev matches |
| result = cache.getDocumentNodeState("/a/c", r1.getRootRevision(), a_c_0 |
| .getLastRevision()); |
| assertTrue(EqualsDiff.equals(a_c_0, result)); |
| |
| //Change which is not pushed would though not be visible |
| AbstractDocumentNodeState a_e_1 = documentState(primary.getRoot(), "/a/e"); |
| result = cache.getDocumentNodeState("/a/e", r1.getRootRevision(), a_e_1 |
| .getLastRevision()); |
| assertNull(result); |
| } |
| |
| @Test |
| public void isCached() throws Exception{ |
| SecondaryStoreCache cache = createCache(new PathFilter(of("/a"), empty)); |
| |
| assertTrue(cache.isCached("/a")); |
| assertTrue(cache.isCached("/a/b")); |
| assertFalse(cache.isCached("/x")); |
| } |
| |
| @Test |
| public void bundledNodes() throws Exception{ |
| SecondaryStoreCache cache = createCache(new PathFilter(of("/"), empty)); |
| primary.setNodeStateCache(cache); |
| |
| NodeBuilder builder = primary.getRoot().builder(); |
| new InitialContent().initialize(builder); |
| BundlingConfigInitializer.INSTANCE.initialize(builder); |
| merge(builder); |
| |
| BundledTypesRegistry registry = BundledTypesRegistry.from(NodeStateUtils.getNode(primary.getRoot(), |
| BundlingConfigHandler.CONFIG_PATH)); |
| assertNotNull("DocumentBundling not found to be enabled for nt:file", |
| registry.getBundlor(newNode("nt:file").getNodeState())); |
| |
| //1. Create a file node |
| builder = primary.getRoot().builder(); |
| NodeBuilder fileNode = newNode("nt:file"); |
| fileNode.child("jcr:content").setProperty("jcr:data", "foo"); |
| builder.child("test").setChildNode("book.jpg", fileNode.getNodeState()); |
| merge(builder); |
| |
| //2. Assert that bundling is working |
| assertNull(getNodeDocument("/test/book.jpg/jcr:content")); |
| |
| //3. Now update the file node |
| builder = primary.getRoot().builder(); |
| builder.getChildNode("test").getChildNode("book.jpg").getChildNode("jcr:content").setProperty("foo", "bar"); |
| merge(builder); |
| } |
| |
| private SecondaryStoreCache createCache(PathFilter pathFilter){ |
| SecondaryStoreBuilder builder = createBuilder(pathFilter); |
| builder.metaPropNames(DocumentNodeStore.META_PROP_NAMES); |
| SecondaryStoreCache cache = builder.buildCache(); |
| SecondaryStoreObserver observer = builder.buildObserver(cache); |
| primary.addObserver(observer); |
| |
| return cache; |
| } |
| |
| private static NodeBuilder newNode(String typeName){ |
| NodeBuilder builder = EMPTY_NODE.builder(); |
| builder.setProperty(JCR_PRIMARYTYPE, typeName); |
| return builder; |
| } |
| |
| private NodeDocument getNodeDocument(String path) { |
| return primary.getDocumentStore().find(Collection.NODES, Utils.getIdFromPath(path)); |
| } |
| |
| private SecondaryStoreBuilder createBuilder(PathFilter pathFilter) { |
| return new SecondaryStoreBuilder(secondary).pathFilter(pathFilter); |
| } |
| |
| private AbstractDocumentNodeState merge(NodeBuilder nb) throws CommitFailedException { |
| return (AbstractDocumentNodeState) primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY); |
| } |
| |
| } |