/*
* 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.access;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.transaction.TransactionManager;
import org.hibernate.cache.infinispan.InfinispanRegionFactory;
import org.hibernate.cache.infinispan.access.PutFromLoadValidator;
import org.hibernate.cache.infinispan.util.InfinispanMessageLogger;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.test.cache.infinispan.util.TestInfinispanRegionFactory;
import org.hibernate.test.cache.infinispan.util.TestTimeService;
import org.hibernate.testing.AfterClassOnce;
import org.hibernate.testing.BeforeClassOnce;
import org.hibernate.testing.TestForIssue;
import org.hibernate.test.cache.infinispan.functional.cluster.DualNodeJtaTransactionManagerImpl;
import org.hibernate.test.cache.infinispan.util.CacheTestUtil;
import org.hibernate.testing.junit4.CustomRunner;
import org.infinispan.AdvancedCache;
import org.infinispan.test.fwk.TestResourceTracker;
import org.junit.After;
import org.junit.Test;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.junit.runner.RunWith;
import static org.infinispan.test.Exceptions.expectException;
import static org.infinispan.test.TestingUtil.withTx;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
/**
* Tests of {@link PutFromLoadValidator}.
*
* @author Brian Stansberry
* @author Galder ZamarreƱo
* @version $Revision: $
*/
@RunWith(CustomRunner.class)
public class PutFromLoadValidatorUnitTest {
private static final InfinispanMessageLogger log = InfinispanMessageLogger.Provider.getLog(
PutFromLoadValidatorUnitTest.class);
private static final TestTimeService TIME_SERVICE = new TestTimeService();
private Object KEY1 = "KEY1";
private TransactionManager tm;
private EmbeddedCacheManager cm;
private AdvancedCache<Object, Object> cache;
private List<Runnable> cleanup = new ArrayList<>();
@BeforeClassOnce
public void setUp() throws Exception {
TestResourceTracker.testStarted(getClass().getSimpleName());
tm = DualNodeJtaTransactionManagerImpl.getInstance("test");
cm = TestCacheManagerFactory.createCacheManager(true);
cache = cm.getCache().getAdvancedCache();
}
@AfterClassOnce
public void stop() {
tm = null;
cm.stop();
TestResourceTracker.testFinished(getClass().getSimpleName());
}
@After
public void tearDown() throws Exception {
cleanup.forEach(Runnable::run);
cleanup.clear();
try {
DualNodeJtaTransactionManagerImpl.cleanupTransactions();
}
finally {
DualNodeJtaTransactionManagerImpl.cleanupTransactionManagers();
}
cache.clear();
cm.getCache(cache.getName() + "-" + InfinispanRegionFactory.DEF_PENDING_PUTS_RESOURCE).clear();
}
private static InfinispanRegionFactory regionFactory(EmbeddedCacheManager cm) {
Properties properties = new Properties();
properties.put(TestInfinispanRegionFactory.TIME_SERVICE, TIME_SERVICE);
InfinispanRegionFactory regionFactory = new TestInfinispanRegionFactory(properties);
regionFactory.setCacheManager(cm);
regionFactory.start(CacheTestUtil.sfOptionsForStart(), properties);
return regionFactory;
}
@Test
public void testNakedPut() throws Exception {
nakedPutTest(false);
}
@Test
public void testNakedPutTransactional() throws Exception {
nakedPutTest(true);
}
private void nakedPutTest(final boolean transactional) throws Exception {
PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
exec(transactional, new NakedPut(testee, true));
}
@Test
public void testRegisteredPut() throws Exception {
registeredPutTest(false);
}
@Test
public void testRegisteredPutTransactional() throws Exception {
registeredPutTest(true);
}
private void registeredPutTest(final boolean transactional) throws Exception {
PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
exec(transactional, new RegularPut(testee));
}
@Test
public void testNakedPutAfterKeyRemoval() throws Exception {
nakedPutAfterRemovalTest(false, false);
}
@Test
public void testNakedPutAfterKeyRemovalTransactional() throws Exception {
nakedPutAfterRemovalTest(true, false);
}
@Test
public void testNakedPutAfterRegionRemoval() throws Exception {
nakedPutAfterRemovalTest(false, true);
}
@Test
public void testNakedPutAfterRegionRemovalTransactional() throws Exception {
nakedPutAfterRemovalTest(true, true);
}
private void nakedPutAfterRemovalTest(final boolean transactional,
final boolean removeRegion) throws Exception {
PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
Invalidation invalidation = new Invalidation(testee, removeRegion);
// the naked put can succeed because it has txTimestamp after invalidation
NakedPut nakedPut = new NakedPut(testee, true);
exec(transactional, invalidation, nakedPut);
}
@Test
public void testRegisteredPutAfterKeyRemoval() throws Exception {
registeredPutAfterRemovalTest(false, false);
}
@Test
public void testRegisteredPutAfterKeyRemovalTransactional() throws Exception {
registeredPutAfterRemovalTest(true, false);
}
@Test
public void testRegisteredPutAfterRegionRemoval() throws Exception {
registeredPutAfterRemovalTest(false, true);
}
@Test
public void testRegisteredPutAfterRegionRemovalTransactional() throws Exception {
registeredPutAfterRemovalTest(true, true);
}
private void registeredPutAfterRemovalTest(final boolean transactional,
final boolean removeRegion) throws Exception {
PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
Invalidation invalidation = new Invalidation(testee, removeRegion);
RegularPut regularPut = new RegularPut(testee);
exec(transactional, invalidation, regularPut);
}
@Test
public void testRegisteredPutWithInterveningKeyRemoval() throws Exception {
registeredPutWithInterveningRemovalTest(false, false);
}
@Test
public void testRegisteredPutWithInterveningKeyRemovalTransactional() throws Exception {
registeredPutWithInterveningRemovalTest(true, false);
}
@Test
public void testRegisteredPutWithInterveningRegionRemoval() throws Exception {
registeredPutWithInterveningRemovalTest(false, true);
}
@Test
public void testRegisteredPutWithInterveningRegionRemovalTransactional() throws Exception {
registeredPutWithInterveningRemovalTest(true, true);
}
private void registeredPutWithInterveningRemovalTest(
final boolean transactional, final boolean removeRegion)
throws Exception {
PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
try {
long txTimestamp = TIME_SERVICE.wallClockTime();
if (transactional) {
tm.begin();
}
SharedSessionContractImplementor session1 = mock(SharedSessionContractImplementor.class);
SharedSessionContractImplementor session2 = mock(SharedSessionContractImplementor.class);
testee.registerPendingPut(session1, KEY1, txTimestamp);
if (removeRegion) {
testee.beginInvalidatingRegion();
} else {
testee.beginInvalidatingKey(session2, KEY1);
}
PutFromLoadValidator.Lock lock = testee.acquirePutFromLoadLock(session1, KEY1, txTimestamp);
try {
assertNull(lock);
}
finally {
if (lock != null) {
testee.releasePutFromLoadLock(KEY1, lock);
}
if (removeRegion) {
testee.endInvalidatingRegion();
} else {
testee.endInvalidatingKey(session2, KEY1);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void testMultipleRegistrations() throws Exception {
multipleRegistrationtest(false);
}
@Test
public void testMultipleRegistrationsTransactional() throws Exception {
multipleRegistrationtest(true);
}
private void multipleRegistrationtest(final boolean transactional) throws Exception {
final PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
final CountDownLatch registeredLatch = new CountDownLatch(3);
final CountDownLatch finishedLatch = new CountDownLatch(3);
final AtomicInteger success = new AtomicInteger();
Runnable r = () -> {
try {
long txTimestamp = TIME_SERVICE.wallClockTime();
if (transactional) {
tm.begin();
}
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
testee.registerPendingPut(session, KEY1, txTimestamp);
registeredLatch.countDown();
registeredLatch.await(5, TimeUnit.SECONDS);
PutFromLoadValidator.Lock lock = testee.acquirePutFromLoadLock(session, KEY1, txTimestamp);
if (lock != null) {
try {
log.trace("Put from load lock acquired for key = " + KEY1);
success.incrementAndGet();
} finally {
testee.releasePutFromLoadLock(KEY1, lock);
}
} else {
log.trace("Unable to acquired putFromLoad lock for key = " + KEY1);
}
finishedLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
};
ExecutorService executor = Executors.newFixedThreadPool(3);
cleanup.add(() -> executor.shutdownNow());
// Start with a removal so the "isPutValid" calls will fail if
// any of the concurrent activity isn't handled properly
testee.beginInvalidatingRegion();
testee.endInvalidatingRegion();
TIME_SERVICE.advance(1);
// Do the registration + isPutValid calls
executor.execute(r);
executor.execute(r);
executor.execute(r);
assertTrue(finishedLatch.await(5, TimeUnit.SECONDS));
assertEquals("All threads succeeded", 3, success.get());
}
@Test
public void testInvalidateKeyBlocksForInProgressPut() throws Exception {
invalidationBlocksForInProgressPutTest(true);
}
@Test
public void testInvalidateRegionBlocksForInProgressPut() throws Exception {
invalidationBlocksForInProgressPutTest(false);
}
private void invalidationBlocksForInProgressPutTest(final boolean keyOnly) throws Exception {
final PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory(cm));
final CountDownLatch removeLatch = new CountDownLatch(1);
final CountDownLatch pferLatch = new CountDownLatch(1);
final AtomicReference<Object> cache = new AtomicReference<>("INITIAL");
Callable<Boolean> pferCallable = () -> {
long txTimestamp = TIME_SERVICE.wallClockTime();
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
testee.registerPendingPut(session, KEY1, txTimestamp);
PutFromLoadValidator.Lock lock = testee.acquirePutFromLoadLock(session, KEY1, txTimestamp);
if (lock != null) {
try {
removeLatch.countDown();
pferLatch.await();
cache.set("PFER");
return Boolean.TRUE;
}
finally {
testee.releasePutFromLoadLock(KEY1, lock);
}
}
return Boolean.FALSE;
};
Callable<Void> invalidateCallable = () -> {
removeLatch.await();
if (keyOnly) {
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
testee.beginInvalidatingKey(session, KEY1);
} else {
testee.beginInvalidatingRegion();
}
cache.set(null);
return null;
};
ExecutorService executor = Executors.newCachedThreadPool();
cleanup.add(() -> executor.shutdownNow());
Future<Boolean> pferFuture = executor.submit(pferCallable);
Future<Void> invalidateFuture = executor.submit(invalidateCallable);
expectException(TimeoutException.class, () -> invalidateFuture.get(1, TimeUnit.SECONDS));
pferLatch.countDown();
assertTrue(pferFuture.get(5, TimeUnit.SECONDS));
invalidateFuture.get(5, TimeUnit.SECONDS);
assertNull(cache.get());
}
protected void exec(boolean transactional, Callable<?>... callables) {
try {
if (transactional) {
for (Callable<?> c : callables) {
withTx(tm, c);
}
} else {
for (Callable<?> c : callables) {
c.call();
}
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private class Invalidation implements Callable<Void> {
private PutFromLoadValidator putFromLoadValidator;
private boolean removeRegion;
public Invalidation(PutFromLoadValidator putFromLoadValidator, boolean removeRegion) {
this.putFromLoadValidator = putFromLoadValidator;
this.removeRegion = removeRegion;
}
@Override
public Void call() throws Exception {
if (removeRegion) {
boolean success = putFromLoadValidator.beginInvalidatingRegion();
assertTrue(success);
putFromLoadValidator.endInvalidatingRegion();;
} else {
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
boolean success = putFromLoadValidator.beginInvalidatingKey(session, KEY1);
assertTrue(success);
success = putFromLoadValidator.endInvalidatingKey(session, KEY1);
assertTrue(success);
}
// if we go for the timestamp-based approach, invalidation in the same millisecond
// as the registerPendingPut/acquirePutFromLoad lock results in failure.
TIME_SERVICE.advance(1);
return null;
}
}
private class RegularPut implements Callable<Void> {
private PutFromLoadValidator putFromLoadValidator;
public RegularPut(PutFromLoadValidator putFromLoadValidator) {
this.putFromLoadValidator = putFromLoadValidator;
}
@Override
public Void call() throws Exception {
try {
long txTimestamp = TIME_SERVICE.wallClockTime(); // this should be acquired before UserTransaction.begin()
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
putFromLoadValidator.registerPendingPut(session, KEY1, txTimestamp);
PutFromLoadValidator.Lock lock = putFromLoadValidator.acquirePutFromLoadLock(session, KEY1, txTimestamp);
try {
assertNotNull(lock);
} finally {
if (lock != null) {
putFromLoadValidator.releasePutFromLoadLock(KEY1, lock);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
}
private class NakedPut implements Callable<Void> {
private final PutFromLoadValidator testee;
private final boolean expectSuccess;
public NakedPut(PutFromLoadValidator testee, boolean expectSuccess) {
this.testee = testee;
this.expectSuccess = expectSuccess;
}
@Override
public Void call() throws Exception {
try {
long txTimestamp = TIME_SERVICE.wallClockTime(); // this should be acquired before UserTransaction.begin()
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
PutFromLoadValidator.Lock lock = testee.acquirePutFromLoadLock(session, KEY1, txTimestamp);
try {
if (expectSuccess) {
assertNotNull(lock);
} else {
assertNull(lock);
}
}
finally {
if (lock != null) {
testee.releasePutFromLoadLock(KEY1, lock);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
}
@Test
@TestForIssue(jiraKey = "HHH-9928")
public void testGetForNullReleasePuts() {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.simpleCache(true).expiration().maxIdle(500);
Configuration ppCfg = cb.build();
InfinispanRegionFactory regionFactory = mock(InfinispanRegionFactory.class);
doReturn(ppCfg).when(regionFactory).getPendingPutsCacheConfiguration();
doAnswer(invocation -> TIME_SERVICE.wallClockTime()).when(regionFactory).nextTimestamp();
PutFromLoadValidator testee = new PutFromLoadValidator(cache, regionFactory, cm);
for (int i = 0; i < 100; ++i) {
try {
withTx(tm, () -> {
SharedSessionContractImplementor session = mock (SharedSessionContractImplementor.class);
testee.registerPendingPut(session, KEY1, 0);
return null;
});
TIME_SERVICE.advance(10);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
String ppName = cm.getCache().getName() + "-" + InfinispanRegionFactory.DEF_PENDING_PUTS_RESOURCE;
Map ppCache = cm.getCache(ppName, false);
assertNotNull(ppCache);
Object pendingPutMap = ppCache.get(KEY1);
assertNotNull(pendingPutMap);
int size;
try {
Method sizeMethod = pendingPutMap.getClass().getMethod("size");
sizeMethod.setAccessible(true);
size = (Integer) sizeMethod.invoke(pendingPutMap);
} catch (Exception e) {
throw new RuntimeException(e);
}
// some of the pending puts need to be expired by now
assertTrue(size < 100);
// but some are still registered
assertTrue(size > 0);
}
}