/* * Copyright 2017 Yonik Seeley. 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.issues; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.testng.annotations.Test; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalListener; import com.github.benmanes.caffeine.testing.ConcurrentTestHarness; /** * SOLR-10141: Removal listener notified with stale value * <p> * When an entry is chosen for eviction and concurrently updated, the value notified should be the * updated one if the <tt>put</tt> was successful. * * @author yseeley@gmail.com (Yonik Seeley) * @author ben.manes@gmail.com (Ben Manes) */ @Test(groups = "isolated") public final class Solr10141Test { static final int blocksInTest = 400; static final int maxEntries = blocksInTest / 2; static final int nThreads = 64; static final int nReads = 10000000; static final int readsPerThread = nReads / nThreads; // odds (1 in N) of the next block operation being on the same block as the previous operation... // helps flush concurrency issues static final int readLastBlockOdds = 10; // sometimes insert a new entry for the key even if one was found static final boolean updateAnyway = true; final Random rnd = new Random(); @Test public void eviction() throws Exception { AtomicLong hits = new AtomicLong(); AtomicLong inserts = new AtomicLong(); AtomicLong removals = new AtomicLong(); RemovalListener<Long, Val> listener = (k, v, removalCause) -> { assertThat(v.key, is(k)); if (!v.live.compareAndSet(true, false)) { throw new RuntimeException(String.format( "listener called more than once! k=%s, v=%s, removalCause=%s", k, v, removalCause)); } removals.incrementAndGet(); }; Cache<Long, Val> cache = Caffeine.newBuilder() .removalListener(listener) .maximumSize(maxEntries) .build(); AtomicLong lastBlock = new AtomicLong(); AtomicBoolean failed = new AtomicBoolean(); AtomicLong maxObservedSize = new AtomicLong(); ConcurrentTestHarness.timeTasks(nThreads, new Runnable() { @Override public void run() { try { Random r = new Random(rnd.nextLong()); for (int i = 0; i < readsPerThread; i++) { test(r); } } catch (Throwable e) { failed.set(true); e.printStackTrace(); } } void test(Random r) { long block = r.nextInt(blocksInTest); if (readLastBlockOdds > 0 && r.nextInt(readLastBlockOdds) == 0) { // some percent of the time, try to read the last block another block = lastBlock.get(); } // thread was just reading/writing lastBlock.set(block); Long k = block; Val v = cache.getIfPresent(k); if (v != null) { hits.incrementAndGet(); assertThat(k, is(v.key)); } if ((v == null) || (updateAnyway && r.nextBoolean())) { v = new Val(); v.key = k; cache.put(k, v); inserts.incrementAndGet(); } long sz = cache.estimatedSize(); if (sz > maxObservedSize.get()) { // race condition here, but an estimate is OK maxObservedSize.set(sz); } } }); await().until(() -> inserts.get() - removals.get() == cache.estimatedSize()); System.out.printf("Done!%n" + "entries=%,d inserts=%,d removals=%,d hits=%,d maxEntries=%,d maxObservedSize=%,d%n", cache.estimatedSize(), inserts.get(), removals.get(), hits.get(), maxEntries, maxObservedSize.get()); assertThat(failed.get(), is(false)); } @Test public void clear() throws Exception { AtomicLong inserts = new AtomicLong(); AtomicLong removals = new AtomicLong(); AtomicBoolean failed = new AtomicBoolean(); RemovalListener<Long, Val> listener = (k, v, removalCause) -> { assertThat(v.key, is(k)); if (!v.live.compareAndSet(true, false)) { throw new RuntimeException(String.format( "listener called more than once! k=%s, v=%s, removalCause=%s", k, v, removalCause)); } removals.incrementAndGet(); }; Cache<Long, Val> cache = Caffeine.newBuilder() .maximumSize(Integer.MAX_VALUE) .removalListener(listener) .build(); ConcurrentTestHarness.timeTasks(nThreads, new Runnable() { @Override public void run() { try { Random r = new Random(rnd.nextLong()); for (int i = 0; i < readsPerThread; i++) { test(r); } } catch (Throwable e) { failed.set(true); e.printStackTrace(); } } void test(Random r) { Long k = (long) r.nextInt(blocksInTest); Val v = cache.getIfPresent(k); if (v != null) { assertThat(k, is(v.key)); } if ((v == null) || (updateAnyway && r.nextBoolean())) { v = new Val(); v.key = k; cache.put(k, v); inserts.incrementAndGet(); } if (r.nextInt(10) == 0) { cache.asMap().clear(); } } }); cache.asMap().clear(); await().until(() -> inserts.get() == removals.get()); assertThat(failed.get(), is(false)); } static class Val { long key; AtomicBoolean live = new AtomicBoolean(true); } }