diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategyTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategyTest.java index 6465a61ee0..cef17d157d 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategyTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategyTest.java @@ -18,27 +18,40 @@ package org.apache.jackrabbit.oak.plugins.index.property.strategy; import static com.google.common.collect.Sets.newHashSet; import static java.util.Arrays.asList; -import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.ENTRY_COUNT_PROPERTY_NAME; -import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME; -import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.KEY_COUNT_PROPERTY_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.*; +import static org.apache.jackrabbit.oak.plugins.index.IndexUtils.createIndexDefinition; import static org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditor.COUNT_PROPERTY_NAME; import static org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditor.DEFAULT_RESOLUTION; import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE; import static org.apache.jackrabbit.oak.util.ApproximateCounter.COUNT_PROPERTY_PREFIX; +import java.util.Arrays; import java.util.Collections; import java.util.Set; +import java.util.concurrent.Semaphore; +import com.google.common.collect.ImmutableSet; import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider; +import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore; +import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider; +import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider; import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.commit.CommitInfo; +import org.apache.jackrabbit.oak.spi.commit.CompositeHook; +import org.apache.jackrabbit.oak.spi.commit.EditorHook; import org.apache.jackrabbit.oak.spi.query.Filter; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStore; import org.junit.Assert; import org.junit.Test; +import javax.annotation.Nonnull; + /** * Test the content mirror strategy */ @@ -288,4 +301,134 @@ public class ContentMirrorStoreStrategyTest { diff < expected * allowedError); } + // OAK-5592 + @Test + public void testWriteSkewInPropertyIndex() throws Exception { + DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider(); + final DocumentNodeStore store = builderProvider.newBuilder().getNodeStore(); + + final String property = "foo"; + final String propertyValue = "bar"; + final String parentName = "parent"; + final String child1Name = "child1"; + final String child2Name = "child2"; + + // create the property index on "foo" + NodeBuilder builder = store.getRoot().builder(); + createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), property, + true, false, ImmutableSet.of(property), null); + merge(store, builder); + + /** + * populate two nodes with common parent and both having property "foo" set to "bar" + * i.e. the tree looks like this: + * + * - parent + * - child1[foo=bar] + * - child2[foo=bar] + * - oak:index + * - foo + * - :index + * - bar + * - parent + * - child1[match=true] + * - child2[match=true] + */ + builder = store.getRoot().builder(); + NodeBuilder parent = builder.child(parentName); + parent.child(child1Name).setProperty(property, propertyValue); + parent.child(child2Name).setProperty(property, propertyValue); + merge(store, builder); + + // used to coordinate the two transactions + final Semaphore semaphore = new Semaphore(0); + + // transaction to remove "foo" property from child1 + final Thread tnx1 = new Thread(new Runnable() { + @Override public void run() { + NodeBuilder root = store.getRoot().builder(); + NodeBuilder child = root.child(parentName).child(child1Name); + child.removeProperty(property); + try { + merge(store, root); + } catch (CommitFailedException e) { + e.printStackTrace(); + } + semaphore.release(); + } + }); + + // transaction remove "foo" property from child2 + final Thread tnx2 = new Thread(new Runnable() { + @Override public void run() { + NodeBuilder root = store.getRoot().builder(); + NodeBuilder child = root.child(parentName).child(child2Name); + child.removeProperty(property); + + // make sure that tnx1 starts after my rebase and finishes before my commit + CommitHook waitingHook = new CommitHook() { + @Nonnull @Override + public NodeState processCommit(NodeState before, NodeState after, CommitInfo info) + throws CommitFailedException { + tnx1.start(); + semaphore.acquireUninterruptibly(); + return after; + } + }; + CommitHook hook = CompositeHook.compose(Arrays.asList( + waitingHook, + new EditorHook(new IndexUpdateProvider(new PropertyIndexEditorProvider())) + )); + + try { + store.merge(root, hook, CommitInfo.EMPTY); + } catch (CommitFailedException e) { + e.printStackTrace(); + } + } + }); + + // start only tnx2, tnx1 is started by tnx2 + tnx2.start(); + + tnx1.join(); + tnx2.join(); + + /** + * + * Write skew happened. the tree looks like this: + * - parent + * - child1 + * - child2 + * - oak:index + * - foo + * - :index + * - bar + * - parent + * + * but it should look like this: + * - parent + * - child1 + * - child2 + * - oak:index + * - foo + * - :index + * + * That is, node /oak:index/foo/:index/bar/parent and its ancestors + * should have been pruned (but haven't). + */ + builder = store.getRoot().builder(); + NodeBuilder node = builder.child(INDEX_DEFINITIONS_NAME). + child(property). + child(INDEX_CONTENT_NODE_NAME). + child(propertyValue); + Assert.assertFalse(node.hasChildNode(parentName)); + } + + private NodeState merge(NodeStore store, NodeBuilder root) + throws CommitFailedException { + CommitHook hook = new EditorHook(new IndexUpdateProvider(new PropertyIndexEditorProvider())); + return store.merge(root, hook, CommitInfo.EMPTY); + } + }