package org.infinispan.notifications.cachelistener.cluster; import static org.infinispan.test.Mocks.invokeAndReturnMock; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.withSettings; import static org.testng.AssertJUnit.assertEquals; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; import org.infinispan.commands.FlagAffectedCommand; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.context.InvocationContext; import org.infinispan.filter.KeyValueFilter; import org.infinispan.manager.CacheContainer; import org.infinispan.marshall.core.ExternalPojo; import org.infinispan.metadata.Metadata; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.CacheNotifier; import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; import org.infinispan.notifications.cachelistener.annotation.CacheEntryExpired; import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified; import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryExpiredEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent; import org.infinispan.notifications.cachelistener.event.Event; import org.infinispan.notifications.cachelistener.filter.CacheEventConverter; import org.infinispan.notifications.cachelistener.filter.CacheEventFilter; import org.infinispan.notifications.cachelistener.filter.CacheEventFilterConverter; import org.infinispan.notifications.cachelistener.filter.EventType; import org.infinispan.notifications.cachemanagerlistener.CacheManagerNotifier; import org.infinispan.remoting.transport.Address; import org.infinispan.test.MultipleCacheManagersTest; import org.infinispan.test.TestingUtil; import org.infinispan.test.fwk.CheckPoint; import org.infinispan.transaction.TransactionMode; import org.infinispan.util.ControlledTimeService; import org.infinispan.util.TimeService; import org.infinispan.util.concurrent.IsolationLevel; import org.mockito.AdditionalAnswers; import org.mockito.stubbing.Answer; /** * Base class to be used for cluster listener tests for both tx and nontx caches * * @author wburns * @since 7.0 */ public abstract class AbstractClusterListenerUtilTest extends MultipleCacheManagersTest { protected final static String CACHE_NAME = "cluster-listener"; protected final static String FIRST_VALUE = "first-value"; protected final static String SECOND_VALUE = "second-value"; protected ConfigurationBuilder builderUsed; protected final boolean tx; protected final CacheMode cacheMode; protected ControlledTimeService ts0; protected ControlledTimeService ts1; protected ControlledTimeService ts2; protected AbstractClusterListenerUtilTest(boolean tx, CacheMode cacheMode) { // Have to have cleanup after each method since listeners need to be cleaned up cleanup = CleanupPhase.AFTER_METHOD; this.tx = tx; this.cacheMode = cacheMode; } @Override protected void createCacheManagers() throws Throwable { builderUsed = new ConfigurationBuilder(); builderUsed.clustering().cacheMode(cacheMode); if (tx) { builderUsed.transaction().transactionMode(TransactionMode.TRANSACTIONAL); builderUsed.locking().isolationLevel(IsolationLevel.READ_COMMITTED); } // Due to ISPN-5507 we can end up waiting 30 seconds for the test to complete with expiration tests builderUsed.transaction().cacheStopTimeout(100, TimeUnit.MILLISECONDS); builderUsed.expiration().disableReaper(); createClusteredCaches(3, CACHE_NAME, builderUsed); injectTimeServices(); } protected void injectTimeServices() { long now = System.currentTimeMillis(); ts0 = new ControlledTimeService(now); TestingUtil.replaceComponent(manager(0), TimeService.class, ts0, true); ts1 = new ControlledTimeService(now); TestingUtil.replaceComponent(manager(1), TimeService.class, ts1, true); ts2 = new ControlledTimeService(now); TestingUtil.replaceComponent(manager(2), TimeService.class, ts2, true); } @Listener(clustered = true) protected class ClusterListener { List<CacheEntryEvent> events = Collections.synchronizedList(new ArrayList<CacheEntryEvent>()); @CacheEntryCreated public void onCreatedEvent(CacheEntryCreatedEvent event) { onCacheEvent(event); } @CacheEntryModified public void onModifiedEvent(CacheEntryModifiedEvent event) { onCacheEvent(event); } @CacheEntryRemoved public void onRemoveEvent(CacheEntryRemovedEvent event) { onCacheEvent(event); } @CacheEntryExpired public void onExpireEvent(CacheEntryExpiredEvent event) { onCacheEvent(event); } void onCacheEvent(CacheEntryEvent event) { log.debugf("Adding new cluster event %s", event); events.add(event); } protected boolean hasIncludeState() { return false; } } @Listener(clustered = true, includeCurrentState = true) protected class ClusterListenerWithIncludeCurrentState extends ClusterListener { protected boolean hasIncludeState() { return true; } } protected static class LifespanFilter<K, V> implements KeyValueFilter<K, V>, Serializable, ExternalPojo { public LifespanFilter(long lifespan) { this.lifespan = lifespan; } private final long lifespan; @Override public boolean accept(K key, V value, Metadata metadata) { if (metadata == null) { return false; } // Only accept entities with a lifespan longer than ours return metadata.lifespan() > lifespan; } } protected static class NewLifespanLargerFilter<K, V> implements CacheEventFilter<K, V>, Serializable, ExternalPojo { @Override public boolean accept(K key, V oldValue, Metadata oldMetadata, V newValue, Metadata newMetadata, EventType eventType) { // If neither metadata is provided dont' raise the event (this will preclude all creations and removals) if (oldMetadata == null || newMetadata == null) { return true; } // Only accept entities with a lifespan that is now longer return newMetadata.lifespan() > oldMetadata.lifespan(); } } protected static class LifespanConverter implements CacheEventConverter<Object, String, Object>, Serializable, ExternalPojo { public LifespanConverter(boolean returnOriginalValueOrNull, long lifespanThreshold) { this.returnOriginalValueOrNull = returnOriginalValueOrNull; this.lifespanThreshold = lifespanThreshold; } private final boolean returnOriginalValueOrNull; private final long lifespanThreshold; @Override public Object convert(Object key, String oldValue, Metadata oldMetadata, String newValue, Metadata newMetadata, EventType eventType) { if (newMetadata != null) { long metaLifespan = newMetadata.lifespan(); if (metaLifespan > lifespanThreshold) { return metaLifespan; } } if (returnOriginalValueOrNull) { return newValue; } return null; } } protected static class StringTruncator implements CacheEventConverter<Object, String, String>, Serializable, ExternalPojo { private final int beginning; private final int length; public StringTruncator(int beginning, int length) { this.beginning = beginning; this.length = length; } @Override public String convert(Object key, String oldValue, Metadata oldMetadata, String newValue, Metadata newMetadata, EventType eventType) { if (newValue != null && newValue.length() > beginning + length) { return newValue.substring(beginning, beginning + length); } else { return newValue; } } } protected static class StringAppender implements CacheEventConverter<Object, String, String>, Serializable, ExternalPojo { @Override public String convert(Object key, String oldValue, Metadata oldMetadata, String newValue, Metadata newMetadata, EventType eventType) { return oldValue + (oldMetadata != null ? oldMetadata.lifespan() : "null") + newValue + (newMetadata != null ? newMetadata.lifespan() : "null"); } } protected static class FilterConverter implements CacheEventFilterConverter<Object, Object, Object>, Serializable, ExternalPojo { private final boolean throwExceptionOnNonFilterAndConverterMethods; private final Object convertedValue; public FilterConverter(boolean throwExceptionOnNonFilterAndConverterMethods, Object convertedValue) { this.throwExceptionOnNonFilterAndConverterMethods = throwExceptionOnNonFilterAndConverterMethods; this.convertedValue = convertedValue; } @Override public Object filterAndConvert(Object key, Object oldValue, Metadata oldMetadata, Object newValue, Metadata newMetadata, EventType eventType) { return convertedValue; } @Override public Object convert(Object key, Object oldValue, Metadata oldMetadata, Object newValue, Metadata newMetadata, EventType eventType) { if (throwExceptionOnNonFilterAndConverterMethods) { throw new AssertionError("Method should not have been invoked!"); } return filterAndConvert(key, oldValue, oldMetadata, oldValue, oldMetadata, eventType); } @Override public boolean accept(Object key, Object oldValue, Metadata oldMetadata, Object newValue, Metadata newMetadata, EventType eventType) { if (throwExceptionOnNonFilterAndConverterMethods) { throw new AssertionError("Method should not have been invoked!"); } return filterAndConvert(key, oldValue, oldMetadata, oldValue, oldMetadata, eventType) != null; } } protected void verifySimpleInsertion(Cache<Object, String> cache, Object key, String value, Long lifespan, ClusterListener listener, Object expectedValue) { if (lifespan != null) { cache.put(key, value, lifespan, TimeUnit.MILLISECONDS); } else { cache.put(key, value); } verifySimpleInsertionEvents(listener, key, expectedValue); } protected void verifySimpleModification(Cache<Object, String> cache, Object key, String value, Long lifespan, ClusterListener listener, Object expectedValue) { if (lifespan != null) { cache.put(key, value, lifespan, TimeUnit.MILLISECONDS); } else { cache.put(key, value); } verifySimpleModificationEvents(listener, key, expectedValue); } protected void verifySimpleInsertionEvents(ClusterListener listener, Object key, Object expectedValue) { assertEquals(1, listener.events.size()); CacheEntryEvent event = listener.events.get(0); assertEquals(Event.Type.CACHE_ENTRY_CREATED, event.getType()); assertEquals(key, event.getKey()); assertEquals(expectedValue, event.getValue()); } protected void verifySimpleModificationEvents(ClusterListener listener, Object key, Object expectedValue) { assertEquals(listener.hasIncludeState() ? 2 : 1, listener.events.size()); CacheEntryEvent event = listener.events.get(listener.hasIncludeState() ? 1 :0); assertEquals(Event.Type.CACHE_ENTRY_MODIFIED, event.getType()); assertEquals(key, event.getKey()); assertEquals(expectedValue, event.getValue()); } protected void verifySimpleExpirationEvents(ClusterListener listener, int expectedNumEvents, Object key, Object expectedValue) { eventually(() -> listener.events.size() >= expectedNumEvents); CacheEntryEvent event = listener.events.get(expectedNumEvents - 1); //the index starts from 0 assertEquals(Event.Type.CACHE_ENTRY_EXPIRED, event.getType()); assertEquals(key, event.getKey()); assertEquals(expectedValue, event.getValue()); } protected void waitUntilListenerInstalled(final Cache<?, ?> cache, final CheckPoint checkPoint) { CacheNotifier cn = TestingUtil.extractComponent(cache, CacheNotifier.class); final Answer<Object> forwardedAnswer = AdditionalAnswers.delegatesTo(cn); CacheNotifier mockNotifier = mock(CacheNotifier.class, withSettings().defaultAnswer(forwardedAnswer)); doAnswer(invocation -> { // Wait for main thread to sync up checkPoint.trigger("pre_add_listener_invoked_" + cache); // Now wait until main thread lets us through checkPoint.awaitStrict("pre_add_listener_release_" + cache, 10, TimeUnit.SECONDS); try { return forwardedAnswer.answer(invocation); } finally { // Wait for main thread to sync up checkPoint.trigger("post_add_listener_invoked_" + cache); // Now wait until main thread lets us through checkPoint.awaitStrict("post_add_listener_release_" + cache, 10, TimeUnit.SECONDS); } }).when(mockNotifier).addFilteredListener(notNull(), nullable(CacheEventFilter.class), nullable(CacheEventConverter.class), any(Set.class)); TestingUtil.replaceComponent(cache, CacheNotifier.class, mockNotifier, true); } protected void waitUntilNotificationRaised(final Cache<?, ?> cache, final CheckPoint checkPoint) { CacheNotifier cn = TestingUtil.extractComponent(cache, CacheNotifier.class); final Answer<Object> forwardedAnswer = AdditionalAnswers.delegatesTo(cn); CacheNotifier mockNotifier = mock(CacheNotifier.class, withSettings().defaultAnswer(forwardedAnswer)); Answer answer = invocation -> { // Wait for main thread to sync up checkPoint.trigger("pre_raise_notification_invoked"); // Now wait until main thread lets us through checkPoint.awaitStrict("pre_raise_notification_release", 10, TimeUnit.SECONDS); try { return forwardedAnswer.answer(invocation); } finally { // Wait for main thread to sync up checkPoint.trigger("post_raise_notification_invoked"); // Now wait until main thread lets us through checkPoint.awaitStrict("post_raise_notification_release", 10, TimeUnit.SECONDS); } }; doAnswer(answer).when(mockNotifier).notifyCacheEntryCreated(any(), any(), any(Metadata.class), eq(false), any(InvocationContext.class), any(FlagAffectedCommand.class)); doAnswer(answer).when(mockNotifier).notifyCacheEntryModified(any(), any(), any(Metadata.class), any(), any(Metadata.class), anyBoolean(), any(InvocationContext.class), any(FlagAffectedCommand.class)); doAnswer(answer).when(mockNotifier).notifyCacheEntryRemoved(any(), any(), any(Metadata.class), eq(false), any(InvocationContext.class), any(FlagAffectedCommand.class)); TestingUtil.replaceComponent(cache, CacheNotifier.class, mockNotifier, true); } protected void waitUntilViewChangeOccurs(final CacheContainer cacheContainer, final String uniqueId, final CheckPoint checkPoint) { CacheManagerNotifier cmn = TestingUtil.extractGlobalComponent(cacheContainer, CacheManagerNotifier.class); final Answer<Object> forwardedAnswer = AdditionalAnswers.delegatesTo(cmn); CacheManagerNotifier mockNotifier = mock(CacheManagerNotifier.class, withSettings().defaultAnswer(forwardedAnswer)); doAnswer(invocation -> { // Wait for main thread to sync up checkPoint.trigger("pre_view_listener_invoked_" + uniqueId); // Now wait until main thread lets us through checkPoint.awaitStrict("pre_view_listener_release_" + uniqueId, 10, TimeUnit.SECONDS); try { return forwardedAnswer.answer(invocation); } finally { checkPoint.trigger("post_view_listener_invoked_" + uniqueId); } }).when(mockNotifier).notifyViewChange(anyList(), anyList(), any(Address.class), anyInt()); TestingUtil.replaceComponent(cacheContainer, CacheManagerNotifier.class, mockNotifier, true); } }