// 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();
}
}
}