/*
* Copyright 2014 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.testing.IsEmptyMap.emptyMap;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import com.github.benmanes.caffeine.cache.Async.AsyncWeigher;
import com.github.benmanes.caffeine.cache.References.WeakKeyReference;
import com.github.benmanes.caffeine.cache.TimerWheel.Sentinel;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher;
import com.github.benmanes.caffeine.testing.Awaits;
import com.github.benmanes.caffeine.testing.DescriptionBuilder;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
/**
* A matcher that evaluates a {@link BoundedLocalCache} to determine if it is in a valid state.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class IsValidBoundedLocalCache<K, V>
extends TypeSafeDiagnosingMatcher<BoundedLocalCache<K, V>> {
DescriptionBuilder desc;
@Override
public void describeTo(Description description) {
description.appendText("valid bounded cache");
if (desc.getDescription() != description) {
description.appendText(desc.getDescription().toString());
}
}
@Override
protected boolean matchesSafely(BoundedLocalCache<K, V> cache, Description description) {
desc = new DescriptionBuilder(description);
drain(cache);
checkReadBuffer(cache);
checkCache(cache);
checkTimerWheel(cache);
checkEvictionDeque(cache);
if (!desc.matches()) {
throw new AssertionError(desc.getDescription().toString());
}
return true;
}
private void drain(BoundedLocalCache<K, V> cache) {
do {
cache.cleanUp();
} while (cache.buffersWrites() && cache.writeBuffer().size() > 0);
}
private void checkReadBuffer(BoundedLocalCache<K, V> cache) {
Buffer<?> buffer = cache.readBuffer;
Awaits.await().until(() -> {
cache.cleanUp();
return (buffer.size() == 0) && buffer.reads() == buffer.writes();
});
}
private void checkCache(BoundedLocalCache<K, V> cache) {
try {
if (cache.evictionLock.tryLock(5, TimeUnit.SECONDS)) {
cache.evictionLock.unlock();
} else {
desc.expected("Maintenance lock can be acquired");
}
} catch (InterruptedException e) {
desc.expected("Maintenance lock can be acquired: " + Throwables.getStackTraceAsString(e));
}
desc.expectThat("Inconsistent size", cache.data.size(), is(cache.size()));
if (cache.evicts()) {
cache.evictionLock.lock();
long weightedSize = cache.weightedSize();
cache.evictionLock.unlock();
desc.expectThat("overflow", cache.maximum(), is(greaterThanOrEqualTo(weightedSize)));
}
if (cache.isEmpty()) {
desc.expectThat("empty map", cache, emptyMap());
}
for (Node<K, V> node : cache.data.values()) {
checkNode(cache, node, desc);
}
}
private void checkTimerWheel(BoundedLocalCache<K, V> cache) {
if (!cache.expiresVariable()) {
return;
}
Set<Node<K, V>> seen = Sets.newIdentityHashSet();
for (int i = 0; i < cache.timerWheel().wheel.length; i++) {
for (int j = 0; j < cache.timerWheel().wheel[i].length; j++) {
Node<K, V> sentinel = cache.timerWheel().wheel[i][j];
desc.expectThat("Wrong sentinel prev",
sentinel.getPreviousInVariableOrder().getNextInVariableOrder(), sameInstance(sentinel));
desc.expectThat("Wrong sentinel next",
sentinel.getNextInVariableOrder().getPreviousInVariableOrder(), sameInstance(sentinel));
desc.expectThat("Sentinel must be first element", sentinel, instanceOf(Sentinel.class));
for (Node<K, V> node = sentinel.getNextInVariableOrder();
node != sentinel; node = node.getNextInVariableOrder()) {
Node<K, V> next = node.getNextInVariableOrder();
Node<K, V> prev = node.getPreviousInVariableOrder();
long duration = node.getVariableTime() - cache.timerWheel().nanos;
desc.expectThat("Expired", duration, greaterThan(0L));
desc.expectThat("Loop detected", seen.add(node), is(true));
desc.expectThat("Wrong prev", prev.getNextInVariableOrder(), is(sameInstance(node)));
desc.expectThat("Wrong next", next.getPreviousInVariableOrder(), is(sameInstance(node)));
}
}
}
desc.expectThat("Timers != Entries", seen, hasSize(cache.size()));
}
private void checkEvictionDeque(BoundedLocalCache<K, V> cache) {
if (cache.evicts()) {
ImmutableList<LinkedDeque<Node<K, V>>> deques = ImmutableList.of(
cache.accessOrderEdenDeque(),
cache.accessOrderProbationDeque(),
cache.accessOrderProtectedDeque());
checkLinks(cache, deques, desc);
checkDeque(cache.accessOrderEdenDeque(), desc);
checkDeque(cache.accessOrderProbationDeque(), desc);
} else if (cache.expiresAfterAccess()) {
checkLinks(cache, ImmutableList.of(cache.accessOrderEdenDeque()), desc);
checkDeque(cache.accessOrderEdenDeque(), desc);
}
if (cache.expiresAfterWrite()) {
checkLinks(cache, ImmutableList.of(cache.writeOrderDeque()), desc);
checkDeque(cache.writeOrderDeque(), desc);
}
}
private void checkDeque(LinkedDeque<Node<K, V>> deque, DescriptionBuilder desc) {
IsValidLinkedDeque.<Node<K, V>>validLinkedDeque().matchesSafely(deque, desc.getDescription());
}
private void checkLinks(BoundedLocalCache<K, V> cache,
ImmutableList<LinkedDeque<Node<K, V>>> deques, DescriptionBuilder desc) {
int size = 0;
long weightedSize = 0;
Set<Node<K, V>> seen = Sets.newIdentityHashSet();
for (LinkedDeque<Node<K, V>> deque : deques) {
size += deque.size();
weightedSize += scanLinks(cache, seen, deque, desc);
}
if (cache.size() != size) {
desc.expectThat(() -> "deque size " + deques, size, is(cache.size()));
}
Supplier<String> errorMsg = () -> String.format(
"Size != list length; pending=%s, additional: %s", cache.writeBuffer().size(),
Sets.difference(seen, ImmutableSet.copyOf(cache.data.values())));
desc.expectThat(errorMsg, cache.size(), is(seen.size()));
final long weighted = weightedSize;
if (cache.evicts()) {
Supplier<String> error = () -> String.format(
"WeightedSize != link weights [%d vs %d] {%d vs %d}",
cache.adjustedWeightedSize(), weighted, seen.size(), cache.size());
desc.expectThat("non-negative weight", weightedSize, is(greaterThanOrEqualTo(0L)));
desc.expectThat(error, cache.adjustedWeightedSize(), is(weightedSize));
}
}
private long scanLinks(BoundedLocalCache<K, V> cache, Set<Node<K, V>> seen,
LinkedDeque<Node<K, V>> deque, DescriptionBuilder desc) {
long weightedSize = 0;
Node<?, ?> prev = null;
for (Node<K, V> node : deque) {
Supplier<String> errorMsg = () -> String.format(
"Loop detected: %s, saw %s in %s", node, seen, cache);
desc.expectThat("wrong previous", deque.getPrevious(node), is(prev));
desc.expectThat(errorMsg, seen.add(node), is(true));
weightedSize += node.getWeight();
prev = node;
}
return weightedSize;
}
private void checkNode(BoundedLocalCache<K, V> cache, Node<K, V> node, DescriptionBuilder desc) {
Weigher<? super K, ? super V> weigher = cache.weigher;
V value = node.getValue();
K key = node.getKey();
desc.expectThat("weight", node.getWeight(), is(greaterThanOrEqualTo(0)));
boolean canCheckWeight = weigher == CacheWeigher.RANDOM;
if (weigher instanceof AsyncWeigher) {
canCheckWeight = ((AsyncWeigher<?, ?>) weigher).delegate == CacheWeigher.RANDOM;
}
if (canCheckWeight) {
desc.expectThat("weight", node.getWeight(), is(weigher.weigh(key, value)));
}
if (cache.collectKeys()) {
if ((key != null) && (value != null)) {
desc.expectThat("inconsistent", cache.containsKey(key), is(true));
}
desc.expectThat("Invalid reference type",
node.getKeyReference(), instanceOf(WeakKeyReference.class));
} else {
desc.expectThat("not null key", key, is(not(nullValue())));
}
desc.expectThat("found wrong node", cache.data.get(node.getKeyReference()), is(node));
if (!cache.collectValues()) {
desc.expectThat("not null value", value, is(not(nullValue())));
if ((key != null) && !cache.hasExpired(node, cache.expirationTicker().read())) {
desc.expectThat(() -> "Could not find key: " + key + ", value: " + value,
cache.containsValue(value), is(true));
}
}
if (cache.refreshAfterWrite()) {
desc.expectThat("infinite timestamp", node.getWriteTime(), is(not(Long.MAX_VALUE)));
}
if (value instanceof CompletableFuture<?>) {
CompletableFuture<?> future = (CompletableFuture<?>) value;
boolean success = future.isDone() && !future.isCompletedExceptionally();
desc.expectThat("future is done", success, is(true));
desc.expectThat("not null value", future.getNow(null), is(not(nullValue())));
}
}
public static <K, V> IsValidBoundedLocalCache<K, V> valid() {
return new IsValidBoundedLocalCache<>();
}
}