package org.infinispan.notifications.cachelistener; 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 java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted; import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified; import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; import org.infinispan.notifications.cachelistener.event.CacheEntryEvent; import org.infinispan.notifications.cachelistener.event.Event; import org.infinispan.test.SingleCacheManagerTest; import org.infinispan.test.TestingUtil; import org.infinispan.test.fwk.CleanupAfterMethod; import org.infinispan.test.fwk.TestCacheManagerFactory; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.testng.annotations.Test; /** * Tests visibility of effects of cache operations on a separate thread once * a cache listener event has been consumed for the corresponding cache * operation. * * @author Galder ZamarreƱo * @since 5.1 */ @Test(groups = "functional", testName = "notifications.cachelistener.CacheListenerVisibilityTest") @CleanupAfterMethod public class CacheListenerVisibilityTest extends SingleCacheManagerTest { @Override protected EmbeddedCacheManager createCacheManager() throws Exception { return TestCacheManagerFactory.createCacheManager(false); } public void testSizeVisibility() throws Exception { updateCache(Visibility.SIZE); } public void testGetVisibility() throws Exception { updateCache(Visibility.GET); } public void testGetVisibilityWithinEntryCreatedListener() throws Exception { updateCacheAssertInListener( new EntryCreatedWithAssertListener(new CountDownLatch(1))); } public void testGetVisibilityWithinEntryModifiedListener() throws Exception { updateCacheAssertInListener( new EntryModifiedWithAssertListener(new CountDownLatch(1))); } public void testRemoveVisibility() throws Exception { cache.put(1, "v1"); final CountDownLatch after = new CountDownLatch(1); final CountDownLatch afterContinue = new CountDownLatch(1); final CountDownLatch before = new CountDownLatch(1); cache.addListener(new EntryListener(before, afterContinue, after)); assertEquals("v1", cache.get(1)); Future<Void> ignore = fork(new Callable<Void>() { @Override public Void call() throws Exception { cache.remove(1); return null; } }); // With removes, there's a before/after event acknowledgment, so verify it // Evicts on the other hand only emit a single event, after boolean signalled = before.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for before listener notification", signalled); assertEquals("v1", cache.get(1)); // Let the isPre=false callback continue afterContinue.countDown(); signalled = after.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for after listener notification", signalled); assertEquals(null, cache.get(1)); ignore.get(5, TimeUnit.SECONDS); } public void testEvictOnCacheEntryEvictedVisibility() throws Exception { checkEvictVisibility(false); } public void testEvictOnCacheEntriesEvictedVisibility() throws Exception { checkEvictVisibility(true); } private void checkEvictVisibility(boolean isCacheEntriesEvicted) throws Exception { cache.put(1, "v1"); final CountDownLatch after = new CountDownLatch(1); Object listener = isCacheEntriesEvicted ? new EntriesEvictedListener(after) : new EntryListener(null, null, after); cache.addListener(listener); assertEquals("v1", cache.get(1)); Future<Void> ignore = fork(new Callable<Void>() { @Override public Void call() throws Exception { cache.evict(1); return null; } }); boolean signalled = after.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for after listener notification", signalled); assertEquals(null, cache.get(1)); ignore.get(5, TimeUnit.SECONDS); } public void testClearVisibility() throws Exception { cache.put(1, "v1"); cache.put(2, "v1"); cache.put(3, "v1"); final CyclicBarrier after = new CyclicBarrier(2); final CountDownLatch afterContinue = new CountDownLatch(1); final CountDownLatch before = new CountDownLatch(1); cache.addListener(new CacheClearListener(before, afterContinue, after)); assertEquals("v1", cache.get(1)); assertEquals("v1", cache.get(2)); assertEquals("v1", cache.get(3)); Future<Void> ignore = fork(new Callable<Void>() { @Override public Void call() throws Exception { cache.clear(); return null; } }); boolean signalled = before.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for before listener notification", signalled); assertEquals("v1", cache.get(1)); assertEquals("v1", cache.get(2)); assertEquals("v1", cache.get(3)); // Let the isPre=false callback continue afterContinue.countDown(); // Wait for isPre=false remove notification for k=1 after.await(30, TimeUnit.SECONDS); assertEquals(null, cache.get(1)); // Wait for isPre=false remove notification for k=2 after.await(30, TimeUnit.SECONDS); assertEquals(null, cache.get(2)); // Wait for isPre=false remove notification for k=3 after.await(30, TimeUnit.SECONDS); assertEquals(null, cache.get(3)); assertTrue(cache.isEmpty()); ignore.get(5, TimeUnit.SECONDS); } private void updateCacheAssertInListener(WithAssertListener listener) throws Exception { cache.addListener(listener); Future<Void> ignore = fork(new Callable<Void>() { @Override public Void call() throws Exception { cache.put("k", "v"); return null; } }); listener.latch.await(30, TimeUnit.SECONDS); assert listener.assertNotNull; assert listener.assertValue; ignore.get(5, TimeUnit.SECONDS); } private void updateCache(Visibility visibility) throws Exception { final String key = "k-" + visibility; final String value = "k-" + visibility; final CountDownLatch after = new CountDownLatch(1); final CountDownLatch afterContinue = new CountDownLatch(1); final CountDownLatch before = new CountDownLatch(1); cache.addListener(new EntryListener(before, afterContinue, after)); switch (visibility) { case SIZE: assertEquals(0, cache.size()); break; case GET: assertNull(cache.get(key)); break; } Future<Void> ignore = fork(new Callable<Void>() { @Override public Void call() throws Exception { cache.put(key, value); return null; } }); boolean signalled = before.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for before listener notification", signalled); switch (visibility) { case SIZE: assertEquals(0, cache.size()); break; case GET: assertNull(cache.get(key)); break; } // Let the isPre=false callback continue afterContinue.countDown(); signalled = after.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for after listener notification", signalled); switch (visibility) { case SIZE: assertEquals(1, cache.size()); break; case GET: Object retVal = cache.get(key); assertNotNull(retVal); assertEquals(retVal, value); break; } ignore.get(5, TimeUnit.SECONDS); } @Listener public static class EntriesEvictedListener { // Use a different listener class for @CacheEntriesEvicted vs // @CacheEntryEvicted to tests both callbacks separately Log log = LogFactory.getLog(EntriesEvictedListener.class); final CountDownLatch after; public EntriesEvictedListener(CountDownLatch after) { this.after = after; } @CacheEntriesEvicted @SuppressWarnings("unused") public void entryEvicted(Event e) { log.info("Cache entries evicted, now check in different thread"); after.countDown(); // Force a bit of delay in the listener so that lack of visibility // of changes in container can be appreciated more easily TestingUtil.sleepThread(1000); } } @Listener public static class CacheClearListener { Log log = LogFactory.getLog(CacheClearListener.class); final CyclicBarrier after; final CountDownLatch before; final CountDownLatch afterContinue; public CacheClearListener(CountDownLatch before, CountDownLatch afterContinue, CyclicBarrier after) { this.before = before; this.after = after; this.afterContinue = afterContinue; } @CacheEntryRemoved @SuppressWarnings("unused") public void entryTouched(Event e) { if (!e.isPre()) { log.infof("Cache entry removed, event is: %s", e); try { after.await(30, TimeUnit.SECONDS); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } catch (BrokenBarrierException e1) { throw new IllegalStateException(e1); } catch (TimeoutException e1) { throw new IllegalStateException(e1); } // Force a bit of delay in the listener so that lack of visibility // of changes in container can be appreciated more easily TestingUtil.sleepThread(1000); } else { before.countDown(); try { boolean signalled = afterContinue.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for post listener event to execute", signalled); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } } } } @Listener public static class EntryListener { Log log = LogFactory.getLog(EntryListener.class); final CountDownLatch after; final CountDownLatch before; final CountDownLatch afterContinue; public EntryListener(CountDownLatch before, CountDownLatch afterContinue, CountDownLatch after) { this.before = before; this.after = after; this.afterContinue = afterContinue; } @CacheEntriesEvicted @SuppressWarnings("unused") public void entryEvicted(Event e) { log.info("Cache entry evicted, now check in different thread"); after.countDown(); // Force a bit of delay in the listener so that lack of visibility // of changes in container can be appreciated more easily TestingUtil.sleepThread(1000); } @CacheEntryCreated @CacheEntryRemoved @SuppressWarnings("unused") public void entryTouched(Event e) { if (!e.isPre()) { log.info("Cache entry touched, now check in different thread"); after.countDown(); // Force a bit of delay in the listener so that lack of visibility // of changes in container can be appreciated more easily TestingUtil.sleepThread(1000); } else { before.countDown(); try { boolean signalled = afterContinue.await(30, TimeUnit.SECONDS); assertTrue("Timed out while waiting for post listener event to execute", signalled); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } } } } public static abstract class WithAssertListener { Log log = LogFactory.getLog(WithAssertListener.class); final CountDownLatch latch; volatile boolean assertNotNull; volatile boolean assertValue; protected WithAssertListener(CountDownLatch latch) { this.latch = latch; } protected void assertCacheContents(CacheEntryEvent e) { if (!e.isPre()) { log.info("Cache entry created, now check cache contents"); Object value = e.getCache().get("k"); if (value == null) { assertNotNull = false; assertValue = false; } else { assertNotNull = true; assertValue = value.equals("v"); } // Force a bit of delay in the listener latch.countDown(); } } } @Listener public static class EntryCreatedWithAssertListener extends WithAssertListener { protected EntryCreatedWithAssertListener(CountDownLatch latch) { super(latch); } @CacheEntryCreated @SuppressWarnings("unused") public void entryCreated(CacheEntryEvent e) { assertCacheContents(e); } } @Listener public static class EntryModifiedWithAssertListener extends WithAssertListener { protected EntryModifiedWithAssertListener(CountDownLatch latch) { super(latch); } @CacheEntryCreated @CacheEntryModified @SuppressWarnings("unused") public void entryCreated(CacheEntryEvent e) { assertCacheContents(e); } } private enum Visibility { SIZE, GET } }