/*
* 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.query;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cache.infinispan.InfinispanRegionFactory;
import org.hibernate.cache.infinispan.query.QueryResultsRegionImpl;
import org.hibernate.cache.internal.StandardQueryCache;
import org.hibernate.cache.spi.CacheDataDescription;
import org.hibernate.cache.spi.QueryResultsRegion;
import org.hibernate.cache.spi.Region;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.testing.TestForIssue;
import org.hibernate.test.cache.infinispan.AbstractGeneralDataRegionTest;
import org.hibernate.test.cache.infinispan.util.CacheTestUtil;
import org.junit.Test;
import junit.framework.AssertionFailedError;
import org.infinispan.AdvancedCache;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryVisited;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryVisitedEvent;
import org.infinispan.util.concurrent.IsolationLevel;
import org.jboss.logging.Logger;
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 of QueryResultRegionImpl.
*
* @author Galder ZamarreƱo
* @since 3.5
*/
public class QueryRegionImplTest extends AbstractGeneralDataRegionTest {
private static final Logger log = Logger.getLogger( QueryRegionImplTest.class );
@Override
protected Region createRegion(
InfinispanRegionFactory regionFactory,
String regionName,
Properties properties,
CacheDataDescription cdd) {
return regionFactory.buildQueryResultsRegion( regionName, properties );
}
@Override
protected String getStandardRegionName(String regionPrefix) {
return regionPrefix + "/" + StandardQueryCache.class.getName();
}
@Override
protected AdvancedCache getInfinispanCache(InfinispanRegionFactory regionFactory) {
return regionFactory.getCacheManager().getCache( getStandardRegionName( REGION_PREFIX ) ).getAdvancedCache();
}
@Override
protected StandardServiceRegistryBuilder createStandardServiceRegistryBuilder() {
return CacheTestUtil.buildCustomQueryCacheStandardServiceRegistryBuilder( REGION_PREFIX, "replicated-query", jtaPlatform );
}
private interface RegionConsumer {
void accept(SessionFactory sessionFactory, QueryResultsRegion region) throws Exception;
}
private void withQueryRegion(RegionConsumer callable) throws Exception {
withSessionFactoriesAndRegions(1, (sessionFactories, regions) -> callable.accept(sessionFactories.get(0), (QueryResultsRegion) regions.get(0)));
}
@Test
public void testPutDoesNotBlockGet() throws Exception {
withQueryRegion((sessionFactory, region) -> {
withSession(sessionFactory, session -> region.put(session, KEY, VALUE1));
assertEquals(VALUE1, callWithSession(sessionFactory, session -> region.get(session, KEY)));
final CountDownLatch readerLatch = new CountDownLatch(1);
final CountDownLatch writerLatch = new CountDownLatch(1);
final CountDownLatch completionLatch = new CountDownLatch(1);
final ExceptionHolder holder = new ExceptionHolder();
Thread reader = new Thread() {
@Override
public void run() {
try {
assertNotEquals(VALUE2, callWithSession(sessionFactory, session -> region.get(session, KEY)));
} catch (AssertionFailedError e) {
holder.addAssertionFailure(e);
} catch (Exception e) {
holder.addException(e);
} finally {
readerLatch.countDown();
}
}
};
Thread writer = new Thread() {
@Override
public void run() {
try {
withSession(sessionFactory, session -> {
region.put(session, KEY, VALUE2);
writerLatch.await();
});
} catch (Exception e) {
holder.addException(e);
} finally {
completionLatch.countDown();
}
}
};
reader.setDaemon(true);
writer.setDaemon(true);
writer.start();
assertFalse("Writer is blocking", completionLatch.await(100, TimeUnit.MILLISECONDS));
// Start the reader
reader.start();
assertTrue("Reader finished promptly", readerLatch.await(100, TimeUnit.MILLISECONDS));
writerLatch.countDown();
assertTrue("Reader finished promptly", completionLatch.await(100, TimeUnit.MILLISECONDS));
assertEquals(VALUE2, callWithSession(sessionFactory, session -> region.get(session, KEY)));
});
}
@Test
public void testGetDoesNotBlockPut() throws Exception {
withQueryRegion((sessionFactory, region) -> {
withSession(sessionFactory, session -> region.put( session, KEY, VALUE1 ));
assertEquals(VALUE1, callWithSession(sessionFactory, session -> region.get( session, KEY )));
final AdvancedCache cache = ((QueryResultsRegionImpl) region).getCache();
final CountDownLatch blockerLatch = new CountDownLatch( 1 );
final CountDownLatch writerLatch = new CountDownLatch( 1 );
final CountDownLatch completionLatch = new CountDownLatch( 1 );
final ExceptionHolder holder = new ExceptionHolder();
Thread reader = new Thread() {
@Override
public void run() {
GetBlocker blocker = new GetBlocker( blockerLatch, KEY );
try {
cache.addListener( blocker );
withSession(sessionFactory, session -> region.get(session, KEY ));
}
catch (Exception e) {
holder.addException(e);
}
finally {
cache.removeListener( blocker );
}
}
};
Thread writer = new Thread() {
@Override
public void run() {
try {
writerLatch.await();
withSession(sessionFactory, session -> region.put( session, KEY, VALUE2 ));
}
catch (Exception e) {
holder.addException(e);
}
finally {
completionLatch.countDown();
}
}
};
reader.setDaemon( true );
writer.setDaemon( true );
boolean unblocked = false;
try {
reader.start();
writer.start();
assertFalse( "Reader is blocking", completionLatch.await( 100, TimeUnit.MILLISECONDS ) );
// Start the writer
writerLatch.countDown();
assertTrue( "Writer finished promptly", completionLatch.await( 100, TimeUnit.MILLISECONDS ) );
blockerLatch.countDown();
unblocked = true;
if ( IsolationLevel.REPEATABLE_READ.equals( cache.getCacheConfiguration().locking().isolationLevel() ) ) {
assertEquals( VALUE1, callWithSession(sessionFactory, session -> region.get( session, KEY )) );
}
else {
assertEquals( VALUE2, callWithSession(sessionFactory, session -> region.get( session, KEY )) );
}
holder.checkExceptions();
}
finally {
if ( !unblocked ) {
blockerLatch.countDown();
}
}
});
}
protected interface SessionConsumer {
void accept(SharedSessionContractImplementor session) throws Exception;
}
protected interface SessionCallable<T> {
T call(SharedSessionContractImplementor session) throws Exception;
}
protected <T> T callWithSession(SessionFactory sessionFactory, SessionCallable<T> callable) throws Exception {
Session session = sessionFactory.openSession();
Transaction tx = session.getTransaction();
tx.begin();
try {
T retval = callable.call((SharedSessionContractImplementor) session);
tx.commit();
return retval;
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
session.close();
}
}
protected void withSession(SessionFactory sessionFactory, SessionConsumer consumer) throws Exception {
callWithSession(sessionFactory, session -> { consumer.accept(session); return null;} );
}
@Test
@TestForIssue(jiraKey = "HHH-7898")
public void testPutDuringPut() throws Exception {
withQueryRegion((sessionFactory, region) -> {
withSession(sessionFactory, session -> region.put(session, KEY, VALUE1));
assertEquals(VALUE1, callWithSession(sessionFactory, session -> region.get(session, KEY) ));
final AdvancedCache cache = ((QueryResultsRegionImpl) region).getCache();
CountDownLatch blockerLatch = new CountDownLatch(1);
CountDownLatch triggerLatch = new CountDownLatch(1);
ExceptionHolder holder = new ExceptionHolder();
Thread blocking = new Thread() {
@Override
public void run() {
PutBlocker blocker = null;
try {
blocker = new PutBlocker(blockerLatch, triggerLatch, KEY);
cache.addListener(blocker);
withSession(sessionFactory, session -> region.put(session, KEY, VALUE2));
} catch (Exception e) {
holder.addException(e);
} finally {
if (blocker != null) {
cache.removeListener(blocker);
}
if (triggerLatch.getCount() > 0) {
triggerLatch.countDown();
}
}
}
};
Thread blocked = new Thread() {
@Override
public void run() {
try {
triggerLatch.await();
// this should silently fail
withSession(sessionFactory, session -> region.put(session, KEY, VALUE3));
} catch (Exception e) {
holder.addException(e);
}
}
};
blocking.setName("blocking-thread");
blocking.start();
blocked.setName("blocked-thread");
blocked.start();
blocked.join();
blockerLatch.countDown();
blocking.join();
holder.checkExceptions();
assertEquals(VALUE2, callWithSession(sessionFactory, session -> region.get(session, KEY)));
});
}
@Test
public void testQueryUpdate() throws Exception {
withQueryRegion((sessionFactory, region) -> {
ExceptionHolder holder = new ExceptionHolder();
CyclicBarrier barrier = new CyclicBarrier(2);
withSession(sessionFactory, session -> region.put(session, KEY, VALUE1));
Thread updater = new Thread() {
@Override
public void run() {
try {
withSession(sessionFactory, (session) -> {
assertEquals(VALUE1, region.get(session, KEY));
region.put(session, KEY, VALUE2);
assertEquals(VALUE2, region.get(session, KEY));
barrier.await(5, TimeUnit.SECONDS);
barrier.await(5, TimeUnit.SECONDS);
region.put(session, KEY, VALUE3);
assertEquals(VALUE3, region.get(session, KEY));
barrier.await(5, TimeUnit.SECONDS);
barrier.await(5, TimeUnit.SECONDS);
});
} catch (AssertionFailedError e) {
holder.addAssertionFailure(e);
barrier.reset();
} catch (Exception e) {
holder.addException(e);
barrier.reset();
}
}
};
Thread reader = new Thread() {
@Override
public void run() {
try {
withSession(sessionFactory, (session) -> {
assertEquals(VALUE1, region.get(session, KEY));
barrier.await(5, TimeUnit.SECONDS);
assertEquals(VALUE1, region.get(session, KEY));
barrier.await(5, TimeUnit.SECONDS);
barrier.await(5, TimeUnit.SECONDS);
assertEquals(VALUE1, region.get(session, KEY));
barrier.await(5, TimeUnit.SECONDS);
});
} catch (AssertionFailedError e) {
holder.addAssertionFailure(e);
barrier.reset();
} catch (Exception e) {
holder.addException(e);
barrier.reset();
}
}
};
updater.start();
reader.start();
updater.join();
reader.join();
holder.checkExceptions();
assertEquals(VALUE3, callWithSession(sessionFactory, session -> region.get(session, KEY)));
});
}
@Test
@TestForIssue(jiraKey = "HHH-10163")
public void testEvictAll() throws Exception {
withQueryRegion((sessionFactory, region) -> {
withSession(sessionFactory, s -> region.put(s, KEY, VALUE1));
withSession(sessionFactory, s -> assertEquals(VALUE1, region.get(s, KEY)));
region.evictAll();
withSession(sessionFactory, s -> assertNull(region.get(s, KEY)));
assertEquals(Collections.EMPTY_MAP, region.toMap());
});
}
@Listener
public class GetBlocker {
private final CountDownLatch latch;
private final Object key;
GetBlocker(CountDownLatch latch, Object key) {
this.latch = latch;
this.key = key;
}
@CacheEntryVisited
public void nodeVisisted(CacheEntryVisitedEvent event) {
if ( event.isPre() && event.getKey().equals( key ) ) {
try {
latch.await();
}
catch (InterruptedException e) {
log.error( "Interrupted waiting for latch", e );
}
}
}
}
@Listener
public class PutBlocker {
private final CountDownLatch blockLatch, triggerLatch;
private final Object key;
private boolean enabled = true;
PutBlocker(CountDownLatch blockLatch, CountDownLatch triggerLatch, Object key) {
this.blockLatch = blockLatch;
this.triggerLatch = triggerLatch;
this.key = key;
}
@CacheEntryModified
public void nodeVisisted(CacheEntryModifiedEvent event) {
// we need isPre since lock is acquired in the commit phase
if ( !event.isPre() && event.getKey().equals( key ) ) {
try {
synchronized (this) {
if (enabled) {
triggerLatch.countDown();
enabled = false;
blockLatch.await();
}
}
}
catch (InterruptedException e) {
log.error( "Interrupted waiting for latch", e );
}
}
}
}
private class ExceptionHolder {
private final List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
private final List<AssertionFailedError> assertionFailures = Collections.synchronizedList(new ArrayList<>());
public void addException(Exception e) {
exceptions.add(e);
}
public void addAssertionFailure(AssertionFailedError e) {
assertionFailures.add(e);
}
public void checkExceptions() throws Exception {
for (AssertionFailedError a : assertionFailures) {
throw a;
}
for (Exception e : exceptions) {
throw e;
}
}
}
}