/* * Copyright 2015 Ben Manes. All Rights Reserved. * * 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 com.github.benmanes.caffeine.cache; import static com.github.benmanes.caffeine.cache.testing.HasRemovalNotifications.hasRemovalNotifications; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasLoadFailureCount; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasLoadSuccessCount; import static com.github.benmanes.caffeine.testing.Awaits.await; import static com.github.benmanes.caffeine.testing.IsEmptyMap.emptyMap; import static com.github.benmanes.caffeine.testing.IsFutureValue.futureOf; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import org.testng.annotations.Listeners; import org.testng.annotations.Test; import com.github.benmanes.caffeine.cache.Policy.Expiration; import com.github.benmanes.caffeine.cache.testing.CacheContext; import com.github.benmanes.caffeine.cache.testing.CacheProvider; import com.github.benmanes.caffeine.cache.testing.CacheSpec; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Advance; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExecutor; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Implementation; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Loader; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheSpec.ReferenceType; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; import com.github.benmanes.caffeine.cache.testing.CheckNoWriter; import com.github.benmanes.caffeine.cache.testing.RefreshAfterWrite; import com.github.benmanes.caffeine.cache.testing.RemovalNotification; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; /** * The test cases for caches that support the refresh after write policy. * * @author ben.manes@gmail.com (Ben Manes) */ @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) public final class RefreshAfterWriteTest { /* ---------------- getIfPresent -------------- */ @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.NEGATIVE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void getIfPresent(LoadingCache<Integer, Integer> cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); assertThat(cache.getIfPresent(context.middleKey()), is(-context.middleKey())); context.ticker().advance(45, TimeUnit.SECONDS); assertThat(cache.getIfPresent(context.middleKey()), is(-context.middleKey())); assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.NEGATIVE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void getIfPresent(AsyncLoadingCache<Integer, Integer> cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); assertThat(cache.getIfPresent(context.middleKey()), is(futureOf(-context.middleKey()))); context.ticker().advance(45, TimeUnit.SECONDS); assertThat(cache.getIfPresent(context.middleKey()), is(futureOf(-context.middleKey()))); assertThat(cache.synchronous().estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } /* ---------------- getAllPresent -------------- */ @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void getAllPresent(LoadingCache<Integer, Integer> cache, CacheContext context) { int count = context.firstMiddleLastKeys().size(); context.ticker().advance(30, TimeUnit.SECONDS); cache.getAllPresent(context.firstMiddleLastKeys()); context.ticker().advance(45, TimeUnit.SECONDS); assertThat(cache.getAllPresent(context.firstMiddleLastKeys()).size(), is(count)); assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.REPLACED)); } /* ---------------- getFunc -------------- */ @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void getFunc(LoadingCache<Integer, Integer> cache, CacheContext context) { Function<Integer, Integer> mappingFunction = context.original()::get; context.ticker().advance(30, TimeUnit.SECONDS); cache.get(context.firstKey(), mappingFunction); context.ticker().advance(45, TimeUnit.SECONDS); cache.get(context.lastKey(), mappingFunction); // refreshed assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) @SuppressWarnings("FutureReturnValueIgnored") public void getFunc(AsyncLoadingCache<Integer, Integer> cache, CacheContext context) { Function<Integer, Integer> mappingFunction = context.original()::get; context.ticker().advance(30, TimeUnit.SECONDS); cache.get(context.firstKey(), mappingFunction); context.ticker().advance(45, TimeUnit.SECONDS); cache.get(context.lastKey(), mappingFunction); // refreshed assertThat(cache.synchronous().estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } /* ---------------- get -------------- */ @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void get(LoadingCache<Integer, Integer> cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.get(context.firstKey()); cache.get(context.absentKey()); context.ticker().advance(45, TimeUnit.SECONDS); assertThat(cache.getIfPresent(context.firstKey()), is(-context.firstKey())); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) @SuppressWarnings("FutureReturnValueIgnored") public void get(AsyncLoadingCache<Integer, Integer> cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.get(context.firstKey()); cache.get(context.absentKey()); context.ticker().advance(45, TimeUnit.SECONDS); assertThat(cache.getIfPresent(context.firstKey()), is(futureOf(-context.firstKey()))); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED, compute = Compute.ASYNC, values = ReferenceType.STRONG) public void get_sameFuture(CacheContext context) { AtomicBoolean done = new AtomicBoolean(); AsyncLoadingCache<Integer, Integer> cache = context.buildAsync(key -> { await().untilTrue(done); return -key; }); Integer key = 1; cache.synchronous().put(key, key); CompletableFuture<Integer> original = cache.get(key); for (int i = 0; i < 10; i++) { context.ticker().advance(1, TimeUnit.MINUTES); CompletableFuture<Integer> next = cache.get(key); assertThat(next, is(sameInstance(original))); } done.set(true); await().until(() -> cache.synchronous().getIfPresent(key), is(-key)); } @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.NULL) public void get_null(AsyncLoadingCache<Integer, Integer> cache, CacheContext context) { Integer key = 1; cache.synchronous().put(key, key); context.ticker().advance(2, TimeUnit.MINUTES); await().until(() -> cache.synchronous().getIfPresent(key), is(nullValue())); } /* ---------------- getAll -------------- */ @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) public void getAll(LoadingCache<Integer, Integer> cache, CacheContext context) { List<Integer> keys = ImmutableList.of(context.firstKey(), context.absentKey()); context.ticker().advance(30, TimeUnit.SECONDS); assertThat(cache.getAll(keys), is(ImmutableMap.of(context.firstKey(), -context.firstKey(), context.absentKey(), context.absentKey()))); // Trigger a refresh, may return old values context.ticker().advance(45, TimeUnit.SECONDS); cache.getAll(keys); // Ensure new values are present assertThat(cache.getAll(keys), is(ImmutableMap.of(context.firstKey(), context.firstKey(), context.absentKey(), context.absentKey()))); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) @SuppressWarnings("FutureReturnValueIgnored") public void getAll(AsyncLoadingCache<Integer, Integer> cache, CacheContext context) { List<Integer> keys = ImmutableList.of(context.firstKey(), context.absentKey()); context.ticker().advance(30, TimeUnit.SECONDS); assertThat(cache.getAll(keys), is(futureOf(ImmutableMap.of(context.firstKey(), -context.firstKey(), context.absentKey(), context.absentKey())))); // Trigger a refresh, may return old values context.ticker().advance(45, TimeUnit.SECONDS); cache.getAll(keys); // Ensure new values are present assertThat(cache.getAll(keys), is(futureOf(ImmutableMap.of(context.firstKey(), context.firstKey(), context.absentKey(), context.absentKey())))); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.REPLACED)); } /* ---------------- put -------------- */ @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) public void put(CacheContext context) { AtomicBoolean refresh = new AtomicBoolean(); Integer key = context.absentKey(); Integer original = 1; Integer updated = 2; Integer refreshed = 3; LoadingCache<Integer, Integer> cache = context.build(k -> { await().untilTrue(refresh); return refreshed; }); cache.put(key, original); context.ticker().advance(2, TimeUnit.MINUTES); assertThat(cache.getIfPresent(key), is(original)); assertThat(cache.asMap().put(key, updated), is(original)); refresh.set(true); await().until(() -> context.consumedNotifications().size(), is(2)); List<Integer> removed = context.consumedNotifications().stream() .map(RemovalNotification::getValue).collect(toList()); assertThat(cache.getIfPresent(key), is(updated)); assertThat(removed, containsInAnyOrder(original, refreshed)); assertThat(cache, hasRemovalNotifications(context, 2, RemovalCause.REPLACED)); assertThat(context, both(hasLoadSuccessCount(1)).and(hasLoadFailureCount(0))); } /* ---------------- invalidate -------------- */ @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) public void invalidate(CacheContext context) { AtomicBoolean refresh = new AtomicBoolean(); Integer key = context.absentKey(); Integer original = 1; Integer refreshed = 2; LoadingCache<Integer, Integer> cache = context.build(k -> { await().untilTrue(refresh); return refreshed; }); cache.put(key, original); context.ticker().advance(2, TimeUnit.MINUTES); assertThat(cache.getIfPresent(key), is(original)); cache.invalidate(key); refresh.set(true); await().until(() -> cache.getIfPresent(key), is(refreshed)); await().until(() -> cache, hasRemovalNotifications(context, 1, RemovalCause.EXPLICIT)); await().until(() -> context, both(hasLoadSuccessCount(1)).and(hasLoadFailureCount(0))); } /* ---------------- Policy -------------- */ @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) public void getExpiresAfter(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { assertThat(refreshAfterWrite.getExpiresAfter(TimeUnit.MINUTES), is(1L)); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) public void setExpiresAfter(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { refreshAfterWrite.setExpiresAfter(2, TimeUnit.MINUTES); assertThat(refreshAfterWrite.getExpiresAfter(TimeUnit.MINUTES), is(2L)); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void ageOf(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { assertThat(refreshAfterWrite.ageOf(context.firstKey(), TimeUnit.SECONDS).getAsLong(), is(0L)); context.ticker().advance(30, TimeUnit.SECONDS); assertThat(refreshAfterWrite.ageOf(context.firstKey(), TimeUnit.SECONDS).getAsLong(), is(30L)); context.ticker().advance(45, TimeUnit.SECONDS); assertThat(refreshAfterWrite.ageOf(context.firstKey(), TimeUnit.SECONDS).isPresent(), is(false)); } /* ---------------- Policy: oldest -------------- */ @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void oldest_unmodifiable(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { refreshAfterWrite.oldest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) public void oldest_negative(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { refreshAfterWrite.oldest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) public void oldest_zero(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { assertThat(refreshAfterWrite.oldest(0), is(emptyMap())); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE) public void oldest_partial(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { int count = (int) context.initialSize() / 2; assertThat(refreshAfterWrite.oldest(count).size(), is(count)); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE, advanceOnPopulation = Advance.ONE_MINUTE, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void oldest_order(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { Map<Integer, Integer> oldest = refreshAfterWrite.oldest(Integer.MAX_VALUE); assertThat(oldest.keySet(), contains(context.original().keySet().toArray(new Integer[0]))); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) public void oldest_snapshot(Cache<Integer, Integer> cache, CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { Map<Integer, Integer> oldest = refreshAfterWrite.oldest(Integer.MAX_VALUE); cache.invalidateAll(); assertThat(oldest, is(equalTo(context.original()))); } /* ---------------- Policy: youngest -------------- */ @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void youngest_unmodifiable(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { refreshAfterWrite.youngest(Integer.MAX_VALUE).clear();; } @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) public void youngest_negative(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { refreshAfterWrite.youngest(-1); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) public void youngest_zero(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { assertThat(refreshAfterWrite.youngest(0), is(emptyMap())); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE) public void youngest_partial(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { int count = (int) context.initialSize() / 2; assertThat(refreshAfterWrite.youngest(count).size(), is(count)); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, refreshAfterWrite = Expire.ONE_MINUTE, advanceOnPopulation = Advance.ONE_MINUTE, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void youngest_order(CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { Map<Integer, Integer> youngest = refreshAfterWrite.youngest(Integer.MAX_VALUE); Set<Integer> keys = new LinkedHashSet<>(ImmutableList.copyOf(youngest.keySet()).reverse()); assertThat(keys, contains(context.original().keySet().toArray(new Integer[0]))); } @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, refreshAfterWrite = Expire.ONE_MINUTE) public void youngest_snapshot(Cache<Integer, Integer> cache, CacheContext context, @RefreshAfterWrite Expiration<Integer, Integer> refreshAfterWrite) { Map<Integer, Integer> youngest = refreshAfterWrite.youngest(Integer.MAX_VALUE); cache.invalidateAll(); assertThat(youngest, is(equalTo(context.original()))); } }