package org.hibernate.test.cache.infinispan.functional; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.Phaser; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.hibernate.PessimisticLockException; import org.hibernate.cache.infinispan.InfinispanRegionFactory; import org.hibernate.cache.infinispan.entity.EntityRegionImpl; import org.hibernate.cache.infinispan.util.InfinispanMessageLogger; import org.hibernate.testing.TestForIssue; import org.hibernate.test.cache.infinispan.functional.entities.Item; import org.hibernate.test.cache.infinispan.util.TestInfinispanRegionFactory; import org.junit.Ignore; import org.junit.Test; import org.infinispan.AdvancedCache; import org.infinispan.commands.read.GetKeyValueCommand; 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.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** * Tests specific to invalidation mode caches * * @author Radim Vansa <rvansa@redhat.com> */ public class InvalidationTest extends SingleNodeTest { static final InfinispanMessageLogger log = InfinispanMessageLogger.Provider.getLog(ReadOnlyTest.class); @Override public List<Object[]> getParameters() { return Arrays.asList(TRANSACTIONAL, READ_WRITE_INVALIDATION); } @Override protected void addSettings(Map settings) { super.addSettings(settings); settings.put(TestInfinispanRegionFactory.PENDING_PUTS_SIMPLE, false); } @Test @TestForIssue(jiraKey = "HHH-9868") public void testConcurrentRemoveAndPutFromLoad() throws Exception { final Item item = new Item( "chris", "Chris's Item" ); withTxSession(s -> { s.persist(item); }); Phaser deletePhaser = new Phaser(2); Phaser getPhaser = new Phaser(2); HookInterceptor hook = new HookInterceptor(); AdvancedCache pendingPutsCache = getPendingPutsCache(Item.class); pendingPutsCache.addInterceptor(hook, 0); AtomicBoolean getThreadBlockedInDB = new AtomicBoolean(false); Thread deleteThread = new Thread(() -> { try { withTxSession(s -> { Item loadedItem = s.get(Item.class, item.getId()); assertNotNull(loadedItem); arriveAndAwait(deletePhaser, 2000); arriveAndAwait(deletePhaser, 2000); log.trace("Item loaded"); s.delete(loadedItem); s.flush(); log.trace("Item deleted"); // start get-thread here arriveAndAwait(deletePhaser, 2000); // we need longer timeout since in non-MVCC DBs the get thread // can be blocked arriveAndAwait(deletePhaser, 4000); }); } catch (Exception e) { throw new RuntimeException(e); } }, "delete-thread"); Thread getThread = new Thread(() -> { try { withTxSession(s -> { // DB load should happen before the record is deleted, // putFromLoad should happen after deleteThread ends Item loadedItem = s.get(Item.class, item.getId()); if (getThreadBlockedInDB.get()) { assertNull(loadedItem); } else { assertNotNull(loadedItem); } }); } catch (PessimisticLockException e) { // If we end up here, database locks guard us against situation tested // in this case and HHH-9868 cannot happen. // (delete-thread has ITEMS table write-locked and we try to acquire read-lock) try { arriveAndAwait(getPhaser, 2000); arriveAndAwait(getPhaser, 2000); } catch (Exception e1) { throw new RuntimeException(e1); } } catch (Exception e) { throw new RuntimeException(e); } }, "get-thread"); deleteThread.start(); // deleteThread loads the entity arriveAndAwait(deletePhaser, 2000); withTx(() -> { sessionFactory().getCache().evictEntity(Item.class, item.getId()); assertFalse(sessionFactory().getCache().containsEntity(Item.class, item.getId())); return null; }); arriveAndAwait(deletePhaser, 2000); // delete thread invalidates PFER arriveAndAwait(deletePhaser, 2000); // get thread gets the entity from DB hook.block(getPhaser, getThread); getThread.start(); try { arriveAndAwait(getPhaser, 2000); } catch (TimeoutException e) { getThreadBlockedInDB.set(true); } arriveAndAwait(deletePhaser, 2000); // delete thread finishes the remove from DB and cache deleteThread.join(); hook.unblock(); arriveAndAwait(getPhaser, 2000); // get thread puts the entry into cache getThread.join(); assertNoInvalidators(pendingPutsCache); withTxSession(s -> { Item loadedItem = s.get(Item.class, item.getId()); assertNull(loadedItem); }); } protected AdvancedCache getPendingPutsCache(Class<Item> entityClazz) { EntityRegionImpl region = (EntityRegionImpl) sessionFactory().getCache() .getEntityRegionAccess(entityClazz.getName()).getRegion(); AdvancedCache entityCache = region.getCache(); return (AdvancedCache) entityCache.getCacheManager().getCache( entityCache.getName() + "-" + InfinispanRegionFactory.DEF_PENDING_PUTS_RESOURCE).getAdvancedCache(); } protected static void arriveAndAwait(Phaser phaser, int timeout) throws TimeoutException, InterruptedException { phaser.awaitAdvanceInterruptibly(phaser.arrive(), timeout, TimeUnit.MILLISECONDS); } @TestForIssue(jiraKey = "HHH-11304") @Test public void testFailedInsert() throws Exception { AdvancedCache pendingPutsCache = getPendingPutsCache(Item.class); assertNoInvalidators(pendingPutsCache); withTxSession(s -> { Item i = new Item("inserted", "bar"); s.persist(i); s.flush(); s.getTransaction().setRollbackOnly(); }); assertNoInvalidators(pendingPutsCache); } @TestForIssue(jiraKey = "HHH-11304") @Test public void testFailedUpdate() throws Exception { AdvancedCache pendingPutsCache = getPendingPutsCache(Item.class); assertNoInvalidators(pendingPutsCache); final Item item = new Item("before-update", "bar"); withTxSession(s -> s.persist(item)); withTxSession(s -> { Item item2 = s.load(Item.class, item.getId()); assertEquals("before-update", item2.getName()); item2.setName("after-update"); s.persist(item2); s.flush(); s.flush(); // workaround for HHH-11312 s.getTransaction().setRollbackOnly(); }); assertNoInvalidators(pendingPutsCache); withTxSession(s -> { Item item3 = s.load(Item.class, item.getId()); assertEquals("before-update", item3.getName()); s.remove(item3); }); assertNoInvalidators(pendingPutsCache); } @TestForIssue(jiraKey = "HHH-11304") @Test public void testFailedRemove() throws Exception { AdvancedCache pendingPutsCache = getPendingPutsCache(Item.class); assertNoInvalidators(pendingPutsCache); final Item item = new Item("before-remove", "bar"); withTxSession(s -> s.persist(item)); withTxSession(s -> { Item item2 = s.load(Item.class, item.getId()); assertEquals("before-remove", item2.getName()); s.remove(item2); s.flush(); s.getTransaction().setRollbackOnly(); }); assertNoInvalidators(pendingPutsCache); withTxSession(s -> { Item item3 = s.load(Item.class, item.getId()); assertEquals("before-remove", item3.getName()); s.remove(item3); }); assertNoInvalidators(pendingPutsCache); } protected void assertNoInvalidators(AdvancedCache<Object, Object> pendingPutsCache) throws Exception { Method getInvalidators = null; for (Map.Entry<Object, Object> entry : pendingPutsCache.entrySet()) { if (getInvalidators == null) { getInvalidators = entry.getValue().getClass().getMethod("getInvalidators"); getInvalidators.setAccessible(true); } Collection invalidators = (Collection) getInvalidators.invoke(entry.getValue()); if (invalidators != null) { assertTrue("Invalidators on key " + entry.getKey() + ": " + invalidators, invalidators.isEmpty()); } } } private static class HookInterceptor extends BaseCustomInterceptor { Phaser phaser; Thread thread; public synchronized void block(Phaser phaser, Thread thread) { this.phaser = phaser; this.thread = thread; } public synchronized void unblock() { phaser = null; thread = null; } @Override public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable { Phaser phaser; Thread thread; synchronized (this) { phaser = this.phaser; thread = this.thread; } if (phaser != null && Thread.currentThread() == thread) { arriveAndAwait(phaser, 2000); arriveAndAwait(phaser, 2000); } return super.visitGetKeyValueCommand(ctx, command); } } }