Index: oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreBranch.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreBranch.java (revision 1448563) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreBranch.java (working copy) @@ -19,8 +19,12 @@ import org.apache.jackrabbit.mk.api.MicroKernel; import org.apache.jackrabbit.mk.api.MicroKernelException; import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; import org.apache.jackrabbit.oak.spi.commit.CommitHook; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStateDiff; import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch; import static com.google.common.base.Preconditions.checkArgument; @@ -29,6 +33,16 @@ import static org.apache.jackrabbit.oak.commons.PathUtils.elements; import static org.apache.jackrabbit.oak.commons.PathUtils.getName; import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.ADD_EXISTING_NODE; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.ADD_EXISTING_PROPERTY; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.CHANGE_CHANGED_PROPERTY; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.CHANGE_DELETED_NODE; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.CHANGE_DELETED_PROPERTY; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.CONFLICT; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.DELETE_CHANGED_NODE; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.DELETE_CHANGED_PROPERTY; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.DELETE_DELETED_NODE; +import static org.apache.jackrabbit.oak.plugins.commit.MergingNodeStateDiff.DELETE_DELETED_PROPERTY; /** * {@code NodeStoreBranch} based on {@link MicroKernel} branching and merging. @@ -46,12 +60,15 @@ /** Revision of the base state of this branch*/ private String baseRevision; - /** Root state of the head revision of this branch*/ + /** Root state of the transient head revision on top of persisted branch, null if merged. */ private NodeState head; - /** Head revision of this branch, null if not yet branched*/ + /** Head revision of persisted branch, null if not yet branched*/ private String headRevision; + /** Number of updates to this branch via {@link #setRoot(NodeState)} */ + private int updates = 0; + KernelNodeStoreBranch(KernelNodeStore store, KernelNodeState root) { this.store = store; this.base = root; @@ -74,9 +91,20 @@ public void setRoot(NodeState newRoot) { checkNotMerged(); if (!head.equals(newRoot)) { - JsopDiff diff = new JsopDiff(store.getKernel()); - newRoot.compareAgainstBaseState(head, diff); - commit(diff.toString()); + NodeState oldRoot = head; + head = newRoot; + if (++updates > 1) { + // persist unless this is the first update + boolean success = false; + try { + persistTransientHead(); + success = true; + } finally { + if (!success) { + head = oldRoot; + } + } + } } } @@ -127,22 +155,35 @@ checkNotMerged(); NodeState toCommit = checkNotNull(hook).processCommit(base, head); NodeState oldRoot = head; - setRoot(toCommit); + head = toCommit; try { - if (headRevision == null) { + if (head.equals(base)) { // Nothing was written to this branch: return base state head = null; // Mark as merged return base; } else { MicroKernel kernel = store.getKernel(); - String mergedRevision = kernel.merge(headRevision, null); - headRevision = null; + String newRevision; + JsopDiff diff = new JsopDiff(kernel); + if (headRevision == null) { + // no branch created yet, commit directly + head.compareAgainstBaseState(base, diff); + newRevision = kernel.commit("", diff.toString(), baseRevision, null); + } else { + // commit into branch and merge + head.compareAgainstBaseState(store.getRootState(headRevision), diff); + if (diff.toString().length() > 0) { + headRevision = kernel.commit("", diff.toString(), headRevision, null); + } + newRevision = kernel.merge(headRevision, null); + headRevision = null; + } head = null; // Mark as merged - return store.getRootState(mergedRevision); + return store.getRootState(newRevision); } } catch (MicroKernelException e) { - setRoot(oldRoot); + head = oldRoot; throw new CommitFailedException(e); } } @@ -150,12 +191,22 @@ @Override public void rebase() { KernelNodeState root = store.getRoot(); - if (headRevision == null) { + if (head.equals(root)) { // Nothing was written to this branch: set new base revision head = root; base = root; baseRevision = root.getRevision(); + } else if (headRevision == null) { + // Nothing written to persistent branch yet + // perform rebase in memory + NodeBuilder builder = new MemoryNodeBuilder(root); + getRoot().compareAgainstBaseState(getBase(), new RebaseDiff(builder)); + head = builder.getNodeState(); + base = root; + baseRevision = root.getRevision(); } else { + // perform rebase in kernel + persistTransientHead(); headRevision = store.getKernel().rebase(headRevision, root.getRevision()); head = store.getRootState(headRevision); base = root; @@ -189,7 +240,136 @@ headRevision = kernel.branch(baseRevision); } + // persist transient changes first + persistTransientHead(); + headRevision = kernel.commit("", jsop, headRevision, null); head = store.getRootState(headRevision); } + + private void persistTransientHead() { + NodeState oldBase = base; + String oldBaseRevision = baseRevision; + NodeState oldHead = head; + String oldHeadRevision = headRevision; + boolean success = false; + try { + MicroKernel kernel = store.getKernel(); + JsopDiff diff = new JsopDiff(store.getKernel()); + if (headRevision == null) { + // no persistent branch yet + if (head.equals(base)) { + // nothing to persist + success = true; + return; + } else { + // create branch + headRevision = kernel.branch(baseRevision); + head.compareAgainstBaseState(base, diff); + } + } else { + // compare against head of branch + NodeState branchHead = store.getRootState(headRevision); + if (head.equals(branchHead)) { + // nothing to persist + return; + } else { + head.compareAgainstBaseState(branchHead, diff); + } + } + // if we get here we have something to persist + // and a branch exists + headRevision = kernel.commit("", diff.toString(), headRevision, null); + head = store.getRootState(headRevision); + success = true; + } finally { + // revert to old state if unsuccessful + if (!success) { + base = oldBase; + baseRevision = oldBaseRevision; + head = oldHead; + headRevision = oldHeadRevision; + } + } + } + + // FIXME: copied from SegmentNodeStoreBranch + private class RebaseDiff implements NodeStateDiff { + + private final NodeBuilder builder; + + RebaseDiff(NodeBuilder builder) { + this.builder = builder; + } + + @Override + public void propertyAdded(PropertyState after) { + PropertyState other = builder.getProperty(after.getName()); + if (other == null) { + builder.setProperty(after); + } else if (!other.equals(after)) { + conflictMarker(ADD_EXISTING_PROPERTY).setProperty(after); + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + PropertyState other = builder.getProperty(before.getName()); + if (other == null) { + conflictMarker(CHANGE_DELETED_PROPERTY).setProperty(after); + } else if (other.equals(before)) { + builder.setProperty(after); + } else if (!other.equals(after)) { + conflictMarker(CHANGE_CHANGED_PROPERTY).setProperty(after); + } + } + + @Override + public void propertyDeleted(PropertyState before) { + PropertyState other = builder.getProperty(before.getName()); + if (other == null) { + conflictMarker(DELETE_DELETED_PROPERTY).setProperty(before); + } else if (other.equals(before)) { + builder.removeProperty(before.getName()); + } else { + conflictMarker(DELETE_CHANGED_PROPERTY).setProperty(before); + } + } + + @Override + public void childNodeAdded(String name, NodeState after) { + if (builder.hasChildNode(name)) { + conflictMarker(ADD_EXISTING_NODE).setNode(name, after); + } else { + builder.setNode(name, after); + } + } + + @Override + public void childNodeChanged( + String name, NodeState before, NodeState after) { + if (builder.hasChildNode(name)) { + after.compareAgainstBaseState( + before, new RebaseDiff(builder.child(name))); + } else { + conflictMarker(CHANGE_DELETED_NODE).setNode(name, after); + } + } + + @Override + public void childNodeDeleted(String name, NodeState before) { + if (!builder.hasChildNode(name)) { + conflictMarker(DELETE_DELETED_NODE).setNode(name, before); + } else if (before.equals(builder.child(name).getNodeState())) { + builder.removeNode(name); + } else { + conflictMarker(DELETE_CHANGED_NODE).setNode(name, before); + } + } + + private NodeBuilder conflictMarker(String name) { + return builder.child(CONFLICT).child(name); + } + + } }