package org.infinispan.eviction.impl;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertNotNull;
import static org.testng.AssertJUnit.assertNull;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
import java.io.Serializable;
import java.util.Arrays;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.read.GetCacheEntryCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commons.util.Util;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.container.DataContainer;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.context.InvocationContext;
import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.PassivationManager;
import org.infinispan.interceptors.AsyncInterceptor;
import org.infinispan.interceptors.AsyncInterceptorChain;
import org.infinispan.interceptors.base.BaseCustomInterceptor;
import org.infinispan.interceptors.impl.EntryWrappingInterceptor;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.marshall.core.ExternalPojo;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.persistence.dummy.DummyInMemoryStoreConfigurationBuilder;
import org.infinispan.persistence.spi.CacheLoader;
import org.infinispan.persistence.spi.PersistenceException;
import org.infinispan.test.SingleCacheManagerTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.annotations.Test;
/**
* Tests size-based eviction with concurrent read and/or write operation. In this test, we have no passivation.
*
* @author Pedro Ruivo
* @since 6.0
*/
@Test(groups = "functional", testName = "eviction.EvictionWithConcurrentOperationsTest")
public class EvictionWithConcurrentOperationsTest extends SingleCacheManagerTest {
protected final AtomicInteger storeNamePrefix = new AtomicInteger(0);
public final String storeName = getClass().getSimpleName();
public EvictionWithConcurrentOperationsTest() {
cleanup = CleanupPhase.AFTER_METHOD;
}
/**
* ISPN-3048: this is a simple scenario. a put triggers the eviction while another thread tries to read. the read
* occurs before the eviction listener is notified (and passivated if enabled). the key still exists in the map
*/
public void testScenario1() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Object key2 = new SameHashCodeKey("key2");
final Latch latch = new Latch();
final ControlledPassivationManager controlledPassivationManager = replacePassivationManager(latch);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
latch.enable();
Future<Object> put;
try {
put = fork(() -> cache.put(key2, "v2"));
latch.waitToBlock(30, TimeUnit.SECONDS);
//the eviction was trigger and it blocked before passivation
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", cache.get(key1));
} finally {
//let the eviction continue and wait for put
latch.disable();
}
put.get(30, TimeUnit.SECONDS);
assertInMemory(key2, "v2");
assertNotInMemory(key1, "v1");
}
/**
* ISPN-3048: this is a simple scenario. a put triggers the eviction while another thread tries to read. the read
* occurs after the eviction listener is notified (if passivation is enabled, it is written to disk). the key still
* exists in the map
*/
public void testScenario2() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Object key2 = new SameHashCodeKey("key2");
final Latch latch = new Latch();
final ControlledPassivationManager controlledPassivationManager = replacePassivationManager(latch);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
latch.enable();
Future<Object> put;
try {
put = fork(() -> cache.put(key2, "v2"));
latch.waitToBlock(30, TimeUnit.SECONDS);
//the eviction was trigger and it blocked before passivation
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", cache.get(key1));
} finally {
//let the eviction continue and wait for put
latch.disable();
}
put.get(30, TimeUnit.SECONDS);
assertInMemory(key2, "v2");
assertNotInMemory(key1, "v1");
}
/**
* ISPN-3048: a put triggers the eviction while another thread tries to read. the read occurs after the eviction
* listener is notified (if passivation is enabled, it is written to disk) and after the entry is removed from the
* map. however, it should be able to load it from persistence.
*/
public void testScenario3() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Object key2 = new SameHashCodeKey("key2");
final Latch latch = new Latch();
final SyncEvictionListener evictionListener = new SyncEvictionListener() {
@CacheEntriesEvicted
@Override
public void evicted(CacheEntriesEvictedEvent event) {
if (event.getEntries().containsKey(key1)) {
latch.blockIfNeeded();
}
}
};
cache.addListener(evictionListener);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
latch.enable();
Future<Object> put;
try {
put = fork(() -> cache.put(key2, "v2"));
latch.waitToBlock(30, TimeUnit.SECONDS);
} finally {
//let the eviction continue and wait for put
latch.disable();
}
put.get(30, TimeUnit.SECONDS);
//the eviction was trigger and the key is no longer in the map
// This should be after the async put is known to finish. It is undefined which would
// win in the case of an entry being activated while it is also being passivated
// This way it is clear which should be there
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", cache.get(key1));
assertInMemory(key1, "v1");
assertNotInMemory(key2, "v2");
}
/**
* ISPN-3048: a put triggers the eviction while another thread tries to read. the read occurs after the eviction
* listener is notified (if passivation is enabled, it is written to disk) and after the entry is removed from the
* map. however, a concurrent read happens at the same time before the first has time to load it from persistence.
*/
public void testScenario4() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Object key2 = new SameHashCodeKey("key2");
final Latch readLatch = new Latch();
final Latch writeLatch = new Latch();
final AtomicBoolean firstGet = new AtomicBoolean(false);
final AfterEntryWrappingInterceptor afterEntryWrappingInterceptor = new AfterEntryWrappingInterceptor()
.injectThis(cache);
afterEntryWrappingInterceptor.beforeGet = () -> {
if (firstGet.compareAndSet(false, true)) {
readLatch.blockIfNeeded();
}
};
final SyncEvictionListener evictionListener = new SyncEvictionListener() {
@CacheEntriesEvicted
@Override
public void evicted(CacheEntriesEvictedEvent event) {
if (event.getEntries().containsKey(key1)) {
writeLatch.blockIfNeeded();
}
}
};
cache.addListener(evictionListener);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
readLatch.enable();
Future<Object> put = fork(() -> cache.put(key2, "v2"));
writeLatch.waitToBlock(30, TimeUnit.SECONDS);
//the eviction was trigger and the key is no longer in the map
Future<Object> get = fork(() -> cache.get(key1));
readLatch.waitToBlock(30, TimeUnit.SECONDS);
//the first read is blocked. it has check the data container and it didn't found any value
//this second get should not block anywhere and it should fetch the value from persistence
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", cache.get(key1));
//let the eviction continue and wait for put
writeLatch.disable();
put.get(30, TimeUnit.SECONDS);
//let the second get continue
readLatch.disable();
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", get.get());
assertInMemory(key1, "v1");
assertNotInMemory(key2, "v2");
}
/**
* ISPN-3048: a put triggers the eviction while another thread tries to read. the read occurs after the eviction
* listener is notified (if passivation is enabled, it is written to disk) and after the entry is removed from the
* map. however, a concurrent put happens at the same time before the get has time to load it from persistence.
*/
public void testScenario5() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Object key2 = new SameHashCodeKey("key2");
final Latch readLatch = new Latch();
final Latch writeLatch = new Latch();
final AfterEntryWrappingInterceptor afterEntryWrappingInterceptor = new AfterEntryWrappingInterceptor()
.injectThis(cache);
afterEntryWrappingInterceptor.beforeGet = () -> readLatch.blockIfNeeded();
final SyncEvictionListener evictionListener = new SyncEvictionListener() {
@CacheEntriesEvicted
@Override
public void evicted(CacheEntriesEvictedEvent event) {
if (event.getEntries().containsKey(key1)) {
writeLatch.blockIfNeeded();
}
}
};
cache.addListener(evictionListener);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
readLatch.enable();
Future<Object> get;
try {
Future<Object> put = fork(() -> cache.put(key2, "v2"));
writeLatch.waitToBlock(30, TimeUnit.SECONDS);
//the eviction was trigger and the key is no longer in the map
get = fork(() -> cache.get(key1));
readLatch.waitToBlock(30, TimeUnit.SECONDS);
//let the eviction continue
writeLatch.disable();
//the first read is blocked. it has check the data container and it didn't found any value
//this second get should not block anywhere and it should fetch the value from persistence
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", cache.put(key1, "v3"));
put.get();
} finally {
//let the get continue
readLatch.disable();
}
assertEquals("Wrong value for key " + key1 + " in get operation.", "v3", get.get(30, TimeUnit.SECONDS));
assertInMemory(key1, "v3");
assertNotInMemory(key2, "v2");
}
/**
* ISPN-3048: a put triggers the eviction while another thread tries to read. the read occurs after the eviction
* listener is notified (if passivation is enabled, it is written to disk) and after the entry is removed from the
* map. however, a concurrent put happens at the same time before the get has time to load it from persistence. The
* get will occur after the put writes to persistence and before writes to data container
*/
public void testScenario6() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Object key2 = new SameHashCodeKey("key2");
final Latch readLatch = new Latch();
final Latch writeLatch = new Latch();
final Latch writeLatch2 = new Latch();
final AtomicBoolean firstWriter = new AtomicBoolean(false);
final AfterEntryWrappingInterceptor afterEntryWrappingInterceptor = new AfterEntryWrappingInterceptor()
.injectThis(cache);
afterEntryWrappingInterceptor.beforeGet = () -> readLatch.blockIfNeeded();
afterEntryWrappingInterceptor.afterPut = () -> {
if (!firstWriter.compareAndSet(false, true)) {
writeLatch2.blockIfNeeded();
}
};
final SyncEvictionListener evictionListener = new SyncEvictionListener() {
@CacheEntriesEvicted
@Override
public void evicted(CacheEntriesEvictedEvent event) {
if (event.getEntries().containsKey(key1)) {
writeLatch.blockIfNeeded();
}
}
};
cache.addListener(evictionListener);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
readLatch.enable();
Future<Object> get;
Future<Object> put2;
try {
Future<Object> put = fork(() -> cache.put(key2, "v2"));
writeLatch.waitToBlock(30, TimeUnit.SECONDS);
//the eviction was trigger and the key is no longer in the map
get = fork(() -> cache.get(key1));
readLatch.waitToBlock(30, TimeUnit.SECONDS);
//let the eviction continue
writeLatch.disable();
put2 = cache.putAsync(key1, "v3");
put.get(30, TimeUnit.SECONDS);
//wait until the 2nd put writes to persistence
writeLatch2.waitToBlock(30, TimeUnit.SECONDS);
} finally {
//let the get continue
readLatch.disable();
}
assertPossibleValues(key1, get.get(30, TimeUnit.SECONDS), "v1", "v3");
writeLatch2.disable();
assertEquals("Wrong value for key " + key1 + " in put operation.", "v1", put2.get(30, TimeUnit.SECONDS));
assertInMemory(key1, "v3");
assertNotInMemory(key2, "v2");
}
/**
* ISPN-3854: an entry is evicted. a get operation is performed and loads the entry from the persistence. However,
* before continue the processing, a put succeeds and updates the key. Check in the end if the key is correctly
* stored or not.
*/
public void testScenario7() throws Exception {
final Object key1 = new SameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Latch readLatch = new Latch();
final AfterActivationOrCacheLoader commandController = new AfterActivationOrCacheLoader()
.injectThis(cache);
commandController.afterGet = () -> readLatch.blockIfNeeded();
cache.evict(key1);
assertNotInMemory(key1, "v1");
//perform the get. it will load the entry from cache loader.
readLatch.enable();
Future<Object> get;
try {
get = fork(() -> cache.get(key1));
readLatch.waitToBlock(30, TimeUnit.SECONDS);
//now, we perform a put.
assertEquals("Wrong value for key " + key1 + " in put operation.", "v1", cache.put(key1, "v2"));
} finally {
//we let the get go...
readLatch.disable();
}
assertPossibleValues(key1, get.get(30, TimeUnit.SECONDS), "v1");
assertInMemory(key1, "v2");
}
@SuppressWarnings("unchecked")
protected void initializeKeyAndCheckData(Object key, Object value) {
assertTrue("A cache store should be configured!", cache.getCacheConfiguration().persistence().usingStores());
cache.put(key, value);
DataContainer container = cache.getAdvancedCache().getDataContainer();
InternalCacheEntry entry = container.get(key);
CacheLoader<Object, Object> loader = TestingUtil.getFirstLoader(cache);
assertNotNull("Key " + key + " does not exist in data container.", entry);
assertEquals("Wrong value for key " + key + " in data container.", value, entry.getValue());
MarshalledEntry<Object, Object> entryLoaded = loader.load(key);
assertNotNull("Key " + key + " does not exist in cache loader.", entryLoaded);
assertEquals("Wrong value for key " + key + " in cache loader.", value, entryLoaded.getValue());
}
@SuppressWarnings("unchecked")
protected void assertInMemory(Object key, Object value) {
DataContainer container = cache.getAdvancedCache().getDataContainer();
InternalCacheEntry entry = container.get(key);
CacheLoader<Object, Object> loader = TestingUtil.getFirstLoader(cache);
assertNotNull("Key " + key + " does not exist in data container", entry);
assertEquals("Wrong value for key " + key + " in data container", value, entry.getValue());
MarshalledEntry<Object, Object> entryLoaded = loader.load(key);
assertNotNull("Key " + key + " does not exist in cache loader", entryLoaded);
assertEquals("Wrong value for key " + key + " in cache loader", value, entryLoaded.getValue());
}
@SuppressWarnings("unchecked")
protected void assertNotInMemory(Object key, Object value) {
DataContainer container = cache.getAdvancedCache().getDataContainer();
InternalCacheEntry entry = container.get(key);
CacheLoader<Object, Object> loader = TestingUtil.getFirstLoader(cache);
assertNull("Key " + key + " exists in data container", entry);
MarshalledEntry<Object, Object> entryLoaded = loader.load(key);
assertNotNull("Key " + key + " does not exist in cache loader", entryLoaded);
assertEquals("Wrong value for key " + key + " in cache loader", value, entryLoaded.getValue());
}
@Override
protected EmbeddedCacheManager createCacheManager() throws Exception {
ConfigurationBuilder builder = getDefaultStandaloneCacheConfig(false);
configurePersistence(builder);
configureEviction(builder);
return TestCacheManagerFactory.createCacheManager(builder);
}
protected void configureEviction(ConfigurationBuilder builder) {
builder.eviction()
.maxEntries(1)
.strategy(EvictionStrategy.LRU);
}
protected void configurePersistence(ConfigurationBuilder builder) {
builder.persistence().passivation(false).addStore(DummyInMemoryStoreConfigurationBuilder.class)
.storeName(storeName + storeNamePrefix.getAndIncrement());
}
protected final ControlledPassivationManager replacePassivationManager(final Latch latch) {
PassivationManager current = TestingUtil.extractComponent(cache, PassivationManager.class);
ControlledPassivationManager controlledPassivationManager = new ControlledPassivationManager(current);
controlledPassivationManager.beforePassivate = new Runnable() {
@Override
public void run() {
latch.blockIfNeeded();
}
};
TestingUtil.replaceComponent(cache, PassivationManager.class, controlledPassivationManager, true);
return controlledPassivationManager;
}
protected void assertPossibleValues(Object key, Object value, Object... expectedValues) {
for (Object expectedValue : expectedValues) {
if (value == null ? expectedValue == null : value.equals(expectedValue)) {
return;
}
}
fail("Wrong value for key " + key + ". value=" + String.valueOf(value) + ", expectedValues=" +
Arrays.toString(expectedValues));
}
public static class SameHashCodeKey implements Serializable, ExternalPojo {
private final String name;
public SameHashCodeKey(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SameHashCodeKey that = (SameHashCodeKey) o;
return name.equals(that.name);
}
@Override
public int hashCode() {
return 0; //same hash code to force the keys to be in the same segment.
}
@Override
public String toString() {
return name;
}
}
protected class ControlledPassivationManager implements PassivationManager {
protected final PassivationManager delegate;
protected volatile Runnable beforePassivate;
protected volatile Runnable afterPassivate;
private ControlledPassivationManager(PassivationManager delegate) {
this.delegate = delegate;
}
@Override
public boolean isEnabled() {
return delegate.isEnabled();
}
@Override
public void passivate(InternalCacheEntry entry) {
final Runnable before = beforePassivate;
if (before != null) {
before.run();
}
delegate.passivate(entry);
final Runnable after = afterPassivate;
if (after != null) {
after.run();
}
}
@Override
public void passivateAll() throws PersistenceException {
delegate.passivateAll();
}
@Override
public void skipPassivationOnStop(boolean skip) {
delegate.skipPassivationOnStop(skip);
}
@Override
public long getPassivations() {
return delegate.getPassivations();
}
@Override
public boolean getStatisticsEnabled() {
return delegate.getStatisticsEnabled();
}
@Override
public void setStatisticsEnabled(boolean enabled) {
delegate.setStatisticsEnabled(enabled);
}
@Override
public void resetStatistics() {
delegate.resetStatistics();
}
}
@Listener(sync = true)
protected abstract class SyncEvictionListener {
@CacheEntriesEvicted
public abstract void evicted(CacheEntriesEvictedEvent event);
}
protected abstract class ControlledCommandInterceptor extends BaseCustomInterceptor {
volatile Runnable beforeGet;
volatile Runnable afterGet;
volatile Runnable beforePut;
volatile Runnable afterPut;
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
return handle(ctx, command, beforePut, afterPut);
}
@Override
public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable {
return handle(ctx, command, beforeGet, afterGet);
}
@Override
public Object visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command) throws Throwable {
return handle(ctx, command, beforeGet, afterGet);
}
protected final Object handle(InvocationContext ctx, VisitableCommand command, Runnable before, Runnable after)
throws Throwable {
if (before != null) {
before.run();
}
Object retVal = invokeNextInterceptor(ctx, command);
if (after != null) {
after.run();
}
return retVal;
}
}
protected class AfterEntryWrappingInterceptor extends ControlledCommandInterceptor {
public AfterEntryWrappingInterceptor injectThis(Cache<Object, Object> injectInCache) {
injectInCache.getAdvancedCache().getAsyncInterceptorChain().addInterceptorAfter(this, EntryWrappingInterceptor.class);
return this;
}
}
private class AfterActivationOrCacheLoader extends ControlledCommandInterceptor {
public AfterActivationOrCacheLoader injectThis(Cache<Object, Object> injectInCache) {
AsyncInterceptorChain chain =
TestingUtil.extractComponent(injectInCache, AsyncInterceptorChain.class);
AsyncInterceptor loaderInterceptor =
chain.findInterceptorExtending(org.infinispan.interceptors.impl.CacheLoaderInterceptor.class);
injectInCache.getAdvancedCache().getAsyncInterceptorChain().addInterceptorAfter(this, loaderInterceptor.getClass());
return this;
}
}
protected class Latch {
private boolean enabled = false;
private boolean blocked = false;
public final synchronized void enable() {
this.enabled = true;
}
public final synchronized void disable() {
this.enabled = false;
notifyAll();
}
public final synchronized void blockIfNeeded() {
blocked = true;
notifyAll();
while (enabled) {
try {
wait(TimeUnit.SECONDS.toMillis(10));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
public final synchronized void waitToBlock(long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException {
final long endTime = Util.currentMillisFromNanotime() + timeUnit.toMillis(timeout);
long waitingTime;
while (!blocked && (waitingTime = endTime - Util.currentMillisFromNanotime()) > 0) {
wait(waitingTime);
}
if (!blocked) {
throw new TimeoutException();
}
}
}
}