| /* |
| * 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; |
| |
| import java.util.Map; |
| |
| import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Key; |
| import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Operation; |
| import org.apache.jackrabbit.oak.plugins.document.util.Utils; |
| import org.jetbrains.annotations.NotNull; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static org.apache.jackrabbit.oak.plugins.document.util.Utils.isCommitted; |
| import static org.apache.jackrabbit.oak.plugins.document.util.Utils.isPropertyName; |
| |
| /** |
| * A <code>Collision</code> happens when a commit modifies a node, which was |
| * also modified in another branch not visible to the current session. This |
| * includes the following situations: |
| * <ul> |
| * <li>Our commit goes to trunk and another session committed to a branch |
| * not yet merged back.</li> |
| * <li>Our commit goes to a branch and another session committed to trunk |
| * or some other branch.</li> |
| * </ul> |
| * Other collisions like concurrent commits to trunk are handled earlier and |
| * do not require collision marking. |
| * See {@link Commit#createOrUpdateNode(DocumentStore, UpdateOp)}. |
| */ |
| class Collision { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(Collision.class); |
| |
| private final NodeDocument document; |
| private final Revision theirRev; |
| private final UpdateOp ourOp; |
| private final Revision ourRev; |
| private final RevisionContext context; |
| private final RevisionVector startRevisions; |
| |
| Collision(@NotNull NodeDocument document, |
| @NotNull Revision theirRev, |
| @NotNull UpdateOp ourOp, |
| @NotNull Revision ourRev, |
| @NotNull RevisionContext context, |
| @NotNull RevisionVector startRevisions) { |
| this.document = checkNotNull(document); |
| this.theirRev = checkNotNull(theirRev); |
| this.ourOp = checkNotNull(ourOp); |
| this.ourRev = checkNotNull(ourRev); |
| this.context = checkNotNull(context); |
| this.startRevisions = checkNotNull(startRevisions); |
| } |
| |
| /** |
| * Marks the collision in the document store. Either our or their |
| * revision is annotated with a collision marker. Their revision is |
| * marked if it is not yet committed, otherwise our revision is marked. |
| * |
| * @param store the document store. |
| * @return the revision that was marked. Either our or their. |
| * @throws DocumentStoreException if the mark operation fails. |
| * @throws IllegalStateException if neither their nor our revision can be |
| * marked because both are already committed. |
| */ |
| @NotNull |
| Revision mark(DocumentStore store) throws DocumentStoreException { |
| // first try to mark their revision |
| if (markCommitRoot(document, theirRev, ourRev, store, context)) { |
| return theirRev; |
| } |
| // their commit wins, we have to mark ourRev |
| NodeDocument newDoc = Collection.NODES.newDocument(store); |
| document.deepCopy(newDoc); |
| UpdateUtils.applyChanges(newDoc, ourOp); |
| if (!markCommitRoot(newDoc, ourRev, theirRev, store, context)) { |
| throw new IllegalStateException("Unable to annotate our revision " |
| + "with collision marker. Our revision: " + ourRev |
| + ", their revision: " + theirRev + ", document:\n" |
| + newDoc.format()); |
| } |
| return ourRev; |
| } |
| |
| /** |
| * Returns {@code true} if this is a conflicting collision, {@code false} |
| * otherwise. |
| * |
| * @return {@code true} if this is a conflicting collision, {@code false} |
| * otherwise. |
| * @throws DocumentStoreException if an operation on the document store |
| * fails. |
| */ |
| boolean isConflicting() throws DocumentStoreException { |
| // their revision is not conflicting when it is identified as branch |
| // commit that cannot be merged (orphaned branch commit, theirRev is |
| // garbage). |
| if (document.getLocalBranchCommits().contains(theirRev) |
| && !startRevisions.isRevisionNewer(theirRev)) { |
| return false; |
| } |
| |
| // did their revision create or delete the node? |
| if (document.getDeleted().containsKey(theirRev)) { |
| return true; |
| } |
| |
| for (Map.Entry<Key, Operation> entry : ourOp.getChanges().entrySet()) { |
| String name = entry.getKey().getName(); |
| if (NodeDocument.isDeletedEntry(name)) { |
| // always conflicts because existence changed |
| return true; |
| } |
| if (isPropertyName(name)) { |
| if (document.getValueMap(name).containsKey(theirRev)) { |
| // concurrent change on the property |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| //--------------------------< internal >------------------------------------ |
| |
| /** |
| * Marks the commit root of the change to the given <code>document</code> in |
| * <code>revision</code>. |
| * |
| * @param document the document. |
| * @param revision the revision of the commit to annotated with a collision |
| * marker. |
| * @param other the revision which detected the collision. |
| * @param store the document store. |
| * @param context the revision context. |
| * @return <code>true</code> if the commit for the given revision was marked |
| * successfully; <code>false</code> otherwise. |
| */ |
| private static boolean markCommitRoot(@NotNull NodeDocument document, |
| @NotNull Revision revision, |
| @NotNull Revision other, |
| @NotNull DocumentStore store, |
| @NotNull RevisionContext context) { |
| Path p = document.getPath(); |
| Path commitRootPath; |
| // first check if we can mark the commit with the given revision |
| if (document.containsRevision(revision)) { |
| if (isCommitted(context.getCommitValue(revision, document))) { |
| // already committed |
| return false; |
| } |
| // node is also commit root, but not yet committed |
| // i.e. a branch commit, which is not yet merged |
| commitRootPath = p; |
| } else { |
| // next look at commit root |
| commitRootPath = document.getCommitRootPath(revision); |
| if (commitRootPath == null) { |
| throwNoCommitRootException(revision, document); |
| } |
| } |
| // at this point we have a commitRootPath |
| UpdateOp op = new UpdateOp(Utils.getIdFromPath(commitRootPath), false); |
| NodeDocument commitRoot = store.find(Collection.NODES, op.getId()); |
| // check commit status of revision |
| if (isCommitted(context.getCommitValue(revision, commitRoot))) { |
| return false; |
| } |
| // check if there is already a collision marker |
| if (commitRoot.getLocalMap(NodeDocument.COLLISIONS).containsKey(revision)) { |
| // already marked |
| return true; |
| } |
| NodeDocument.addCollision(op, revision, other); |
| String commitValue = commitRoot.getLocalRevisions().get(revision); |
| if (commitValue == null) { |
| // no revision entry yet |
| // apply collision marker only if entry is still not there |
| op.containsMapEntry(NodeDocument.REVISIONS, revision, false); |
| } else { |
| // not yet merged branch commit |
| // apply collision marker only if branch commit is still not merged |
| op.equals(NodeDocument.REVISIONS, revision, commitValue); |
| } |
| commitRoot = store.findAndUpdate(Collection.NODES, op); |
| if (commitRoot == null) { |
| // commit state changed meanwhile |
| // -> assume revision is now committed |
| return false; |
| } else { |
| // check again if revision is still not committed |
| // See OAK-3882 |
| if (isCommitted(context.getCommitValue(revision, commitRoot))) { |
| // meanwhile the change was committed and |
| // already moved to a previous document |
| // -> remove collision marker again |
| UpdateOp revert = new UpdateOp(op.getId(), false); |
| NodeDocument.removeCollision(revert, revision); |
| store.findAndUpdate(Collection.NODES, op); |
| return false; |
| } |
| } |
| // otherwise collision marker was set successfully |
| LOG.debug("Marked collision on: {} for {} ({})", |
| commitRootPath, p, revision); |
| return true; |
| } |
| |
| private static void throwNoCommitRootException(@NotNull Revision revision, |
| @NotNull Document document) |
| throws DocumentStoreException { |
| throw new DocumentStoreException("No commit root for revision: " |
| + revision + ", document: " + document.format()); |
| } |
| } |