package org.rapidoid.cache; import org.junit.Test; import org.rapidoid.annotation.Authors; import org.rapidoid.annotation.Since; import org.rapidoid.cache.impl.CacheStats; import org.rapidoid.cache.impl.ConcurrentCacheAtomWithStats; import org.rapidoid.commons.Rnd; import org.rapidoid.io.IO; import org.rapidoid.lambda.Mapper; import org.rapidoid.log.Log; import org.rapidoid.test.TestCommons; import org.rapidoid.u.U; import org.rapidoid.util.BenchmarkOperation; import org.rapidoid.util.Msc; import java.util.concurrent.atomic.AtomicInteger; /* * #%L * rapidoid-commons * %% * Copyright (C) 2014 - 2017 Nikolche Mihajlovski and contributors * %% * 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. * #L% */ @Authors("Nikolche Mihajlovski") @Since("5.3.0") public class SimpleCachingTest extends TestCommons { private static final Mapper<Integer, Integer> N_TO_N = new Mapper<Integer, Integer>() { @Override public Integer map(Integer key) throws Exception { return key; } }; private static Mapper<String, Integer> LENGTH = new Mapper<String, Integer>() { @Override public Integer map(String src) throws Exception { return src.length(); } }; private static Mapper<Integer, Integer> NEXT = new Mapper<Integer, Integer>() { @Override public Integer map(Integer x) throws Exception { return x + 1; } }; private static Mapper<String, String> ABC = new Mapper<String, String>() { @Override public String map(String key) throws Exception { return IO.load(key); } }; @Test public void testCachedValue() { final ConcurrentCacheAtomWithStats<String, String> cached = new ConcurrentCacheAtomWithStats<>("cached-file.txt", ABC, 10, new CacheStats()); Msc.benchmarkMT(100, "reads", 10000000, new Runnable() { @Override public void run() { try { eq(cached.get(), "ABC"); } catch (Exception e) { throw U.rte(e); } } }); U.print(cached); } @SuppressWarnings("unchecked") @Test public void testCache() { final int capacity = 1000; final Cache<Integer, Integer> cache = Caching.of(NEXT).capacity(capacity).ttl(10).statistics(true).build(); Msc.benchmarkMT(100, "ops", 10000000, new Runnable() { @Override public void run() { int n = Rnd.rnd(capacity * 100); if (Rnd.rnd(3) == 0) cache.invalidate(n); Integer maybe = cache.getIfExists(n); isTrue(maybe == null || maybe == n + 1); eq(cache.get(n).intValue(), n + 1); if (Rnd.rnd(5) == 0) cache.set(n, n + 1); } }); CacheStats stats = cache.stats(); U.print(stats); } @SuppressWarnings("unchecked") @Test public void testPreloadedCache() { int count = 100_000; final Cache<Integer, Integer> cache = Caching.of(N_TO_N).statistics(true).build(); loadCacheValues(cache, count); CacheStats stats = cache.stats(); stats.reset(); final int mask = Msc.bitMask(10); // 1024 hot keys int total = 200_000_000; Msc.benchmarkMT(8, "ops", total, new BenchmarkOperation() { @Override public void run(int i) { int key = i & mask; int n = cache.get(key); eq(n, key); } }); U.print(stats); eq(stats.hits.get(), total); eq(stats.total(), total); eq(cache.size(), count); // 1024 hot keys, with L1 cache size == 512 -> expecting 50% L1 hit rate double l1HitRate = stats.l1Hits.get() * 1.0 / stats.l1Misses.get(); Log.info("L1 hit rate", "rate", l1HitRate); isTrue(0.8 < l1HitRate); isTrue(l1HitRate < 1.2); } private void loadCacheValues(Cache<Integer, ?> cache, int countTo) { for (int i = 0; i < countTo; i++) { cache.get(Rnd.rnd(countTo)); } U.print(cache.stats()); for (int i = 0; i < countTo; i++) { cache.get(i); } U.print(cache.stats()); for (int i = 0; i < countTo; i++) { cache.get(i); } U.print(cache.stats()); } @SuppressWarnings("unchecked") @Test public void testCacheCrawling() { Cache<Integer, Integer> cache = Caching.of(N_TO_N).capacity(100).statistics(true).build(); eq(cache.get(1000).intValue(), 1000); U.sleep(1500); eq(Caching.scheduler().getCompletedTaskCount(), 0); eq(cache.stats().crawls.get(), 0); Cache<Integer, Integer> cacheWithTTL = Caching.of(N_TO_N).capacity(100).statistics(true).ttl(100).build(); eq(cacheWithTTL.get(22).intValue(), 22); U.sleep(1500); eq(Caching.scheduler().getCompletedTaskCount(), 1); isTrue(cacheWithTTL.stats().crawls.get() > 0); isFalse(Caching.scheduler().isShutdown()); isFalse(Caching.scheduler().isTerminated()); isFalse(Caching.scheduler().isTerminating()); Caching.shutdown(); isTrue(Caching.scheduler().isShutdown()); isTrue(Caching.scheduler().isTerminated()); } @Test @SuppressWarnings("unchecked") public void testCacheInvalidation() { final AtomicInteger value = new AtomicInteger(); final Cache<Integer, Integer> cache = Caching.of(new Mapper<Integer, Integer>() { @Override public Integer map(Integer key) throws Exception { return value.get(); } }).capacity(64).build(); for (int i = 0; i < 100; i++) { iteration(value, cache); } } private void iteration(AtomicInteger value, final Cache<Integer, Integer> cache) { final int max = 2000; final int n = value.incrementAndGet(); for (int i = 0; i < max; i++) { cache.set(i, -n); } checkValues("changed", cache, max, -n, true); for (int i = 0; i < max; i++) { cache.invalidate(i); } checkValues("invalidated", cache, max, n, false); } private void checkValues(String op, final Cache<Integer, Integer> cache, final int max, final int expect, final boolean optional) { Msc.benchmarkMT(64, op, 640_000, new BenchmarkOperation() { @Override public void run(int i) { int key = i % max; Integer maybeVal = cache.getIfExists(key); if (maybeVal != null) { eq(maybeVal.intValue(), expect); } if (!optional) { eq(cache.get(key).intValue(), expect); } } }); } }