/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.test.cache.infinispan.entity; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.hibernate.cache.infinispan.entity.EntityRegionImpl; import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cache.spi.access.EntityRegionAccessStrategy; import org.hibernate.cache.spi.access.SoftLock; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.test.cache.infinispan.AbstractRegionAccessStrategyTest; import org.hibernate.test.cache.infinispan.NodeEnvironment; import org.hibernate.test.cache.infinispan.util.TestSynchronization; import org.hibernate.test.cache.infinispan.util.TestingKeyFactory; import org.junit.Ignore; import org.junit.Test; import junit.framework.AssertionFailedError; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** * Base class for tests of EntityRegionAccessStrategy impls. * * @author Galder ZamarreƱo * @since 3.5 */ public class EntityRegionAccessStrategyTest extends AbstractRegionAccessStrategyTest<EntityRegionImpl, EntityRegionAccessStrategy> { protected static int testCount; @Override protected Object generateNextKey() { return TestingKeyFactory.generateEntityCacheKey( KEY_BASE + testCount++ ); } @Override protected EntityRegionImpl getRegion(NodeEnvironment environment) { return environment.getEntityRegion(REGION_NAME, CACHE_DATA_DESCRIPTION); } @Override protected EntityRegionAccessStrategy getAccessStrategy(EntityRegionImpl region) { return region.buildAccessStrategy( accessType ); } @Test public void testGetRegion() { assertEquals("Correct region", localRegion, localAccessStrategy.getRegion()); } @Test public void testPutFromLoad() throws Exception { if (accessType == AccessType.READ_ONLY) { putFromLoadTestReadOnly(false); } else { putFromLoadTest(false, false); } } @Test public void testPutFromLoadMinimal() throws Exception { if (accessType == AccessType.READ_ONLY) { putFromLoadTestReadOnly(true); } else { putFromLoadTest(true, false); } } @Test public void testInsert() throws Exception { final Object KEY = generateNextKey(); final CountDownLatch readLatch = new CountDownLatch(1); final CountDownLatch commitLatch = new CountDownLatch(1); final CountDownLatch completionLatch = new CountDownLatch(2); CountDownLatch asyncInsertLatch = expectAfterUpdate(); Thread inserter = new Thread(() -> { try { SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { assertNull("Correct initial value", localAccessStrategy.get(session, KEY, session.getTimestamp())); doInsert(localAccessStrategy, session, KEY, VALUE1, 1); readLatch.countDown(); commitLatch.await(); return null; }); } catch (Exception e) { log.error("node1 caught exception", e); node1Exception = e; } catch (AssertionFailedError e) { node1Failure = e; } finally { completionLatch.countDown(); } }, "testInsert-inserter"); Thread reader = new Thread(() -> { try { SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { readLatch.await(); assertNull("Correct initial value", localAccessStrategy.get(session, KEY, session.getTimestamp())); return null; }); } catch (Exception e) { log.error("node1 caught exception", e); node1Exception = e; } catch (AssertionFailedError e) { node1Failure = e; } finally { commitLatch.countDown(); completionLatch.countDown(); } }, "testInsert-reader"); inserter.setDaemon(true); reader.setDaemon(true); inserter.start(); reader.start(); assertTrue("Threads completed", completionLatch.await(10, TimeUnit.SECONDS)); assertThreadsRanCleanly(); SharedSessionContractImplementor s1 = mockedSession(); assertEquals("Correct node1 value", VALUE1, localAccessStrategy.get(s1, KEY, s1.getTimestamp())); assertTrue(asyncInsertLatch.await(10, TimeUnit.SECONDS)); Object expected = isUsingInvalidation() ? null : VALUE1; SharedSessionContractImplementor s2 = mockedSession(); assertEquals("Correct node2 value", expected, remoteAccessStrategy.get(s2, KEY, s2.getTimestamp())); } protected void doInsert(EntityRegionAccessStrategy strategy, SharedSessionContractImplementor session, Object key, String value, Object version) { strategy.insert(session, key, value, null); session.getTransactionCoordinator().getLocalSynchronizations().registerSynchronization( new TestSynchronization.AfterInsert(strategy, session, key, value, version)); } protected void putFromLoadTestReadOnly(boolean minimal) throws Exception { final Object KEY = TestingKeyFactory.generateEntityCacheKey( KEY_BASE + testCount++ ); CountDownLatch remotePutFromLoadLatch = expectPutFromLoad(); SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { assertNull(localAccessStrategy.get(session, KEY, session.getTimestamp())); if (minimal) localAccessStrategy.putFromLoad(session, KEY, VALUE1, session.getTimestamp(), 1, true); else localAccessStrategy.putFromLoad(session, KEY, VALUE1, session.getTimestamp(), 1); return null; }); SharedSessionContractImplementor s2 = mockedSession(); assertEquals(VALUE1, localAccessStrategy.get(s2, KEY, s2.getTimestamp())); SharedSessionContractImplementor s3 = mockedSession(); Object expected; if (isUsingInvalidation()) { expected = null; } else { if (accessType != AccessType.NONSTRICT_READ_WRITE) { assertTrue(remotePutFromLoadLatch.await(2, TimeUnit.SECONDS)); } expected = VALUE1; } assertEquals(expected, remoteAccessStrategy.get(s3, KEY, s3.getTimestamp())); } @Test public void testUpdate() throws Exception { if (accessType == AccessType.READ_ONLY) { return; } final Object KEY = generateNextKey(); // Set up initial state SharedSessionContractImplementor s1 = mockedSession(); localAccessStrategy.putFromLoad(s1, KEY, VALUE1, s1.getTimestamp(), 1); SharedSessionContractImplementor s2 = mockedSession(); remoteAccessStrategy.putFromLoad(s2, KEY, VALUE1, s2.getTimestamp(), 1); // both nodes are updated, we don't have to wait for any async replication of putFromLoad CountDownLatch asyncUpdateLatch = expectAfterUpdate(); final CountDownLatch readLatch = new CountDownLatch(1); final CountDownLatch commitLatch = new CountDownLatch(1); final CountDownLatch completionLatch = new CountDownLatch(2); Thread updater = new Thread(() -> { try { SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { log.debug("Transaction began, get initial value"); assertEquals("Correct initial value", VALUE1, localAccessStrategy.get(session, KEY, session.getTimestamp())); log.debug("Now update value"); doUpdate(localAccessStrategy, session, KEY, VALUE2, 2); log.debug("Notify the read latch"); readLatch.countDown(); log.debug("Await commit"); commitLatch.await(); return null; }); } catch (Exception e) { log.error("node1 caught exception", e); node1Exception = e; } catch (AssertionFailedError e) { node1Failure = e; } finally { if (readLatch.getCount() > 0) { readLatch.countDown(); } log.debug("Completion latch countdown"); completionLatch.countDown(); } }, "testUpdate-updater"); Thread reader = new Thread(() -> { try { SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { log.debug("Transaction began, await read latch"); readLatch.await(); log.debug("Read latch acquired, verify local access strategy"); // This won't block w/ mvc and will read the old value (if transactional as the transaction // is not being committed yet, or if non-strict as we do the actual update only after transaction) // or null if non-transactional Object expected = isTransactional() || accessType == AccessType.NONSTRICT_READ_WRITE ? VALUE1 : null; assertEquals("Correct value", expected, localAccessStrategy.get(session, KEY, session.getTimestamp())); return null; }); } catch (Exception e) { log.error("node1 caught exception", e); node1Exception = e; } catch (AssertionFailedError e) { node1Failure = e; } finally { commitLatch.countDown(); log.debug("Completion latch countdown"); completionLatch.countDown(); } }, "testUpdate-reader"); updater.setDaemon(true); reader.setDaemon(true); updater.start(); reader.start(); assertTrue(completionLatch.await(2, TimeUnit.SECONDS)); assertThreadsRanCleanly(); SharedSessionContractImplementor s3 = mockedSession(); assertEquals("Correct node1 value", VALUE2, localAccessStrategy.get(s3, KEY, s3.getTimestamp())); assertTrue(asyncUpdateLatch.await(10, TimeUnit.SECONDS)); Object expected = isUsingInvalidation() ? null : VALUE2; SharedSessionContractImplementor s4 = mockedSession(); assertEquals("Correct node2 value", expected, remoteAccessStrategy.get(s4, KEY, s4.getTimestamp())); } @Override protected void doUpdate(EntityRegionAccessStrategy strategy, SharedSessionContractImplementor session, Object key, Object value, Object version) throws javax.transaction.RollbackException, javax.transaction.SystemException { SoftLock softLock = strategy.lockItem(session, key, null); strategy.update(session, key, value, null, null); session.getTransactionCoordinator().getLocalSynchronizations().registerSynchronization( new TestSynchronization.AfterUpdate(strategy, session, key, value, version, softLock)); } /** * This test fails in CI too often because it depends on very short timeout. The behaviour is basically * non-testable as we want to make sure that the "Putter" is always progressing; however, it is sometimes * progressing in different thread (on different node), and sometimes even in system, sending a message * over network. Therefore even checking that some OOB/remote thread is in RUNNABLE/RUNNING state is prone * to spurious failure (and we can't grab the state of all threads atomically). */ @Ignore @Test public void testContestedPutFromLoad() throws Exception { if (accessType == AccessType.READ_ONLY) { return; } final Object KEY = TestingKeyFactory.generateEntityCacheKey(KEY_BASE + testCount++); SharedSessionContractImplementor s1 = mockedSession(); localAccessStrategy.putFromLoad(s1, KEY, VALUE1, s1.getTimestamp(), 1); final CountDownLatch pferLatch = new CountDownLatch(1); final CountDownLatch pferCompletionLatch = new CountDownLatch(1); final CountDownLatch commitLatch = new CountDownLatch(1); final CountDownLatch completionLatch = new CountDownLatch(1); Thread blocker = new Thread("Blocker") { @Override public void run() { try { SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { assertEquals("Correct initial value", VALUE1, localAccessStrategy.get(session, KEY, session.getTimestamp())); doUpdate(localAccessStrategy, session, KEY, VALUE2, 2); pferLatch.countDown(); commitLatch.await(); return null; }); } catch (Exception e) { log.error("node1 caught exception", e); node1Exception = e; } catch (AssertionFailedError e) { node1Failure = e; } finally { completionLatch.countDown(); } } }; Thread putter = new Thread("Putter") { @Override public void run() { try { SharedSessionContractImplementor session = mockedSession(); withTx(localEnvironment, session, () -> { localAccessStrategy.putFromLoad(session, KEY, VALUE1, session.getTimestamp(), 1); return null; }); } catch (Exception e) { log.error("node1 caught exception", e); node1Exception = e; } catch (AssertionFailedError e) { node1Failure = e; } finally { pferCompletionLatch.countDown(); } } }; blocker.start(); assertTrue("Active tx has done an update", pferLatch.await(1, TimeUnit.SECONDS)); putter.start(); assertTrue("putFromLoad returns promptly", pferCompletionLatch.await(10, TimeUnit.MILLISECONDS)); commitLatch.countDown(); assertTrue("Threads completed", completionLatch.await(1, TimeUnit.SECONDS)); assertThreadsRanCleanly(); SharedSessionContractImplementor session = mockedSession(); assertEquals("Correct node1 value", VALUE2, localAccessStrategy.get(session, KEY, session.getTimestamp())); } }