package org.infinispan.eviction.impl;
import static org.infinispan.test.TestingUtil.extractComponent;
import static org.testng.AssertJUnit.assertEquals;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import org.infinispan.Cache;
import org.infinispan.commands.write.EvictCommand;
import org.infinispan.configuration.cache.CacheMode;
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.distribution.DistributionManager;
import org.infinispan.factories.annotations.Stop;
import org.infinispan.filter.KeyFilter;
import org.infinispan.filter.KeyValueFilter;
import org.infinispan.interceptors.AsyncInterceptor;
import org.infinispan.interceptors.AsyncInterceptorChain;
import org.infinispan.interceptors.impl.CacheLoaderInterceptor;
import org.infinispan.interceptors.impl.CacheWriterInterceptor;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.marshall.core.ExternalPojo;
import org.infinispan.metadata.Metadata;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.persistence.dummy.DummyInMemoryStoreConfigurationBuilder;
import org.infinispan.remoting.transport.Address;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.AssertJUnit;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* Tests manual eviction with concurrent read and/or write operation. This test has passivation disabled and the
* eviction happens in the primary owner
*
* @author Pedro Ruivo
* @since 6.0
*/
@Test(groups = "functional", testName = "eviction.ManualEvictionWithSizeBasedAndConcurrentOperationsInPrimaryOwnerTest", singleThreaded = true)
public class ManualEvictionWithSizeBasedAndConcurrentOperationsInPrimaryOwnerTest extends EvictionWithConcurrentOperationsTest {
protected EmbeddedCacheManager otherCacheManager;
@AfterMethod(alwaysRun = true)
public void stopSecondCacheManager() {
if (otherCacheManager != null) {
otherCacheManager.getCache().stop();
otherCacheManager.stop();
otherCacheManager = null;
}
}
@BeforeMethod(alwaysRun = true)
public void startSecondCacheManager() throws Exception {
if (otherCacheManager == null) {
otherCacheManager = createCacheManager();
} else {
AssertJUnit.fail("Other cache manager should not be set!");
}
Cache otherCache = otherCacheManager.getCache();
TestingUtil.waitForNoRebalance(cache, otherCache);
}
@Override
public void testScenario1() throws Exception {
final Object key1 = createSameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final AfterPassivationOrCacheWriter controller = new AfterPassivationOrCacheWriter().injectThis(cache);
final Latch latch = new Latch();
controller.beforeEvict = () -> latch.blockIfNeeded();
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
latch.enable();
Future<Void> evict = evictWithFuture(key1);
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));
//let the eviction continue and wait for put
latch.disable();
evict.get(30, TimeUnit.SECONDS);
assertNotInMemory(key1, "v1");
}
@Override
public void testScenario2() throws Exception {
final Object key1 = createSameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Latch latch = new Latch();
replaceControlledDataContainer(latch);
//this will trigger the eviction of key1. key1 eviction will be blocked in the latch
latch.enable();
Future<Void> evict = evictWithFuture(key1);
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));
//let the eviction continue and wait for put
latch.disable();
evict.get(30, TimeUnit.SECONDS);
assertNotInMemory(key1, "v1");
}
@Override
public void testScenario3() throws Exception {
final Object key1 = createSameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
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<Void> evict = evictWithFuture(key1);
latch.waitToBlock(30, TimeUnit.SECONDS);
//the eviction was trigger and the key is no longer in the map
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", cache.get(key1));
//let the eviction continue and wait for put
latch.disable();
evict.get(30, TimeUnit.SECONDS);
assertInMemory(key1, "v1");
}
@Override
public void testScenario4() throws Exception {
final Object key1 = createSameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
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<Void> evict = evictWithFuture(key1);
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();
evict.get(30, TimeUnit.SECONDS);
//let the second get continue
readLatch.disable();
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", get.get(30, TimeUnit.SECONDS));
assertInMemory(key1, "v1");
}
@Override
public void testScenario5() throws Exception {
final Object key1 = createSameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
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<Void> evict = evictWithFuture(key1);
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);
//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 put operation.", "v1", cache.put(key1, "v3"));
evict.get(30, TimeUnit.SECONDS);
//let the get continue
readLatch.disable();
assertEquals("Wrong value for key " + key1 + " in get operation.", "v3", get.get(30, TimeUnit.SECONDS));
assertInMemory(key1, "v3");
}
@Override
public void testScenario6() throws Exception {
final Object key1 = createSameHashCodeKey("key1");
initializeKeyAndCheckData(key1, "v1");
final Latch readLatch = new Latch();
final Latch writeLatch = new Latch();
final Latch writeLatch2 = new Latch();
final AfterEntryWrappingInterceptor afterEntryWrappingInterceptor = new AfterEntryWrappingInterceptor()
.injectThis(cache);
afterEntryWrappingInterceptor.beforeGet = () -> readLatch.blockIfNeeded();
afterEntryWrappingInterceptor.afterPut = () -> 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<Void> evict = evictWithFuture(key1);
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);
//let the eviction continue
writeLatch.disable();
Future<Object> put2 = fork(() -> cache.put(key1, "v3"));
evict.get(30, TimeUnit.SECONDS);
//wait until the 2nd put writes to persistence
writeLatch2.waitToBlock(30, TimeUnit.SECONDS);
//let the get continue
readLatch.disable();
assertPossibleValues(key1, get.get(30, TimeUnit.SECONDS), "v1", "v3");
writeLatch2.disable();
assertEquals("Wrong value for key " + key1 + " in get operation.", "v1", put2.get(30, TimeUnit.SECONDS));
assertInMemory(key1, "v3");
}
@Override
protected void configurePersistence(ConfigurationBuilder builder) {
builder.persistence().passivation(false).addStore(DummyInMemoryStoreConfigurationBuilder.class)
.storeName(storeName + storeNamePrefix.getAndIncrement());
}
@Override
protected EmbeddedCacheManager createCacheManager() throws Exception {
ConfigurationBuilder builder = getDefaultStandaloneCacheConfig(false);
builder.clustering().cacheMode(CacheMode.DIST_SYNC)
.hash().numOwners(2).numSegments(2);
configurePersistence(builder);
configureEviction(builder);
return TestCacheManagerFactory.createClusteredCacheManager(builder);
}
protected Object createSameHashCodeKey(String name) {
final Address address = cache.getAdvancedCache().getRpcManager().getAddress();
DistributionManager distributionManager = cache.getAdvancedCache().getDistributionManager();
int hashCode = 0;
SameHashCodeKey key = new SameHashCodeKey(name, hashCode);
while (!distributionManager.getPrimaryLocation(key).equals(address)) {
hashCode++;
key = new SameHashCodeKey(name, hashCode);
}
return key;
}
protected final Future<Void> evictWithFuture(final Object key) {
return fork(() -> {
cache.evict(key);
return null;
});
}
private ControlledDataContainer replaceControlledDataContainer(final Latch latch) {
DataContainer current = TestingUtil.extractComponent(cache, DataContainer.class);
//noinspection unchecked
ControlledDataContainer controlledDataContainer = new ControlledDataContainer(current);
controlledDataContainer.beforeEvict = new Runnable() {
@Override
public void run() {
latch.blockIfNeeded();
}
};
TestingUtil.replaceComponent(cache, DataContainer.class, controlledDataContainer, true);
return controlledDataContainer;
}
public static class SameHashCodeKey implements Serializable, ExternalPojo {
private final String name;
private final int hashCode;
public SameHashCodeKey(String name, int hashCode) {
this.name = name;
this.hashCode = hashCode;
}
@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 hashCode;
}
@Override
public String toString() {
return name;
}
}
private class ControlledDataContainer<K, V> implements DataContainer<K, V> {
private final DataContainer<K, V> delegate;
private volatile Runnable beforeEvict;
private ControlledDataContainer(DataContainer<K, V> delegate) {
this.delegate = delegate;
}
@Override
public InternalCacheEntry<K, V> get(Object k) {
return delegate.get(k);
}
@Override
public InternalCacheEntry<K, V> peek(Object k) {
return delegate.peek(k);
}
@Override
public void put(K k, V v, Metadata metadata) {
delegate.put(k, v, metadata);
}
@Override
public boolean containsKey(Object k) {
return delegate.containsKey(k);
}
@Override
public InternalCacheEntry<K, V> remove(Object k) {
return delegate.remove(k);
}
@Override
public int size() {
return delegate.size();
}
@Override
public int sizeIncludingExpired() {
return delegate.sizeIncludingExpired();
}
@Override
@Stop(priority = 999)
public void clear() {
delegate.clear();
}
@Override
public Set<K> keySet() {
return delegate.keySet();
}
@Override
public Collection<V> values() {
return delegate.values();
}
@Override
public Set<InternalCacheEntry<K, V>> entrySet() {
return delegate.entrySet();
}
@Override
public void evict(K key) {
run(beforeEvict);
delegate.evict(key);
}
@Override
public InternalCacheEntry<K, V> compute(K key, ComputeAction<K, V> action) {
return delegate.compute(key, action);
}
@Override
public Iterator<InternalCacheEntry<K, V>> iterator() {
return delegate.iterator();
}
@Override
public Iterator<InternalCacheEntry<K, V>> iteratorIncludingExpired() {
return delegate.iteratorIncludingExpired();
}
@Override
public void executeTask(KeyFilter<? super K> filter, BiConsumer<? super K, InternalCacheEntry<K, V>> action)
throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public void executeTask(KeyValueFilter<? super K, ? super V> filter, BiConsumer<? super K, InternalCacheEntry<K, V>> action) throws InterruptedException {
throw new UnsupportedOperationException();
}
private void run(Runnable runnable) {
if (runnable == null) {
return;
}
runnable.run();
}
}
private class AfterPassivationOrCacheWriter extends ControlledCommandInterceptor {
volatile Runnable beforeEvict;
volatile Runnable afterEvict;
public AfterPassivationOrCacheWriter injectThis(Cache<Object, Object> injectInCache) {
AsyncInterceptorChain chain = extractComponent(injectInCache, AsyncInterceptorChain.class);
AsyncInterceptor interceptor = chain.findInterceptorExtending(CacheWriterInterceptor.class);
if (interceptor == null) {
interceptor = chain.findInterceptorExtending(CacheLoaderInterceptor.class);
}
if (interceptor == null) {
throw new IllegalStateException("Should not happen!");
}
chain.addInterceptorAfter(this, interceptor.getClass());
return this;
}
@Override
public Object visitEvictCommand(InvocationContext ctx, EvictCommand command) throws Throwable {
return handle(ctx, command, beforeEvict, afterEvict);
}
}
}