package org.cloudfoundry.promregator.cache; import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.Assert; import com.github.benmanes.caffeine.cache.AsyncCacheLoader; import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.testing.FakeTicker; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; public class CaffeineAsyncLoadingTest { private static final Logger log = LoggerFactory.getLogger(CaffeineAsyncLoadingTest.class); private final class AsyncCacheLoaderTimingImplementation implements AsyncCacheLoader<String, Integer> { private int executionNumber = 0; @Override public @NonNull CompletableFuture<Integer> asyncLoad(@NonNull String key, @NonNull Executor executor) { log.info(String.format("Request loading iteration %d for request %s", this.executionNumber, key)); Mono<Integer> result = null; synchronized(this) { result = Mono.just(executionNumber++); } result = result.subscribeOn(Schedulers.fromExecutor(executor)); if (this.executionNumber > 1) { result = result.map( x-> { log.info(String.format("Starting to delay - iteration: %d", x)); return x; }).delayElement(Duration.ofMillis(200)) .map( x-> { log.info(String.format("Finished delaying - iteration: %d", x)); return x; }); } result = result.cache(); return result.toFuture(); } } @Test public void testRefreshIsAsynchronous() throws InterruptedException { FakeTicker ticker = new FakeTicker(); AsyncLoadingCache<String, Integer> subject = Caffeine.newBuilder() .expireAfterAccess(240, TimeUnit.SECONDS) .refreshAfterWrite(120, TimeUnit.SECONDS) .ticker(ticker::read) .recordStats() .buildAsync(new AsyncCacheLoaderTimingImplementation()); log.info("Starting first request"); Assert.assertEquals(new Integer(0), Mono.fromFuture(subject.get("a")).block()); log.info("Stats on cache: "+subject.synchronous().stats().toString()); ticker.advance(Duration.ofSeconds(10)); log.info("Sending second request"); Assert.assertEquals(new Integer(0), Mono.fromFuture(subject.get("a")).block()); log.info("Stats on cache: "+subject.synchronous().stats().toString()); ticker.advance(Duration.ofSeconds(120)); log.info("Sending third request"); Assert.assertEquals(new Integer(0), Mono.fromFuture(subject.get("a")).block()); // That's the interesting case here! Note the zero above: This means that we get old cache data (which is what we want!) log.info("Stats on cache: "+subject.synchronous().stats().toString()); ticker.advance(Duration.ofSeconds(10)); Thread.sleep(250); // wait until async loading took place log.info("Sending fourth request"); Assert.assertEquals(new Integer(1), Mono.fromFuture(subject.get("a")).block()); log.info("Stats on cache: "+subject.synchronous().stats().toString()); } private final class AsyncCacheLoaderFailureImplementation implements AsyncCacheLoader<String, Integer> { private int executionNumber = 0; @Override public @NonNull CompletableFuture<Integer> asyncLoad(@NonNull String key, @NonNull Executor executor) { log.info(String.format("Request loading iteration %d for request %s", this.executionNumber, key)); Mono<Integer> result = null; synchronized(this) { result = Mono.just(executionNumber++); } result = result.subscribeOn(Schedulers.fromExecutor(executor)).cache(); if (this.executionNumber > 1) { result = Mono.error(new Error("Failure while async loading")); } return result.toFuture(); } } @Test public void testFailureOnAsynchronous() { FakeTicker ticker = new FakeTicker(); AsyncLoadingCache<String, Integer> subject = Caffeine.newBuilder() .expireAfterAccess(240, TimeUnit.SECONDS) .refreshAfterWrite(120, TimeUnit.SECONDS) .ticker(ticker::read) .recordStats() .buildAsync(new AsyncCacheLoaderFailureImplementation()); Assert.assertEquals(new Integer(0), Mono.fromFuture(subject.get("a")).block()); ticker.advance(Duration.ofSeconds(10)); Assert.assertEquals(new Integer(0), Mono.fromFuture(subject.get("a")).block()); ticker.advance(Duration.ofSeconds(250)); Mono<Integer> errorMono = Mono.fromFuture(subject.get("a")); boolean thrown = false; try { errorMono.block(); thrown = false; } catch (Throwable t) { thrown = true; } Assert.assertTrue(thrown); } }