package org.hibernate.test.cache.infinispan.functional; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import javax.transaction.Synchronization; import org.hibernate.PessimisticLockException; import org.hibernate.Session; import org.hibernate.StaleStateException; import org.hibernate.cache.infinispan.impl.BaseTransactionalDataRegion; import org.hibernate.cache.infinispan.util.Caches; import org.hibernate.cache.infinispan.util.VersionedEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.test.cache.infinispan.functional.entities.Item; import org.hibernate.test.cache.infinispan.functional.entities.OtherItem; import org.junit.Test; import org.infinispan.AdvancedCache; import org.infinispan.commands.write.PutKeyValueCommand; import org.infinispan.commons.util.ByRef; import org.infinispan.context.Flag; import org.infinispan.context.InvocationContext; import org.infinispan.interceptors.base.BaseCustomInterceptor; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** * Tests specific to versioned entries -based caches. * Similar to {@link TombstoneTest} but some cases have been removed since * we are modifying the cache only once, therefore some sequences of operations * would fail before touching the cache. * * @author Radim Vansa <rvansa@redhat.com> */ public class VersionedTest extends AbstractNonInvalidationTest { @Override public List<Object[]> getParameters() { return Arrays.asList(NONSTRICT_REPLICATED, NONSTRICT_DISTRIBUTED); } @Override protected boolean getUseQueryCache() { return false; } @Test public void testTwoRemoves() 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); assertSingleCacheEntry(); commitLatch.countDown(); boolean firstResult = first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); boolean secondResult = second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); assertTrue(firstResult != secondResult); assertSingleEmpty(); TIME_SERVICE.advance(timeout + 1); assertEmptyCache(); } @Test public void testRemoveRolledBack() throws Exception { withTxSession(s -> { Item item = s.load(Item.class, itemId); s.delete(item); assertSingleCacheEntry(); s.flush(); assertSingleCacheEntry(); markRollbackOnly(s); }); assertSingleCacheEntry(); } @Test public void testUpdateRolledBack() throws Exception { ByRef<Object> entryRef = new ByRef<>(null); withTxSession(s -> { Item item = s.load(Item.class, itemId); item.getDescription(); Object prevEntry = assertSingleCacheEntry(); entryRef.set(prevEntry); item.setDescription("Updated item"); s.update(item); assertEquals(prevEntry, assertSingleCacheEntry()); s.flush(); assertEquals(prevEntry, assertSingleCacheEntry()); markRollbackOnly(s); }); assertEquals(entryRef.get(), assertSingleCacheEntry()); } @Test public void testStaleReadDuringUpdate() throws Exception { ByRef<Object> entryRef = testStaleRead((s, item) -> { item.setDescription("Updated item"); s.update(item); }); assertNotEquals(entryRef.get(), assertSingleCacheEntry()); withTxSession(s -> { Item item = s.load(Item.class, itemId); assertEquals("Updated item", item.getDescription()); }); } @Test public void testStaleReadDuringRemove() throws Exception { testStaleRead((s, item) -> s.delete(item)); assertSingleEmpty(); withTxSession(s -> { Item item = s.get(Item.class, itemId); assertNull(item); }); } protected ByRef<Object> testStaleRead(BiConsumer<Session, Item> consumer) throws Exception { AtomicReference<Exception> synchronizationException = new AtomicReference<>(); CountDownLatch syncLatch = new CountDownLatch(1); CountDownLatch commitLatch = new CountDownLatch(1); Future<Boolean> action = executor.submit(() -> withTxSessionApply(s -> { try { ((SharedSessionContractImplementor) s).getTransactionCoordinator().getLocalSynchronizations().registerSynchronization(new Synchronization() { @Override public void beforeCompletion() { } @Override public void afterCompletion(int i) { syncLatch.countDown(); try { awaitOrThrow(commitLatch); } catch (Exception e) { synchronizationException.set(e); } } }); Item item = s.load(Item.class, itemId); consumer.accept(s, item); s.flush(); } catch (StaleStateException e) { log.info("Exception thrown: ", e); markRollbackOnly(s); return false; } catch (PessimisticLockException e) { log.info("Exception thrown: ", e); markRollbackOnly(s); return false; } return true; })); awaitOrThrow(syncLatch); ByRef<Object> entryRef = new ByRef<>(null); try { withTxSession(s -> { Item item = s.load(Item.class, itemId); assertEquals("Original item", item.getDescription()); entryRef.set(assertSingleCacheEntry()); }); } finally { commitLatch.countDown(); } assertTrue(action.get(WAIT_TIMEOUT, TimeUnit.SECONDS)); assertNull(synchronizationException.get()); return entryRef; } @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); assertSingleCacheEntry(); preEvictLatch.countDown(); awaitOrThrow(postEvictLatch); assertSingleEmpty(); commitLatch.countDown(); first.get(WAIT_TIMEOUT, TimeUnit.SECONDS); second.get(WAIT_TIMEOUT, TimeUnit.SECONDS); assertSingleEmpty(); TIME_SERVICE.advance(timeout + 1); assertEmptyCache(); } @Test public void testEvictUpdateExpiration() throws Exception { // since the timestamp for update is based on session open/tx begin time, we have to do this sequentially sessionFactory().getCache().evictEntity(Item.class, itemId); assertSingleEmpty(); TIME_SERVICE.advance(1); withTxSession(s -> { Item item = s.load(Item.class, itemId); item.setDescription("Updated item"); s.update(item); }); assertSingleCacheEntry(); TIME_SERVICE.advance(timeout + 1); assertSingleCacheEntry(); } @Test public void testEvictAndPutFromLoad() throws Exception { sessionFactory().getCache().evictEntity(Item.class, itemId); assertSingleEmpty(); TIME_SERVICE.advance(1); withTxSession(s -> { Item item = s.load(Item.class, itemId); assertEquals("Original item", item.getDescription()); }); assertSingleCacheEntry(); TIME_SERVICE.advance(TIMEOUT + 1); assertSingleCacheEntry(); } @Test public void testCollectionUpdate() throws Exception { // the first insert puts VersionedEntry(null, null, timestamp), so we have to wait a while to cache the entry TIME_SERVICE.advance(1); withTxSession(s -> { Item item = s.load(Item.class, itemId); OtherItem otherItem = new OtherItem(); otherItem.setName("Other 1"); s.persist(otherItem); item.addOtherItem(otherItem); }); withTxSession(s -> { Item item = s.load(Item.class, itemId); Set<OtherItem> otherItems = item.getOtherItems(); assertFalse(otherItems.isEmpty()); otherItems.remove(otherItems.iterator().next()); }); AdvancedCache collectionCache = ((BaseTransactionalDataRegion) sessionFactory().getSecondLevelCacheRegion(Item.class.getName() + ".otherItems")).getCache(); CountDownLatch putFromLoadLatch = new CountDownLatch(1); AtomicBoolean committing = new AtomicBoolean(false); CollectionUpdateTestInterceptor collectionUpdateTestInterceptor = new CollectionUpdateTestInterceptor(putFromLoadLatch); AnotherCollectionUpdateTestInterceptor anotherInterceptor = new AnotherCollectionUpdateTestInterceptor(putFromLoadLatch, committing); collectionCache.addInterceptor(collectionUpdateTestInterceptor, collectionCache.getInterceptorChain().size() - 1); collectionCache.addInterceptor(anotherInterceptor, 0); TIME_SERVICE.advance(1); Future<Boolean> addFuture = executor.submit(() -> withTxSessionApply(s -> { collectionUpdateTestInterceptor.updateLatch.await(); Item item = s.load(Item.class, itemId); OtherItem otherItem = new OtherItem(); otherItem.setName("Other 2"); s.persist(otherItem); item.addOtherItem(otherItem); committing.set(true); return true; })); Future<Boolean> readFuture = executor.submit(() -> withTxSessionApply(s -> { Item item = s.load(Item.class, itemId); assertTrue(item.getOtherItems().isEmpty()); return true; })); addFuture.get(); readFuture.get(); collectionCache.removeInterceptor(CollectionUpdateTestInterceptor.class); collectionCache.removeInterceptor(AnotherCollectionUpdateTestInterceptor.class); withTxSession(s -> assertFalse(s.load(Item.class, itemId).getOtherItems().isEmpty())); } private class CollectionUpdateTestInterceptor extends BaseCustomInterceptor { final AtomicBoolean firstPutFromLoad = new AtomicBoolean(true); final CountDownLatch putFromLoadLatch; final CountDownLatch updateLatch = new CountDownLatch(1); public CollectionUpdateTestInterceptor(CountDownLatch putFromLoadLatch) { this.putFromLoadLatch = putFromLoadLatch; } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { if (command.hasFlag(Flag.ZERO_LOCK_ACQUISITION_TIMEOUT)) { if (firstPutFromLoad.compareAndSet(true, false)) { updateLatch.countDown(); putFromLoadLatch.await(); } } return super.visitPutKeyValueCommand(ctx, command); } } private class AnotherCollectionUpdateTestInterceptor extends BaseCustomInterceptor { final CountDownLatch putFromLoadLatch; final AtomicBoolean committing; public AnotherCollectionUpdateTestInterceptor(CountDownLatch putFromLoadLatch, AtomicBoolean committing) { this.putFromLoadLatch = putFromLoadLatch; this.committing = committing; } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { if (committing.get() && !command.hasFlag(Flag.ZERO_LOCK_ACQUISITION_TIMEOUT)) { putFromLoadLatch.countDown(); } return super.visitPutKeyValueCommand(ctx, command); } } protected void assertSingleEmpty() { Map contents = Caches.entrySet(entityCache).toMap(); Object value; assertEquals(1, contents.size()); value = contents.get(itemId); assertEquals(VersionedEntry.class, value.getClass()); assertNull(((VersionedEntry) value).getValue()); } }