/*
* 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);
}
}