// 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.cache;
import com.google.common.collect.Lists;
import org.janusgraph.diskstorage.EntryList;
import org.janusgraph.diskstorage.StaticBuffer;
import org.janusgraph.diskstorage.keycolumnvalue.KeyColumnValueStore;
import org.janusgraph.diskstorage.keycolumnvalue.KeySliceQuery;
import org.janusgraph.diskstorage.keycolumnvalue.SliceQuery;
import org.janusgraph.diskstorage.keycolumnvalue.StoreTransaction;
import org.janusgraph.diskstorage.keycolumnvalue.cache.CacheTransaction;
import org.janusgraph.diskstorage.keycolumnvalue.cache.ExpirationKCVSCache;
import org.janusgraph.diskstorage.keycolumnvalue.cache.KCVSCache;
import org.janusgraph.diskstorage.util.BufferUtil;
import org.junit.Test;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
/**
* @author Matthias Broecheler (me@matthiasb.com)
*/
public class ExpirationCacheTest extends KCVSCacheTest {
public static final String METRICS_STRING = "metrics";
public static final long CACHE_SIZE = 1024*1024*48; //48 MB
@Override
public KCVSCache getCache(KeyColumnValueStore store) {
return getCache(store,Duration.ofDays(1), Duration.ZERO);
}
private static KCVSCache getCache(KeyColumnValueStore store, Duration expirationTime, Duration graceWait) {
return new ExpirationKCVSCache(store,METRICS_STRING,expirationTime.toMillis(),graceWait.toMillis(),CACHE_SIZE);
}
@Test
public void testExpiration() throws Exception {
testExpiration(Duration.ofMillis(200));
testExpiration(Duration.ofSeconds(4));
testExpiration(Duration.ofSeconds(1));
}
private void testExpiration(Duration expirationTime) throws Exception {
final int numKeys = 100, numCols = 10;
loadStore(numKeys,numCols);
//Replace cache with proper times
cache = getCache(store,expirationTime, Duration.ZERO);
StaticBuffer key = BufferUtil.getIntBuffer(81);
List<StaticBuffer> keys = new ArrayList<StaticBuffer>();
keys.add(key);
keys.add(BufferUtil.getIntBuffer(37));
keys.add(BufferUtil.getIntBuffer(2));
SliceQuery query = getQuery(2,8);
verifyResults(key,keys,query,6);
//Modify store directly
StoreTransaction txs = getStoreTx();
store.mutate(key,KeyColumnValueStore.NO_ADDITIONS, Lists.newArrayList(BufferUtil.getIntBuffer(5)),txs);
txs.commit();
Instant utime = times.getTime();
//Should still see cached results
verifyResults(key,keys,query,6);
times.sleepPast(utime.plus(expirationTime.dividedBy(2))); //Sleep half way through expiration time
verifyResults(key, keys, query, 6);
times.sleepPast(utime.plus(expirationTime)); //Sleep past expiration time...
times.sleepFor(Duration.ofMillis(5)); //...and just a little bit longer
//Now the results should be different
verifyResults(key, keys, query, 5);
//If we modify through cache store...
CacheTransaction tx = getCacheTx();
cache.mutateEntries(key, KeyColumnValueStore.NO_ADDITIONS, Lists.newArrayList(getEntry(4, 4)), tx);
tx.commit();
store.resetCounter();
//...invalidation should happen and the result set is updated immediately
verifyResults(key, keys, query, 4);
}
@Test
public void testGracePeriod() throws Exception {
testGracePeriod(Duration.ofMillis(200));
testGracePeriod(Duration.ZERO);
testGracePeriod(Duration.ofSeconds(1));
}
private void testGracePeriod(Duration graceWait) throws Exception {
final int minCleanupTriggerCalls = 5;
final int numKeys = 100, numCols = 10;
loadStore(numKeys,numCols);
//Replace cache with proper times
cache = getCache(store,Duration.ofDays(200),graceWait);
StaticBuffer key = BufferUtil.getIntBuffer(81);
List<StaticBuffer> keys = new ArrayList<StaticBuffer>();
keys.add(key);
keys.add(BufferUtil.getIntBuffer(37));
keys.add(BufferUtil.getIntBuffer(2));
SliceQuery query = getQuery(2,8);
verifyResults(key,keys,query,6);
//If we modify through cache store...
CacheTransaction tx = getCacheTx();
cache.mutateEntries(key,KeyColumnValueStore.NO_ADDITIONS, Lists.newArrayList(getEntry(4,4)),tx);
tx.commit();
Instant utime = times.getTime();
store.resetCounter();
//...invalidation should happen and the result set is updated immediately
verifyResults(key, keys, query, 5);
assertEquals(2,store.getSliceCalls());
//however, the key is expired and hence repeated calls need to go through to the store
verifyResults(key, keys, query, 5);
assertEquals(4,store.getSliceCalls());
//however, when we sleep past the grace wait time and trigger a cleanup...
times.sleepPast(utime.plus(graceWait));
for (int t=0; t<minCleanupTriggerCalls;t++) {
assertEquals(5,cache.getSlice(new KeySliceQuery(key,query),tx).size());
times.sleepFor(Duration.ofMillis(5));
}
//...the cache should cache results again
store.resetCounter();
verifyResults(key, keys, query, 5);
assertEquals(0,store.getSliceCalls());
verifyResults(key, keys, query, 5);
assertEquals(0,store.getSliceCalls());
}
private void verifyResults(StaticBuffer key, List<StaticBuffer> keys, SliceQuery query, int expectedResults) throws Exception {
CacheTransaction tx = getCacheTx();
assertEquals(expectedResults,cache.getSlice(new KeySliceQuery(key,query),tx).size());
Map<StaticBuffer,EntryList> results = cache.getSlice(keys,query,tx);
assertEquals(keys.size(),results.size());
assertEquals(expectedResults, results.get(key).size());
tx.commit();
}
}