Author: mreutegg
Date: Tue Oct  3 07:40:17 2017
New Revision: 1810662

URL: http://svn.apache.org/viewvc?rev=1810662&view=rev
Log:
OAK-5788: Perform update of single node in one remote call if possible

Modified:
    
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Collision.java
    
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
    
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
    
jackrabbit/oak/trunk/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java

Modified: 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Collision.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Collision.java?rev=1810662&r1=1810661&r2=1810662&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Collision.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Collision.java
 Tue Oct  3 07:40:17 2017
@@ -90,7 +90,8 @@ class Collision {
         if (!markCommitRoot(newDoc, ourRev, theirRev, store, context)) {
             throw new IllegalStateException("Unable to annotate our revision "
                     + "with collision marker. Our revision: " + ourRev
-                    + ", document:\n" + newDoc.format());
+                    + ", their revision: " + theirRev + ", document:\n"
+                    + newDoc.format());
         }
         return ourRev;
     }

Modified: 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java?rev=1810662&r1=1810661&r2=1810662&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
 Tue Oct  3 07:40:17 2017
@@ -50,6 +50,7 @@ import static java.util.Collections.sing
 import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
 import static org.apache.jackrabbit.oak.plugins.document.Collection.JOURNAL;
 import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
+import static org.apache.jackrabbit.oak.plugins.document.Document.MOD_COUNT;
 import static 
org.apache.jackrabbit.oak.plugins.document.NodeDocument.COLLISIONS;
 import static 
org.apache.jackrabbit.oak.plugins.document.NodeDocument.SPLIT_CANDIDATE_THRESHOLD;
 
@@ -337,60 +338,64 @@ public class Commit {
         boolean success = false;
         try {
             opLog.addAll(changedNodes);
-            List<NodeDocument> oldDocs = store.createOrUpdate(NODES, 
changedNodes);
-            checkConflicts(oldDocs, changedNodes);
-            checkSplitCandidate(oldDocs);
-
-            // finally write the commit root (the commit root might be written
-            // twice, first to check if there was a conflict, and only then to
-            // commit the revision, with the revision property set)
-            NodeDocument.setRevision(commitRoot, revision, commitValue);
-            if (commitRootHasChanges) {
-                // remove previously added commit root
-                NodeDocument.removeCommitRoot(commitRoot, revision);
-            }
-            opLog.add(commitRoot);
-            if (baseBranchRevision == null) {
-                // create a clone of the commitRoot in order
-                // to set isNew to false. If we get here the
-                // commitRoot document already exists and
-                // only needs an update
-                UpdateOp commit = commitRoot.copy();
-                commit.setNew(false);
-                // only set revision on commit root when there is
-                // no collision for this commit revision
-                commit.containsMapEntry(COLLISIONS, revision, false);
-                NodeDocument before = nodeStore.updateCommitRoot(commit, 
revision);
-                if (before == null) {
-                    String msg = "Conflicting concurrent change. " +
-                            "Update operation failed: " + commitRoot;
-                    NodeDocument commitRootDoc = store.find(NODES, 
commitRoot.getId());
-                    DocumentStoreException dse;
-                    if (commitRootDoc == null) {
-                        dse = new DocumentStoreException(msg);
+
+            if (conditionalCommit(changedNodes, commitValue)) {
+                success = true;
+            } else {
+                List<NodeDocument> oldDocs = store.createOrUpdate(NODES, 
changedNodes);
+                checkConflicts(oldDocs, changedNodes);
+                checkSplitCandidate(oldDocs);
+
+                // finally write the commit root (the commit root might be 
written
+                // twice, first to check if there was a conflict, and only 
then to
+                // commit the revision, with the revision property set)
+                NodeDocument.setRevision(commitRoot, revision, commitValue);
+                if (commitRootHasChanges) {
+                    // remove previously added commit root
+                    NodeDocument.removeCommitRoot(commitRoot, revision);
+                }
+                opLog.add(commitRoot);
+                if (baseBranchRevision == null) {
+                    // create a clone of the commitRoot in order
+                    // to set isNew to false. If we get here the
+                    // commitRoot document already exists and
+                    // only needs an update
+                    UpdateOp commit = commitRoot.copy();
+                    commit.setNew(false);
+                    // only set revision on commit root when there is
+                    // no collision for this commit revision
+                    commit.containsMapEntry(COLLISIONS, revision, false);
+                    NodeDocument before = nodeStore.updateCommitRoot(commit, 
revision);
+                    if (before == null) {
+                        String msg = "Conflicting concurrent change. " +
+                                "Update operation failed: " + commitRoot;
+                        NodeDocument commitRootDoc = store.find(NODES, 
commitRoot.getId());
+                        DocumentStoreException dse;
+                        if (commitRootDoc == null) {
+                            dse = new DocumentStoreException(msg);
+                        } else {
+                            dse = new ConflictException(msg,
+                                    commitRootDoc.getConflictsFor(
+                                            Collections.singleton(revision)));
+                        }
+                        throw dse;
                     } else {
-                        dse = new ConflictException(msg,
-                                commitRootDoc.getConflictsFor(
-                                        Collections.singleton(revision)));
+                        success = true;
+                        // if we get here the commit was successful and
+                        // the commit revision is set on the commitRoot
+                        // document for this commit.
+                        // now check for conflicts/collisions by other commits.
+                        // use original commitRoot operation with
+                        // correct isNew flag.
+                        checkConflicts(commitRoot, before);
+                        checkSplitCandidate(before);
                     }
-                    throw dse;
                 } else {
-                    success = true;
-                    // if we get here the commit was successful and
-                    // the commit revision is set on the commitRoot
-                    // document for this commit.
-                    // now check for conflicts/collisions by other commits.
-                    // use original commitRoot operation with
-                    // correct isNew flag.
-                    checkConflicts(commitRoot, before);
-                    checkSplitCandidate(before);
+                    // this is a branch commit, do not fail on collisions now
+                    // trying to merge the branch will fail later
+                    createOrUpdateNode(store, commitRoot);
                 }
-            } else {
-                // this is a branch commit, do not fail on collisions now
-                // trying to merge the branch will fail later
-                createOrUpdateNode(store, commitRoot);
             }
-            operations.put(commitRootPath, commitRoot);
         } catch (DocumentStoreException e) {
             // OAK-3084 do not roll back if already committed
             if (success) {
@@ -413,6 +418,49 @@ public class Commit {
         }
     }
 
+    private boolean conditionalCommit(List<UpdateOp> changedNodes,
+                                      String commitValue)
+            throws DocumentStoreException {
+        // conditional commit is only possible when not on a branch
+        // and commit root is on the same document as the changes
+        if (!Utils.isCommitted(commitValue) || changedNodes.size() != 1) {
+            return false;
+        }
+        UpdateOp op = changedNodes.get(0);
+        DocumentStore store = nodeStore.getDocumentStore();
+        NodeDocument doc = store.getIfCached(NODES, op.getId());
+        if (doc == null || doc.getModCount() == null) {
+            // document not in cache or store does not maintain modCount
+            return false;
+        }
+        try {
+            checkConflicts(op, doc);
+        } catch (ConflictException e) {
+            // remove collision marker again
+            removeCollisionMarker(op.getId());
+            return false;
+        }
+        // if we get here, update based on current doc does not conflict
+        // create a new commit update operation, setting the revisions
+        // commit entry together with the other changes
+        UpdateOp commit = op.copy();
+        NodeDocument.unsetCommitRoot(commit, revision);
+        NodeDocument.setRevision(commit, revision, commitValue);
+        // make the update conditional on the modCount
+        commit.equals(MOD_COUNT, doc.getModCount());
+        NodeDocument before = store.findAndUpdate(NODES, commit);
+        if (before != null) {
+            checkSplitCandidate(before);
+        }
+        return before != null;
+    }
+
+    private void removeCollisionMarker(String id) {
+        UpdateOp removeCollision = new UpdateOp(id, false);
+        NodeDocument.removeCollision(removeCollision, revision);
+        nodeStore.getDocumentStore().findAndUpdate(NODES, removeCollision);
+    }
+
     private void updateParentChildStatus() {
         final Set<String> processedParents = Sets.newHashSet();
         for (String path : addedNodes) {
@@ -447,9 +495,7 @@ public class Commit {
             }
             store.findAndUpdate(NODES, reverse);
         }
-        UpdateOp removeCollision = new UpdateOp(commitRoot.getId(), false);
-        NodeDocument.removeCollision(removeCollision, revision);
-        store.findAndUpdate(NODES, removeCollision);
+        removeCollisionMarker(commitRoot.getId());
     }
 
     /**

Modified: 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java?rev=1810662&r1=1810661&r2=1810662&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
 Tue Oct  3 07:40:17 2017
@@ -1816,6 +1816,11 @@ public final class NodeDocument extends
         checkNotNull(op).removeMapEntry(COMMIT_ROOT, revision);
     }
 
+    public static void unsetCommitRoot(@Nonnull UpdateOp op,
+                                       @Nonnull Revision revision) {
+        checkNotNull(op).unsetMapEntry(COMMIT_ROOT, revision);
+    }
+
     public static void setDeleted(@Nonnull UpdateOp op,
                                   @Nonnull Revision revision,
                                   boolean deleted) {

Modified: 
jackrabbit/oak/trunk/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java?rev=1810662&r1=1810661&r2=1810662&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
 Tue Oct  3 07:40:17 2017
@@ -17,6 +17,7 @@
 package org.apache.jackrabbit.oak.plugins.document;
 
 import static java.util.Collections.emptyList;
+import static java.util.Collections.synchronizedList;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.apache.jackrabbit.oak.api.CommitFailedException.CONSTRAINT;
 import static org.apache.jackrabbit.oak.plugins.document.Collection.JOURNAL;
@@ -77,6 +78,7 @@ import java.util.concurrent.locks.Reentr
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
+import javax.jcr.InvalidItemStateException;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
@@ -2904,10 +2906,9 @@ public class DocumentNodeStoreTest {
         assertTrue("Two added paths should have forced flush", numChangedPaths 
== 0);
     }
 
-    @Ignore("OAK-5788")
     @Test
     public void commitRootSameAsModifiedPath() throws Exception{
-        WriteCountingStore ws = new WriteCountingStore();
+        WriteCountingStore ws = new WriteCountingStore(true);
 
         DocumentNodeStore ns = 
builderProvider.newBuilder().setAsyncDelay(0).setDocumentStore(ws).getNodeStore();
         NodeBuilder builder = ns.getRoot().builder();
@@ -2923,6 +2924,80 @@ public class DocumentNodeStoreTest {
         assertEquals(1, ws.count);
     }
 
+    @Test
+    public void commitRootSameAsModifiedPathWithConflicts() throws Exception{
+        MemoryDocumentStore store = new MemoryDocumentStore(true);
+        final DocumentNodeStore ns = 
builderProvider.newBuilder().setAsyncDelay(0)
+                .setDocumentStore(store).getNodeStore();
+        NodeBuilder builder = ns.getRoot().builder();
+        builder.child("a").child("b").setProperty("p", 0L);
+        merge(ns, builder);
+
+        final List<Throwable> exceptions = 
synchronizedList(Lists.newArrayList());
+
+        Runnable task = new Runnable() {
+
+            CommitHook hook = new CompositeHook(
+                    new ConflictHook(new AnnotatingConflictHandler()),
+                    new EditorHook(new ConflictValidatorProvider())
+            );
+
+            @Override
+            public void run() {
+                try {
+                    for (int i = 0; i < 100; i++) {
+                        NodeBuilder builder = ns.getRoot().builder();
+                        NodeBuilder b = builder.child("a").child("b");
+                        PropertyState p = b.getProperty("p");
+                        assertNotNull(p);
+                        long value = p.getValue(Type.LONG) + 1;
+                        b.setProperty(p.getName(), value);
+                        try {
+                            ns.merge(builder, hook, CommitInfo.EMPTY);
+                        } catch (CommitFailedException e) {
+                            if (e.asRepositoryException() instanceof 
InvalidItemStateException) {
+                                // this is fine and may happen from time to
+                                // time because the test updates the same
+                                // property concurrently
+                            } else {
+                                // anything else is unexpected
+                                exceptions.add(e);
+                            }
+                        }
+                    }
+                } catch (AssertionError e) {
+                    exceptions.add(e);
+                }
+            }
+        };
+
+        List<Thread> threads = Lists.newArrayList();
+        for (int i = 0; i < 4; i++) {
+            threads.add(new Thread(task));
+        }
+        for (Thread t : threads) {
+            t.start();
+        }
+        for (Thread t : threads) {
+            t.join();
+        }
+
+        // check updates are consecutive
+        NodeDocument doc = store.find(NODES, Utils.getIdFromPath("/a/b"));
+        assertNotNull(doc);
+        long previousValue = -1;
+        List<String> values = 
Lists.newArrayList(doc.getLocalMap("p").values());
+        for (String v : Lists.reverse(values)) {
+            long currentValue = Long.parseLong(v);
+            assertEquals(previousValue + 1, currentValue);
+            previousValue = currentValue;
+        }
+
+        for (Throwable e : exceptions) {
+            fail(e.toString());
+        }
+    }
+
     @Ignore("OAK-5791")
     @Test
     public void createChildNodeAndCheckNoOfCalls() throws Exception{
@@ -3477,6 +3552,13 @@ public class DocumentNodeStoreTest {
         private final ThreadLocal<Boolean> createMulti = new ThreadLocal<>();
         int count;
 
+        WriteCountingStore() {
+        }
+
+        WriteCountingStore(boolean maintainModCount) {
+            super(maintainModCount);
+        }
+
         @Override
         public <T extends Document> T createOrUpdate(Collection<T> collection, 
UpdateOp update) {
             if (createMulti.get() == null) {


Reply via email to