package org.hibernate.test.cache.infinispan.functional; import java.util.Arrays; import java.util.List; import java.util.concurrent.BrokenBarrierException; 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.hibernate.cache.infinispan.util.Tombstone; import org.hibernate.test.cache.infinispan.functional.entities.Item; import org.hibernate.testing.TestForIssue; import org.infinispan.commands.write.PutKeyValueCommand; import org.infinispan.distribution.BlockingInterceptor; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * Tests specific to tombstone-based caches * * @author Radim Vansa <rvansa@redhat.com> */ public class TombstoneTest extends AbstractNonInvalidationTest { @Override public List<Object[]> getParameters() { return Arrays.asList(READ_WRITE_REPLICATED, READ_WRITE_DISTRIBUTED); } @Test public void testTombstoneExpiration() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch flushLatch = new CountDownLatch(2); CountDownLatch commitLatch = new CountDownLatch(1); Future<Boolean> first = removeFlushWait(itemId, loadBarrier, null, flushLatch, commitLatch); Future<Boolean> second = removeFlushWait(itemId, loadBarrier, null, flushLatch, commitLatch); awaitOrThrow(flushLatch); // Second remove fails due to being unable to lock entry *before* writing the tombstone assertTombstone(1); commitLatch.countDown(); first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); // after commit, the tombstone should still be in memory for some time (though, updatable) assertTombstone(1); TIME_SERVICE.advance(timeout + 1); assertEmptyCache(); } @Test public void testTwoUpdates1() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch preFlushLatch = new CountDownLatch(1); CountDownLatch flushLatch1 = new CountDownLatch(1); CountDownLatch flushLatch2 = new CountDownLatch(1); CountDownLatch commitLatch1 = new CountDownLatch(1); CountDownLatch commitLatch2 = new CountDownLatch(1); // Note: this is a single node case, we don't have to deal with async replication Future<Boolean> update1 = updateFlushWait(itemId, loadBarrier, null, flushLatch1, commitLatch1); Future<Boolean> update2 = updateFlushWait(itemId, loadBarrier, preFlushLatch, flushLatch2, commitLatch2); awaitOrThrow(flushLatch1); assertTombstone(1); preFlushLatch.countDown(); awaitOrThrow(flushLatch2); // Second update fails due to being unable to lock entry *before* writing the tombstone assertTombstone(1); commitLatch2.countDown(); assertFalse(update2.get(WAIT_TIMEOUT, TimeUnit.SECONDS)); assertTombstone(1); commitLatch1.countDown(); assertTrue(update1.get(WAIT_TIMEOUT, TimeUnit.SECONDS)); assertSingleCacheEntry(); } @Test public void testTwoUpdates2() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch preFlushLatch = new CountDownLatch(1); CountDownLatch flushLatch1 = new CountDownLatch(1); CountDownLatch flushLatch2 = new CountDownLatch(1); CountDownLatch commitLatch1 = new CountDownLatch(1); CountDownLatch commitLatch2 = new CountDownLatch(1); // Note: this is a single node case, we don't have to deal with async replication Future<Boolean> update1 = updateFlushWait(itemId, loadBarrier, null, flushLatch1, commitLatch1); Future<Boolean> update2 = updateFlushWait(itemId, loadBarrier, preFlushLatch, flushLatch2, commitLatch2); awaitOrThrow(flushLatch1); assertCacheContains(Tombstone.class); preFlushLatch.countDown(); awaitOrThrow(flushLatch2); // Second update fails due to being unable to lock entry *before* writing the tombstone assertTombstone(1); commitLatch1.countDown(); assertTrue(update1.get(WAIT_TIMEOUT, TimeUnit.SECONDS)); assertSingleCacheEntry(); commitLatch2.countDown(); assertFalse(update2.get(WAIT_TIMEOUT, TimeUnit.SECONDS)); assertSingleCacheEntry(); TIME_SERVICE.advance(TIMEOUT + 1); assertSingleCacheEntry(); } @Test public void testRemoveUpdateExpiration() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch preFlushLatch = new CountDownLatch(1); CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); Future<Boolean> first = removeFlushWait(itemId, loadBarrier, null, flushLatch, commitLatch); Future<Boolean> second = updateFlushWait(itemId, loadBarrier, preFlushLatch, null, commitLatch); awaitOrThrow(flushLatch); // Second update fails due to being unable to lock entry *before* writing the tombstone assertTombstone(1); preFlushLatch.countDown(); commitLatch.countDown(); first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); assertTombstone(1); TIME_SERVICE.advance(timeout + 1); assertEmptyCache(); } @Test public void testUpdateRemoveExpiration() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch preFlushLatch = new CountDownLatch(1); CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); Future<Boolean> first = updateFlushWait(itemId, loadBarrier, null, flushLatch, commitLatch); Future<Boolean> second = removeFlushWait(itemId, loadBarrier, preFlushLatch, null, commitLatch); awaitOrThrow(flushLatch); assertTombstone(1); preFlushLatch.countDown(); commitLatch.countDown(); first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); boolean removeSucceeded = second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); if (removeSucceeded) { assertCacheContains(Tombstone.class); TIME_SERVICE.advance(timeout + 1); assertEmptyCache(); } else { assertSingleCacheEntry(); TIME_SERVICE.advance(timeout + 1); assertSingleCacheEntry(); } } @Test public void testUpdateEvictExpiration() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch preEvictLatch = new CountDownLatch(1); CountDownLatch postEvictLatch = new CountDownLatch(1); CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); Future<Boolean> first = updateFlushWait(itemId, loadBarrier, null, flushLatch, commitLatch); Future<Boolean> second = evictWait(itemId, loadBarrier, preEvictLatch, postEvictLatch); awaitOrThrow(flushLatch); assertTombstone(1); preEvictLatch.countDown(); awaitOrThrow(postEvictLatch); assertTombstone(1); commitLatch.countDown(); first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); assertSingleCacheEntry(); TIME_SERVICE.advance(timeout + 1); assertSingleCacheEntry(); } @Test public void testEvictUpdate() throws Exception { CyclicBarrier loadBarrier = new CyclicBarrier(2); CountDownLatch preFlushLatch = new CountDownLatch(1); CountDownLatch postEvictLatch = new CountDownLatch(1); CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); Future<Boolean> first = evictWait(itemId, loadBarrier, null, postEvictLatch); Future<Boolean> second = updateFlushWait(itemId, loadBarrier, preFlushLatch, flushLatch, commitLatch); awaitOrThrow(postEvictLatch); assertEmptyCache(); preFlushLatch.countDown(); awaitOrThrow(flushLatch); // The tombstone from update has overwritten the eviction tombstone as it has timestamp = now + 60s assertTombstone(1); commitLatch.countDown(); first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); // Since evict was executed during the update, we cannot insert the entry into cache assertSingleCacheEntry(); TIME_SERVICE.advance(timeout + 1); assertSingleCacheEntry(); } @Test public void testEvictUpdate2() throws Exception { CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); sessionFactory().getCache().evictEntity(Item.class, itemId); // When the cache was empty, the tombstone is not stored assertEmptyCache(); TIME_SERVICE.advance(1); Future<Boolean> update = updateFlushWait(itemId, null, null, flushLatch, commitLatch); awaitOrThrow(flushLatch); assertTombstone(1); commitLatch.countDown(); update.get(WAIT_TIMEOUT, TimeUnit.SECONDS); assertSingleCacheEntry(); TIME_SERVICE.advance(timeout + 2); assertSingleCacheEntry(); } @Test public void testEvictPutFromLoad() throws Exception { sessionFactory().getCache().evictEntity(Item.class, itemId); assertEmptyCache(); TIME_SERVICE.advance(1); assertItemDescription("Original item"); assertSingleCacheEntry(); TIME_SERVICE.advance(timeout + 2); assertSingleCacheEntry(); } protected void assertItemDescription(String expected) throws Exception { assertEquals(expected, withTxSessionApply(s -> s.load(Item.class, itemId).getDescription())); } @Test public void testPutFromLoadDuringUpdate() throws Exception { CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); CyclicBarrier putFromLoadBarrier = new CyclicBarrier(2); // We cannot just do load during update because that could be blocked in DB Future<?> putFromLoad = blockedPutFromLoad(putFromLoadBarrier); Future<Boolean> update = updateFlushWait(itemId, null, null, flushLatch, commitLatch); awaitOrThrow(flushLatch); assertTombstone(1); unblockPutFromLoad(putFromLoadBarrier, putFromLoad); commitLatch.countDown(); update.get(WAIT_TIMEOUT, TimeUnit.SECONDS); assertSingleCacheEntry(); assertItemDescription("Updated item"); } @TestForIssue(jiraKey = "HHH-11323") @Test public void testEvictPutFromLoadDuringUpdate() throws Exception { CountDownLatch flushLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); CyclicBarrier putFromLoadBarrier = new CyclicBarrier(2); Future<?> putFromLoad = blockedPutFromLoad(putFromLoadBarrier); Future<Boolean> update = updateFlushWait(itemId, null, null, flushLatch, commitLatch); // Flush stores FutureUpdate(timestamp, null) awaitOrThrow(flushLatch); sessionFactory().getCache().evictEntity(Item.class, itemId); commitLatch.countDown(); update.get(WAIT_TIMEOUT, TimeUnit.SECONDS); unblockPutFromLoad(putFromLoadBarrier, putFromLoad); assertItemDescription("Updated item"); } private Future<?> blockedPutFromLoad(CyclicBarrier putFromLoadBarrier) throws InterruptedException, BrokenBarrierException, TimeoutException { BlockingInterceptor blockingInterceptor = new BlockingInterceptor(putFromLoadBarrier, PutKeyValueCommand.class, false, true); entityCache.addInterceptor(blockingInterceptor, 0); cleanup.add(() -> entityCache.removeInterceptor(BlockingInterceptor.class)); // the putFromLoad should be blocked in the interceptor Future<?> putFromLoad = executor.submit(() -> withTxSessionApply(s -> { assertEquals("Original item", s.load(Item.class, itemId).getDescription()); return null; })); putFromLoadBarrier.await(WAIT_TIMEOUT, TimeUnit.SECONDS); blockingInterceptor.suspend(true); return putFromLoad; } private void unblockPutFromLoad(CyclicBarrier putFromLoadBarrier, Future<?> putFromLoad) throws InterruptedException, BrokenBarrierException, TimeoutException, java.util.concurrent.ExecutionException { putFromLoadBarrier.await(WAIT_TIMEOUT, TimeUnit.SECONDS); putFromLoad.get(WAIT_TIMEOUT, TimeUnit.SECONDS); } private void assertTombstone(int expectedSize) { Tombstone tombstone = assertCacheContains(Tombstone.class); assertEquals("Tombstone is " + tombstone, expectedSize, tombstone.size()); } }