| /* |
| * 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.List; |
| |
| import com.google.common.base.Supplier; |
| import com.google.common.cache.Cache; |
| |
| import org.apache.jackrabbit.oak.plugins.document.util.Utils; |
| import org.apache.jackrabbit.oak.stats.Clock; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import static com.google.common.cache.CacheBuilder.newBuilder; |
| import static com.google.common.collect.ImmutableList.of; |
| |
| /** |
| * Resolves the commit value for a given change revision on a document. |
| */ |
| final class CachingCommitValueResolver implements CommitValueResolver { |
| |
| private static final List<String> COMMIT_ROOT_OR_REVISIONS |
| = of(NodeDocument.COMMIT_ROOT, NodeDocument.REVISIONS); |
| |
| private final Cache<Revision, String> commitValueCache; |
| |
| private final int cacheSize; |
| |
| private final Supplier<RevisionVector> sweepRevisions; |
| |
| CachingCommitValueResolver(int cacheSize, Supplier<RevisionVector> sweepRevisions) { |
| this.commitValueCache = newBuilder().maximumSize(cacheSize).build(); |
| this.cacheSize = cacheSize; |
| this.sweepRevisions = sweepRevisions; |
| } |
| |
| CommitValueResolver withEmptyCommitValueCache(boolean enable, |
| Clock clock, |
| long maxAgeMillis) { |
| if (enable) { |
| return new CommitValueResolver() { |
| |
| private final Cache<Revision, String> emptyCommitValueCache |
| = newBuilder().maximumSize(cacheSize).build(); |
| |
| @Override |
| public String resolve(@NotNull Revision changeRevision, |
| @NotNull NodeDocument doc) { |
| // check cache first |
| String value = commitValueCache.getIfPresent(changeRevision); |
| if (value != null) { |
| return value; |
| } |
| // check negative cache |
| if (emptyCommitValueCache.getIfPresent(changeRevision) != null) { |
| return null; |
| } |
| value = CachingCommitValueResolver.this.resolve(changeRevision, doc); |
| if (value == null && isOld(changeRevision, clock, maxAgeMillis)) { |
| // remember |
| emptyCommitValueCache.put(changeRevision, ""); |
| } |
| return value; |
| } |
| |
| private boolean isOld(Revision r, Clock c, long maxAgeMillis) { |
| return r.getTimestamp() + maxAgeMillis < c.getTime(); |
| } |
| }; |
| } else { |
| return this; |
| } |
| } |
| |
| @Override |
| public String resolve(@NotNull Revision changeRevision, |
| @NotNull NodeDocument doc) { |
| // check cache first |
| String value = commitValueCache.getIfPresent(changeRevision); |
| if (value != null) { |
| return value; |
| } |
| // need to determine the commit value |
| doc = resolveDocument(doc, changeRevision); |
| if (doc == null) { |
| // the document including its history does not contain a change |
| // for the given revision |
| return null; |
| } |
| // at this point 'doc' is guaranteed to have a local entry |
| // for the given change revision |
| if (sweepRevisions.get().isRevisionNewer(changeRevision)) { |
| // change revision is newer than sweep revision |
| // resolve the commit value without any short cuts |
| value = doc.resolveCommitValue(changeRevision); |
| } else { |
| // change revision is equal or older than sweep revision |
| // there are different cases: |
| // - doc is a main document and we have a guarantee that a |
| // potential branch commit is marked accordingly |
| // - doc is a split document and the revision is guaranteed |
| // to be committed. the remaining question is whether the |
| // revision is from a branch commit |
| NodeDocument.SplitDocType sdt = doc.getSplitDocType(); |
| if (sdt == NodeDocument.SplitDocType.NONE) { |
| // sweeper ensures that all changes on main document |
| // before the sweep revision are properly marked with |
| // branch commit entry if applicable |
| if (doc.getLocalBranchCommits().contains(changeRevision)) { |
| // resolve the commit value the classic way |
| value = doc.resolveCommitValue(changeRevision); |
| } else { |
| value = "c"; |
| } |
| } else if (sdt == NodeDocument.SplitDocType.DEFAULT_NO_BRANCH) { |
| // split document without branch commits, we don't have |
| // to check the commit root, the commit value is always 'c' |
| value = "c"; |
| } else { |
| // some other split document type introduced |
| // before Oak 1.8 and we don't know if this is a branch |
| // commit. first try to resolve |
| value = doc.resolveCommitValue(changeRevision); |
| if (value == null) { |
| // then it must be a non-branch commit and the |
| // split document with the commit value was |
| // already garbage collected |
| value = "c"; |
| } |
| } |
| } |
| if (Utils.isCommitted(value)) { |
| // only cache committed states |
| // e.g. branch commits may be merged later and |
| // the commit value will change |
| commitValueCache.put(changeRevision, value); |
| } |
| return value; |
| } |
| |
| /** |
| * Resolves to the document that contains the change with the given |
| * revision. If the given document contains a local change for the given |
| * revision, then the passed document is returned. Otherwise this method |
| * looks up previous documents and returns one with a change for the given |
| * revision. This method returns {@code null} if neither the passed document |
| * nor any of its previous documents contains a change for the given |
| * revision. |
| * |
| * @param doc the document to resolve for the given change revision. |
| * @param changeRevision the revision of a change. |
| * @return the document with the change or {@code null} if there is no |
| * document with such a change. |
| */ |
| @Nullable |
| private NodeDocument resolveDocument(@NotNull NodeDocument doc, |
| @NotNull Revision changeRevision) { |
| // check if the document contains the change or we need to |
| // look up previous documents for the actual change |
| if (doc.getLocalCommitRoot().containsKey(changeRevision) |
| || doc.getLocalRevisions().containsKey(changeRevision)) { |
| return doc; |
| } |
| // find the previous document with this change |
| // first check if there is a commit root entry for this revision |
| NodeDocument d = null; |
| for (String p : COMMIT_ROOT_OR_REVISIONS) { |
| for (NodeDocument prev : doc.getPreviousDocs(p, changeRevision)) { |
| d = prev; |
| break; |
| } |
| if (d != null) { |
| break; |
| } |
| } |
| return d; |
| } |
| |
| } |