| /* |
| * 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.Collections; |
| import java.util.Map; |
| |
| import com.google.common.cache.Cache; |
| import com.google.common.collect.Maps; |
| |
| import org.apache.jackrabbit.oak.cache.CacheStats; |
| import org.apache.jackrabbit.oak.cache.CacheValue; |
| import org.apache.jackrabbit.oak.commons.json.JsopBuilder; |
| import org.apache.jackrabbit.oak.commons.json.JsopReader; |
| import org.apache.jackrabbit.oak.commons.json.JsopTokenizer; |
| import org.apache.jackrabbit.oak.plugins.document.util.RevisionsKey; |
| import org.apache.jackrabbit.oak.plugins.document.util.StringValue; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * A diff cache, which is pro-actively filled after a commit. |
| */ |
| public class LocalDiffCache extends DiffCache { |
| private static final Logger LOG = LoggerFactory.getLogger(LocalDiffCache.class); |
| |
| /** |
| * Limit is arbitrary for now i.e. 16 MB. Same as in MongoDiffCache |
| */ |
| private static int MAX_ENTRY_SIZE = 16 * 1024 * 1024; |
| |
| private final Cache<RevisionsKey, Diff> diffCache; |
| private final CacheStats diffCacheStats; |
| |
| LocalDiffCache(DocumentNodeStoreBuilder<?> builder) { |
| this.diffCache = builder.buildLocalDiffCache(); |
| this.diffCacheStats = new CacheStats(diffCache, |
| "Document-LocalDiff", |
| builder.getWeigher(), builder.getLocalDiffCacheSize()); |
| } |
| |
| @Override |
| public String getChanges(@NotNull RevisionVector from, |
| @NotNull RevisionVector to, |
| @NotNull Path path, |
| @Nullable Loader loader) { |
| RevisionsKey key = new RevisionsKey(from, to); |
| Diff diff = diffCache.getIfPresent(key); |
| if (diff != null) { |
| String result = diff.get(path); |
| return result != null ? result : ""; |
| } |
| if (loader != null) { |
| return loader.call(); |
| } |
| return null; |
| } |
| |
| @NotNull |
| @Override |
| public Entry newEntry(final @NotNull RevisionVector from, |
| final @NotNull RevisionVector to, |
| boolean local /*ignored*/) { |
| return new Entry() { |
| private final Map<Path, String> changesPerPath = Maps.newHashMap(); |
| private long size; |
| @Override |
| public void append(@NotNull Path path, @NotNull String changes) { |
| if (exceedsSize()){ |
| return; |
| } |
| size += path.getMemory() + size(changes); |
| changesPerPath.put(path, changes); |
| } |
| |
| @Override |
| public boolean done() { |
| if (exceedsSize()){ |
| return false; |
| } |
| diffCache.put(new RevisionsKey(from, to), |
| new Diff(changesPerPath, size)); |
| LOG.debug("Adding cache entry from {} to {}", from, to); |
| return true; |
| } |
| |
| private boolean exceedsSize(){ |
| return size > MAX_ENTRY_SIZE; |
| } |
| }; |
| } |
| |
| @NotNull |
| @Override |
| public Iterable<CacheStats> getStats() { |
| return Collections.singleton(diffCacheStats); |
| } |
| |
| @Override |
| public void invalidateAll() { |
| diffCache.invalidateAll(); |
| } |
| |
| //-----------------------------< internal >--------------------------------- |
| |
| |
| public static final class Diff implements CacheValue { |
| |
| private final Map<Path, String> changes; |
| private long memory; |
| |
| public Diff(Map<Path, String> changes, long memory) { |
| this.changes = changes; |
| this.memory = memory; |
| } |
| |
| public static Diff fromString(String value) { |
| Map<Path, String> map = Maps.newHashMap(); |
| JsopReader reader = new JsopTokenizer(value); |
| while (true) { |
| if (reader.matches(JsopReader.END)) { |
| break; |
| } |
| String k = reader.readString(); |
| reader.read(':'); |
| String v = reader.readString(); |
| map.put(Path.fromString(k), v); |
| if (reader.matches(JsopReader.END)) { |
| break; |
| } |
| reader.read(','); |
| } |
| return new Diff(map, 0); |
| } |
| |
| public String asString(){ |
| JsopBuilder builder = new JsopBuilder(); |
| for (Map.Entry<Path, String> entry : changes.entrySet()) { |
| builder.key(entry.getKey().toString()); |
| builder.value(entry.getValue()); |
| } |
| return builder.toString(); |
| } |
| |
| public Map<Path, String> getChanges() { |
| return Collections.unmodifiableMap(changes); |
| } |
| |
| @Override |
| public int getMemory() { |
| if (memory == 0) { |
| long m = 0; |
| for (Map.Entry<Path, String> e : changes.entrySet()){ |
| m += e.getKey().getMemory() + size(e.getValue()); |
| } |
| memory = m; |
| } |
| if (memory > Integer.MAX_VALUE) { |
| LOG.debug("Estimated memory footprint larger than Integer.MAX_VALUE: {}.", memory); |
| return Integer.MAX_VALUE; |
| } else { |
| return (int) memory; |
| } |
| } |
| |
| String get(Path path) { |
| return changes.get(path); |
| } |
| |
| @Override |
| public String toString() { |
| return asString(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == this) { |
| return true; |
| } |
| if (obj instanceof Diff) { |
| Diff other = (Diff) obj; |
| return changes.equals(other.changes); |
| } |
| return false; |
| } |
| } |
| |
| private static long size(String s){ |
| return StringValue.getMemory(s); |
| } |
| } |