// Copyright 2017 JanusGraph Authors // // Licensed 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.janusgraph.diskstorage.keycolumnvalue.cache; import com.google.common.base.Preconditions; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.Weigher; import org.janusgraph.core.JanusGraphException; import org.janusgraph.diskstorage.*; import org.janusgraph.diskstorage.keycolumnvalue.*; import org.janusgraph.diskstorage.util.CacheMetricsAction; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.janusgraph.util.datastructures.ByteSize.*; /** * @author Matthias Broecheler (me@matthiasb.com) */ public class ExpirationKCVSCache extends KCVSCache { //Weight estimation private static final int STATICARRAYBUFFER_SIZE = STATICARRAYBUFFER_RAW_SIZE + 10; // 10 = last number is average length private static final int KEY_QUERY_SIZE = OBJECT_HEADER + 4 + 1 + 3 * (OBJECT_REFERENCE + STATICARRAYBUFFER_SIZE); // object_size + int + boolean + 3 static buffers private static final int INVALIDATE_KEY_FRACTION_PENALTY = 1000; private static final int PENALTY_THRESHOLD = 5; private volatile CountDownLatch penaltyCountdown; private final Cache<KeySliceQuery,EntryList> cache; private final ConcurrentHashMap<StaticBuffer,Long> expiredKeys; private final long cacheTimeMS; private final long invalidationGracePeriodMS; private final CleanupThread cleanupThread; public ExpirationKCVSCache(final KeyColumnValueStore store, String metricsName, final long cacheTimeMS, final long invalidationGracePeriodMS, final long maximumByteSize) { super(store, metricsName); Preconditions.checkArgument(cacheTimeMS > 0, "Cache expiration must be positive: %s", cacheTimeMS); Preconditions.checkArgument(System.currentTimeMillis()+1000l*3600*24*365*100+cacheTimeMS>0,"Cache expiration time too large, overflow may occur: %s",cacheTimeMS); this.cacheTimeMS = cacheTimeMS; int concurrencyLevel = Runtime.getRuntime().availableProcessors(); Preconditions.checkArgument(invalidationGracePeriodMS >=0,"Invalid expiration grace peiod: %s", invalidationGracePeriodMS); this.invalidationGracePeriodMS = invalidationGracePeriodMS; CacheBuilder<KeySliceQuery,EntryList> cachebuilder = CacheBuilder.newBuilder() .maximumWeight(maximumByteSize) .concurrencyLevel(concurrencyLevel) .initialCapacity(1000) .expireAfterWrite(cacheTimeMS, TimeUnit.MILLISECONDS) .weigher(new Weigher<KeySliceQuery, EntryList>() { @Override public int weigh(KeySliceQuery keySliceQuery, EntryList entries) { return GUAVA_CACHE_ENTRY_SIZE + KEY_QUERY_SIZE + entries.getByteSize(); } }); cache = cachebuilder.build(); expiredKeys = new ConcurrentHashMap<StaticBuffer, Long>(50,0.75f,concurrencyLevel); penaltyCountdown = new CountDownLatch(PENALTY_THRESHOLD); cleanupThread = new CleanupThread(); cleanupThread.start(); } @Override public EntryList getSlice(final KeySliceQuery query, final StoreTransaction txh) throws BackendException { incActionBy(1, CacheMetricsAction.RETRIEVAL,txh); if (isExpired(query)) { incActionBy(1, CacheMetricsAction.MISS,txh); return store.getSlice(query, unwrapTx(txh)); } try { return cache.get(query,new Callable<EntryList>() { @Override public EntryList call() throws Exception { incActionBy(1, CacheMetricsAction.MISS,txh); return store.getSlice(query, unwrapTx(txh)); } }); } catch (Exception e) { if (e instanceof JanusGraphException) throw (JanusGraphException)e; else if (e.getCause() instanceof JanusGraphException) throw (JanusGraphException)e.getCause(); else throw new JanusGraphException(e); } } @Override public Map<StaticBuffer,EntryList> getSlice(final List<StaticBuffer> keys, final SliceQuery query, final StoreTransaction txh) throws BackendException { Map<StaticBuffer,EntryList> results = new HashMap<StaticBuffer, EntryList>(keys.size()); List<StaticBuffer> remainingKeys = new ArrayList<StaticBuffer>(keys.size()); KeySliceQuery[] ksqs = new KeySliceQuery[keys.size()]; incActionBy(keys.size(), CacheMetricsAction.RETRIEVAL,txh); //Find all cached queries for (int i=0;i<keys.size();i++) { StaticBuffer key = keys.get(i); ksqs[i] = new KeySliceQuery(key,query); EntryList result = null; if (!isExpired(ksqs[i])) result = cache.getIfPresent(ksqs[i]); else ksqs[i]=null; if (result!=null) results.put(key,result); else remainingKeys.add(key); } //Request remaining ones from backend if (!remainingKeys.isEmpty()) { incActionBy(remainingKeys.size(), CacheMetricsAction.MISS,txh); Map<StaticBuffer,EntryList> subresults = store.getSlice(remainingKeys, query, unwrapTx(txh)); for (int i=0;i<keys.size();i++) { StaticBuffer key = keys.get(i); EntryList subresult = subresults.get(key); if (subresult!=null) { results.put(key,subresult); if (ksqs[i]!=null) cache.put(ksqs[i],subresult); } } } return results; } @Override public void clearCache() { cache.invalidateAll(); expiredKeys.clear(); penaltyCountdown = new CountDownLatch(PENALTY_THRESHOLD); } @Override public void invalidate(StaticBuffer key, List<CachableStaticBuffer> entries) { Preconditions.checkArgument(!hasValidateKeysOnly() || entries.isEmpty()); expiredKeys.put(key,getExpirationTime()); if (Math.random()<1.0/INVALIDATE_KEY_FRACTION_PENALTY) penaltyCountdown.countDown(); } @Override public void close() throws BackendException { cleanupThread.stopThread(); super.close(); } private boolean isExpired(final KeySliceQuery query) { Long until = expiredKeys.get(query.getKey()); if (until==null) return false; if (isBeyondExpirationTime(until)) { expiredKeys.remove(query.getKey(),until); return false; } //We suffer a cache miss, hence decrease the count down penaltyCountdown.countDown(); return true; } private final long getExpirationTime() { return System.currentTimeMillis()+cacheTimeMS; } private final boolean isBeyondExpirationTime(long until) { return until<System.currentTimeMillis(); } private final long getAge(long until) { long age = System.currentTimeMillis() - (until-cacheTimeMS); assert age>=0; return age; } private class CleanupThread extends Thread { private boolean stop = false; public CleanupThread() { this.setDaemon(true); this.setName("ExpirationStoreCache-" + getId()); } @Override public void run() { while (true) { if (stop) return; try { penaltyCountdown.await(); } catch (InterruptedException e) { if (stop) return; else throw new RuntimeException("Cleanup thread got interrupted",e); } //Do clean up work by invalidating all entries for expired keys HashMap<StaticBuffer,Long> expiredKeysCopy = new HashMap<StaticBuffer,Long>(expiredKeys.size()); for (Map.Entry<StaticBuffer,Long> expKey : expiredKeys.entrySet()) { if (isBeyondExpirationTime(expKey.getValue())) expiredKeys.remove(expKey.getKey(), expKey.getValue()); else if (getAge(expKey.getValue())>= invalidationGracePeriodMS) expiredKeysCopy.put(expKey.getKey(),expKey.getValue()); } for (KeySliceQuery ksq : cache.asMap().keySet()) { if (expiredKeysCopy.containsKey(ksq.getKey())) cache.invalidate(ksq); } penaltyCountdown = new CountDownLatch(PENALTY_THRESHOLD); for (Map.Entry<StaticBuffer,Long> expKey : expiredKeysCopy.entrySet()) { expiredKeys.remove(expKey.getKey(),expKey.getValue()); } } } void stopThread() { stop = true; this.interrupt(); } } }