You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by mr...@apache.org on 2013/04/25 10:32:03 UTC

svn commit: r1475669 - in /jackrabbit/oak/trunk/oak-mongomk/src: main/java/org/apache/jackrabbit/mongomk/ test/java/org/apache/jackrabbit/mongomk/

Author: mreutegg
Date: Thu Apr 25 08:32:03 2013
New Revision: 1475669

URL: http://svn.apache.org/r1475669
Log:
OAK-619 Lock-free MongoMK implementation
- Improved collision detection
- More conflict tests with branches (some ignored because conflict detection is not yet complete)

Modified:
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Collision.java
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Commit.java
    jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/ConflictTest.java
    jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoMKBranchMergeTest.java

Modified: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Collision.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Collision.java?rev=1475669&r1=1475668&r2=1475669&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Collision.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Collision.java Thu Apr 25 08:32:03 2013
@@ -32,15 +32,45 @@ import static com.google.common.base.Pre
 class Collision {
 
     private final Map<String, Object> document;
-    private final String revision;
+    private final String theirRev;
+    private final UpdateOp ourOp;
+    private final String ourRev;
 
     Collision(@Nonnull Map<String, Object> document,
-              @Nonnull Revision revision) {
+              @Nonnull Revision theirRev,
+              @Nonnull UpdateOp ourOp,
+              @Nonnull Revision ourRev) {
         this.document = checkNotNull(document);
-        this.revision = checkNotNull(revision).toString();
+        this.theirRev = checkNotNull(theirRev).toString();
+        this.ourOp = checkNotNull(ourOp);
+        this.ourRev = checkNotNull(ourRev).toString();
     }
 
     boolean mark(DocumentStore store) {
+        if (markCommitRoot(document, theirRev, store)) {
+            return true;
+        }
+        @SuppressWarnings("unchecked")
+        Map<String, String> revisions = (Map<String, String>) document.get(UpdateOp.REVISIONS);
+        if (revisions.containsKey(theirRev)) {
+            String value = revisions.get(theirRev);
+            if ("true".equals(value)) {
+                // their commit wins, we have to mark ourRev
+                Map<String, Object> newDoc = Utils.newMap();
+                Utils.deepCopyMap(document, newDoc);
+                MemoryDocumentStore.applyChanges(newDoc, ourOp);
+                if (markCommitRoot(newDoc, ourRev, store)) {
+                    return true;
+                }
+            }
+        }
+        // TODO: detect concurrent commit of previously un-merged changes
+        return true;
+    }
+
+    private static boolean markCommitRoot(@Nonnull Map<String, Object> document,
+                                          @Nonnull String revision,
+                                          @Nonnull DocumentStore store) {
         @SuppressWarnings("unchecked")
         Map<String, Integer> commitRoots = (Map<String, Integer>) document.get(UpdateOp.COMMIT_ROOT);
         if (commitRoots != null) {
@@ -52,9 +82,9 @@ class Collision {
                         Utils.getIdFromPath(commitRootPath), false);
                 op.setMapEntry(UpdateOp.COLLISIONS, revision, true);
                 store.createOrUpdate(DocumentStore.Collection.NODES, op);
+                return true;
             }
         }
-        // TODO: detect concurrent commit of previously un-merged changes
-        return true;
+        return false;
     }
 }

Modified: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Commit.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Commit.java?rev=1475669&r1=1475668&r2=1475669&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Commit.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/Commit.java Thu Apr 25 08:32:03 2013
@@ -299,7 +299,7 @@ public class Commit {
             if (collisions.get() != null && isConflicting(map, op)) {
                 for (Revision r : collisions.get()) {
                     // mark collisions on commit root
-                    Collision c = new Collision(map, r);
+                    Collision c = new Collision(map, r, op, revision);
                     boolean success = c.mark(store);
                     if (!success) {
                         // TODO: fail this commit

Modified: jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/ConflictTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/ConflictTest.java?rev=1475669&r1=1475668&r2=1475669&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/ConflictTest.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/ConflictTest.java Thu Apr 25 08:32:03 2013
@@ -17,6 +17,7 @@
 package org.apache.jackrabbit.mongomk;
 
 import org.apache.jackrabbit.mk.api.MicroKernelException;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import static org.junit.Assert.fail;
@@ -41,6 +42,53 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void addExistingPropertyBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "^\"prop\":\"value\"", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
+            fail("Must fail with conflict for addExistingProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void addExistingPropertyBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"value\"", branchRev, null);
+        mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for addExistingProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void addExistingPropertyBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"value\"", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for addExistingProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void removeRemovedProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop\":null", rev, null);
@@ -54,6 +102,53 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void removeRemovedPropertyBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "^\"prop\":null", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "^\"prop\":null", rev, null);
+            fail("Must fail with conflict for removeRemovedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void removeRemovedPropertyBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":null", branchRev, null);
+        mk.commit("/foo", "^\"prop\":null", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeRemovedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void removeRemovedPropertyBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop\":null", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":null", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeRemovedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void removeChangedProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop\":\"bar\"", rev, null);
@@ -67,6 +162,53 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void removeChangedPropertyBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "^\"prop\":\"bar\"", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "^\"prop\":null", rev, null);
+            fail("Must fail with conflict for removeChangedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void removeChangedPropertyBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"bar\"", branchRev, null);
+        mk.commit("/foo", "^\"prop\":null", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeChangedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void removeChangedPropertyBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop\":null", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"bar\"", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeChangedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void changeRemovedProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop\":null", rev, null);
@@ -80,6 +222,53 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void changeRemovedPropertyBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "^\"prop\":null", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "^\"prop\":\"bar\"", rev, null);
+            fail("Must fail with conflict for changeRemovedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void changeRemovedPropertyBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":null", branchRev, null);
+        mk.commit("/foo", "^\"prop\":\"bar\"", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for changeRemovedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void changeRemovedPropertyBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop\":\"bar\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":null", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for changeRemovedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void changeChangedProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop\":\"bar\"", rev, null);
@@ -93,6 +282,53 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void changeChangedPropertyBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "^\"prop\":\"bar\"", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "^\"prop\":\"baz\"", rev, null);
+            fail("Must fail with conflict for changeChangedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void changeChangedPropertyBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"bar\"", branchRev, null);
+        mk.commit("/foo", "^\"prop\":\"baz\"", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for changeChangedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void changeChangedPropertyBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop\":\"baz\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"bar\"", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for changeChangedProperty");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void addExistingNode() {
         String rev = mk.commit("/", "+\"foo\":{}", null, null);
         mk.commit("/foo", "+\"bar\":{}", rev, null);
@@ -106,6 +342,55 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void addExistingNodeBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "+\"bar\":{}", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "+\"bar\":{}", rev, null);
+            fail("Must fail with conflict for addExistingNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void addExistingNodeBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "+\"bar\":{}", branchRev, null);
+        mk.commit("/foo", "+\"bar\":{}", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for addExistingNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void addExistingNodeBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "+\"bar\":{}", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "+\"bar\":{}", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for addExistingNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void removeRemovedNode() {
         String rev = mk.commit("/", "+\"foo\":{}", null, null);
         mk.commit("/", "-\"foo\"", rev, null);
@@ -119,6 +404,55 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void removeRemovedNodeBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/", "-\"foo\"", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/", "-\"foo\"", rev, null);
+            fail("Must fail with conflict for removeRemovedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void removeRemovedNodeBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/", "-\"foo\"", branchRev, null);
+        mk.commit("/", "-\"foo\"", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeRemovedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void removeRemovedNodeBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/", "-\"foo\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/", "-\"foo\"", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeRemovedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void removeChangedNode() {
         String rev = mk.commit("/", "+\"foo\":{}", null, null);
         mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
@@ -132,6 +466,55 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void removeChangedNodeBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/foo", "^\"prop\":\"value\"", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/", "-\"foo\"", rev, null);
+            fail("Must fail with conflict for removeChangedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void removeChangedNodeBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"value\"", branchRev, null);
+        mk.commit("/", "-\"foo\"", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeChangedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void removeChangedNodeBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/", "-\"foo\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop\":\"value\"", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for removeChangedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void changeRemovedNode() {
         String rev = mk.commit("/", "+\"foo\":{}", null, null);
         mk.commit("/", "-\"foo\"", rev, null);
@@ -145,6 +528,55 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void changeRemovedNodeBranchWins() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        branchRev = mk.commit("/", "-\"foo\"", branchRev, null);
+        mk.merge(branchRev, null);
+
+        try {
+            mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
+            fail("Must fail with conflict for changeRemovedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void changeRemovedNodeBranchLoses1() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/", "-\"foo\"", branchRev, null);
+        mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for changeRemovedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @Ignore
+    public void changeRemovedNodeBranchLoses2() {
+        String rev = mk.commit("/", "+\"foo\":{}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop\":\"value\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/", "-\"foo\"", branchRev, null);
+
+        try {
+            mk.merge(branchRev, null);
+            fail("Must fail with conflict for changeRemovedNode");
+        } catch (MicroKernelException e) {
+            // expected
+        }
+    }
+
+    @Test
     public void nonConflictingChangeProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\", \"prop2\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop1\":\"bar\"", rev, null);
@@ -152,6 +584,26 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void nonConflictingChangePropertyWithBranch1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\", \"prop2\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop1\":\"bar\"", branchRev, null);
+        mk.commit("/foo", "^\"prop2\":\"baz\"", rev, null);
+        mk.merge(branchRev, null);
+    }
+
+    @Test
+    public void nonConflictingChangePropertyWithBranch2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\", \"prop2\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop2\":\"baz\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop1\":\"bar\"", branchRev, null);
+        mk.merge(branchRev, null);
+    }
+
+    @Test
     public void nonConflictingAddProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop1\":\"bar\"", rev, null);
@@ -159,9 +611,49 @@ public class ConflictTest extends BaseMo
     }
 
     @Test
+    public void nonConflictingAddPropertyWithBranch1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop1\":\"bar\"", branchRev, null);
+        mk.commit("/foo", "^\"prop2\":\"baz\"", rev, null);
+        mk.merge(branchRev, null);
+    }
+
+    @Test
+    public void nonConflictingAddPropertyWithBranch2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop2\":\"baz\"", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop1\":\"bar\"", branchRev, null);
+        mk.merge(branchRev, null);
+    }
+
+    @Test
     public void nonConflictingRemoveProperty() {
         String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\", \"prop2\":\"value\"}", null, null);
         mk.commit("/foo", "^\"prop1\":\"bar\"", rev, null);
         mk.commit("/foo", "^\"prop2\":null", rev, null);
     }
+
+    @Test
+    public void nonConflictingRemovePropertyWithBranch1() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\", \"prop2\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        // branch commit happens before trunk commit
+        branchRev = mk.commit("/foo", "^\"prop1\":\"bar\"", branchRev, null);
+        mk.commit("/foo", "^\"prop2\":null", rev, null);
+        mk.merge(branchRev, null);
+    }
+
+    @Test
+    public void nonConflictingRemovePropertyWithBranch2() {
+        String rev = mk.commit("/", "+\"foo\":{\"prop1\":\"value\", \"prop2\":\"value\"}", null, null);
+        String branchRev = mk.branch(rev);
+        mk.commit("/foo", "^\"prop2\":null", rev, null);
+        // branch commit happens after trunk commit
+        branchRev = mk.commit("/foo", "^\"prop1\":\"bar\"", branchRev, null);
+        mk.merge(branchRev, null);
+    }
 }

Modified: jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoMKBranchMergeTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoMKBranchMergeTest.java?rev=1475669&r1=1475668&r2=1475669&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoMKBranchMergeTest.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/MongoMKBranchMergeTest.java Thu Apr 25 08:32:03 2013
@@ -279,6 +279,27 @@ public class MongoMKBranchMergeTest exte
     }
 
     @Test
+    public void propertyConflictWithMergedBranch() {
+        addNodes(null, "/trunk");
+        String rev = setProp(null, "/trunk/prop1", "value1");
+        assertPropExists(null, "/trunk", "prop1");
+
+        String branchRev = mk.branch(null);
+
+        branchRev = setProp(branchRev, "/trunk/prop1", "value1a");
+        assertPropValue(branchRev, "/trunk", "prop1", "value1a");
+
+        mk.merge(branchRev, "");
+        assertPropValue(null, "/trunk", "prop1", "value1a");
+        try {
+            setProp(rev, "/trunk/prop1", "value1b");
+            fail("Expected: Concurrent modification exception");
+        } catch (Exception expected) {
+            // expected
+        }
+    }
+
+    @Test
     public void oneBranchChangedPropertiesWithConflict() {
         addNodes(null, "/trunk");
         setProp(null, "/trunk/prop1", "value1");