/*
* 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.functional.cluster;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cache.infinispan.InfinispanRegionFactory;
import org.hibernate.cache.infinispan.access.PutFromLoadValidator;
import org.hibernate.cache.infinispan.util.FutureUpdate;
import org.hibernate.cache.infinispan.util.InfinispanMessageLogger;
import org.hibernate.cache.spi.access.AccessType;
import org.hibernate.test.cache.infinispan.functional.entities.Contact;
import org.hibernate.test.cache.infinispan.functional.entities.Customer;
import org.hibernate.test.cache.infinispan.util.ExpectingInterceptor;
import org.hibernate.test.cache.infinispan.util.TestInfinispanRegionFactory;
import org.hibernate.testing.TestForIssue;
import org.infinispan.AdvancedCache;
import org.infinispan.Cache;
import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commons.util.Util;
import org.infinispan.context.InvocationContext;
import org.infinispan.interceptors.base.BaseCustomInterceptor;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryVisited;
import org.infinispan.notifications.cachelistener.event.CacheEntryVisitedEvent;
import org.jboss.util.collection.ConcurrentSet;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
/**
* EntityCollectionInvalidationTestCase.
*
* @author Galder ZamarreƱo
* @since 3.5
*/
public class EntityCollectionInvalidationTest extends DualNodeTest {
private static final InfinispanMessageLogger log = InfinispanMessageLogger.Provider.getLog( EntityCollectionInvalidationTest.class );
private static final Integer CUSTOMER_ID = new Integer( 1 );
private EmbeddedCacheManager localManager, remoteManager;
private AdvancedCache localCustomerCache, remoteCustomerCache;
private AdvancedCache localContactCache, remoteContactCache;
private AdvancedCache localCollectionCache, remoteCollectionCache;
private MyListener localListener, remoteListener;
private SessionFactory localFactory, remoteFactory;
@Override
public List<Object[]> getParameters() {
return getParameters(true, true, false, true);
}
@Override
public void startUp() {
super.startUp();
// Bind a listener to the "local" cache
// Our region factory makes its CacheManager available to us
localManager = ClusterAwareRegionFactory.getCacheManager( DualNodeTest.LOCAL );
// Cache localCache = localManager.getCache("entity");
localCustomerCache = localManager.getCache( Customer.class.getName() ).getAdvancedCache();
localContactCache = localManager.getCache( Contact.class.getName() ).getAdvancedCache();
localCollectionCache = localManager.getCache( Customer.class.getName() + ".contacts" ).getAdvancedCache();
localListener = new MyListener( "local" );
localCustomerCache.addListener( localListener );
localContactCache.addListener( localListener );
localCollectionCache.addListener( localListener );
// Bind a listener to the "remote" cache
remoteManager = ClusterAwareRegionFactory.getCacheManager( DualNodeTest.REMOTE );
remoteCustomerCache = remoteManager.getCache( Customer.class.getName() ).getAdvancedCache();
remoteContactCache = remoteManager.getCache( Contact.class.getName() ).getAdvancedCache();
remoteCollectionCache = remoteManager.getCache( Customer.class.getName() + ".contacts" ).getAdvancedCache();
remoteListener = new MyListener( "remote" );
remoteCustomerCache.addListener( remoteListener );
remoteContactCache.addListener( remoteListener );
remoteCollectionCache.addListener( remoteListener );
localFactory = sessionFactory();
remoteFactory = secondNodeEnvironment().getSessionFactory();
}
@Override
public void shutDown() {
cleanupTransactionManagement();
}
@Override
protected void cleanupTest() throws Exception {
cleanup(localFactory);
localListener.clear();
remoteListener.clear();
// do not call super.cleanupTest becasue we would clean transaction managers
}
@Override
protected void addSettings(Map settings) {
super.addSettings(settings);
settings.put(TestInfinispanRegionFactory.PENDING_PUTS_SIMPLE, false);
}
@Test
public void testAll() throws Exception {
assertEmptyCaches();
assertTrue( remoteListener.isEmpty() );
assertTrue( localListener.isEmpty() );
log.debug( "Create node 0" );
IdContainer ids = createCustomer( localFactory );
assertTrue( remoteListener.isEmpty() );
assertTrue( localListener.isEmpty() );
log.debug( "Find node 0" );
// This actually brings the collection into the cache
getCustomer( ids.customerId, localFactory );
// Now the collection is in the cache so, the 2nd "get"
// should read everything from the cache
log.debug( "Find(2) node 0" );
localListener.clear();
getCustomer( ids.customerId, localFactory );
// Check the read came from the cache
log.debug( "Check cache 0" );
assertLoadedFromCache( localListener, ids.customerId, ids.contactIds );
log.debug( "Find node 1" );
// This actually brings the collection into the cache since invalidation is in use
getCustomer( ids.customerId, remoteFactory );
// Now the collection is in the cache so, the 2nd "get"
// should read everything from the cache
log.debug( "Find(2) node 1" );
remoteListener.clear();
getCustomer( ids.customerId, remoteFactory );
// Check the read came from the cache
log.debug( "Check cache 1" );
assertLoadedFromCache( remoteListener, ids.customerId, ids.contactIds );
// Modify customer in remote
remoteListener.clear();
CountDownLatch modifyLatch = null;
if (!cacheMode.isInvalidation() && accessType != AccessType.NONSTRICT_READ_WRITE) {
modifyLatch = new CountDownLatch(1);
ExpectingInterceptor.get(localCustomerCache).when(this::isFutureUpdate).countDown(modifyLatch);
}
ids = modifyCustomer( ids.customerId, remoteFactory );
assertLoadedFromCache( remoteListener, ids.customerId, ids.contactIds );
if (modifyLatch != null) {
assertTrue(modifyLatch.await(2, TimeUnit.SECONDS));
ExpectingInterceptor.cleanup(localCustomerCache);
}
assertEquals( 0, localCollectionCache.size() );
if (cacheMode.isInvalidation()) {
// After modification, local cache should have been invalidated and hence should be empty
assertEquals(0, localCustomerCache.size());
} else {
// Replicated cache is updated, not invalidated
assertEquals(1, localCustomerCache.size());
}
}
@TestForIssue(jiraKey = "HHH-9881")
@Test
public void testConcurrentLoadAndRemoval() throws Exception {
if (!remoteCustomerCache.getCacheConfiguration().clustering().cacheMode().isInvalidation()) {
// This test is tailored for invalidation-based strategies, using pending puts cache
return;
}
AtomicReference<Exception> getException = new AtomicReference<>();
AtomicReference<Exception> deleteException = new AtomicReference<>();
Phaser getPhaser = new Phaser(2);
HookInterceptor hookInterceptor = new HookInterceptor(getException);
AdvancedCache remotePPCache = remoteCustomerCache.getCacheManager().getCache(
remoteCustomerCache.getName() + "-" + InfinispanRegionFactory.DEF_PENDING_PUTS_RESOURCE).getAdvancedCache();
remotePPCache.getAdvancedCache().addInterceptor(hookInterceptor, 0);
IdContainer idContainer = new IdContainer();
withTxSession(localFactory, s -> {
Customer customer = new Customer();
customer.setName( "JBoss" );
s.persist(customer);
idContainer.customerId = customer.getId();
});
// start loading
Thread getThread = new Thread(() -> {
try {
withTxSession(remoteFactory, s -> {
s.get(Customer.class, idContainer.customerId);
});
} catch (Exception e) {
log.error("Failure to get customer", e);
getException.set(e);
}
}, "get-thread");
Thread deleteThread = new Thread(() -> {
try {
withTxSession(localFactory, s -> {
Customer customer = s.get(Customer.class, idContainer.customerId);
s.delete(customer);
});
} catch (Exception e) {
log.error("Failure to delete customer", e);
deleteException.set(e);
}
}, "delete-thread");
// get thread should block on the beginning of PutFromLoadValidator#acquirePutFromLoadLock
hookInterceptor.block(getPhaser, getThread);
getThread.start();
arriveAndAwait(getPhaser);
deleteThread.start();
deleteThread.join();
hookInterceptor.unblock();
arriveAndAwait(getPhaser);
getThread.join();
if (getException.get() != null) {
throw new IllegalStateException("get-thread failed", getException.get());
}
if (deleteException.get() != null) {
throw new IllegalStateException("delete-thread failed", deleteException.get());
}
Customer localCustomer = getCustomer(idContainer.customerId, localFactory);
assertNull(localCustomer);
Customer remoteCustomer = getCustomer(idContainer.customerId, remoteFactory);
assertNull(remoteCustomer);
assertTrue(remoteCustomerCache.isEmpty());
}
protected void assertEmptyCaches() {
assertTrue( localCustomerCache.isEmpty() );
assertTrue( localContactCache.isEmpty() );
assertTrue( localCollectionCache.isEmpty() );
assertTrue( remoteCustomerCache.isEmpty() );
assertTrue( remoteContactCache.isEmpty() );
assertTrue( remoteCollectionCache.isEmpty() );
}
private IdContainer createCustomer(SessionFactory sessionFactory)
throws Exception {
log.debug( "CREATE CUSTOMER" );
Customer customer = new Customer();
customer.setName("JBoss");
Set<Contact> contacts = new HashSet<Contact>();
Contact kabir = new Contact();
kabir.setCustomer(customer);
kabir.setName("Kabir");
kabir.setTlf("1111");
contacts.add(kabir);
Contact bill = new Contact();
bill.setCustomer(customer);
bill.setName("Bill");
bill.setTlf("2222");
contacts.add(bill);
customer.setContacts(contacts);
ArrayList<Runnable> cleanup = new ArrayList<>();
CountDownLatch customerLatch = new CountDownLatch(1);
CountDownLatch collectionLatch = new CountDownLatch(1);
CountDownLatch contactsLatch = new CountDownLatch(2);
if (cacheMode.isInvalidation()) {
cleanup.add(mockValidator(remoteCustomerCache, customerLatch));
cleanup.add(mockValidator(remoteCollectionCache, collectionLatch));
cleanup.add(mockValidator(remoteContactCache, contactsLatch));
} else if (accessType == AccessType.NONSTRICT_READ_WRITE) {
// ATM nonstrict mode has sync after-invalidation update
Stream.of(customerLatch, collectionLatch, contactsLatch, contactsLatch).forEach(l -> l.countDown());
} else {
ExpectingInterceptor.get(remoteCustomerCache).when(this::isFutureUpdate).countDown(collectionLatch);
ExpectingInterceptor.get(remoteCollectionCache).when(this::isFutureUpdate).countDown(customerLatch);
ExpectingInterceptor.get(remoteContactCache).when(this::isFutureUpdate).countDown(contactsLatch);
cleanup.add(() -> ExpectingInterceptor.cleanup(remoteCustomerCache, remoteCollectionCache, remoteContactCache));
}
withTxSession(sessionFactory, session -> session.save(customer));
assertTrue(customerLatch.await(2, TimeUnit.SECONDS));
assertTrue(collectionLatch.await(2, TimeUnit.SECONDS));
assertTrue(contactsLatch.await(2, TimeUnit.SECONDS));
cleanup.forEach(Runnable::run);
IdContainer ids = new IdContainer();
ids.customerId = customer.getId();
Set contactIds = new HashSet();
contactIds.add( kabir.getId() );
contactIds.add( bill.getId() );
ids.contactIds = contactIds;
log.debug( "CREATE CUSTOMER - END" );
return ids;
}
private boolean isFutureUpdate(InvocationContext ctx, VisitableCommand cmd) {
return cmd instanceof PutKeyValueCommand && ((PutKeyValueCommand) cmd).getValue() instanceof FutureUpdate;
}
private Runnable mockValidator(AdvancedCache cache, CountDownLatch latch) {
PutFromLoadValidator originalValidator = PutFromLoadValidator.removeFromCache(cache);
PutFromLoadValidator mockValidator = spy(originalValidator);
doAnswer(invocation -> {
try {
return invocation.callRealMethod();
} finally {
latch.countDown();
}
}).when(mockValidator).endInvalidatingKey(any(), any());
PutFromLoadValidator.addToCache(cache, mockValidator);
return () -> {
PutFromLoadValidator.removeFromCache(cache);
PutFromLoadValidator.addToCache(cache, originalValidator);
};
}
private Customer getCustomer(Integer id, SessionFactory sessionFactory) throws Exception {
log.debug( "Find customer with id=" + id );
return withTxSessionApply(sessionFactory, session -> doGetCustomer(id, session));
}
private Customer doGetCustomer(Integer id, Session session) throws Exception {
Customer customer = session.get( Customer.class, id );
if (customer == null) {
return null;
}
// Access all the contacts
Set<Contact> contacts = customer.getContacts();
if (contacts != null) {
for (Iterator it = contacts.iterator(); it.hasNext(); ) {
((Contact) it.next()).getName();
}
}
return customer;
}
private IdContainer modifyCustomer(Integer id, SessionFactory sessionFactory)
throws Exception {
log.debug( "Modify customer with id=" + id );
return withTxSessionApply(sessionFactory, session -> {
IdContainer ids = new IdContainer();
Set contactIds = new HashSet();
Customer customer = doGetCustomer( id, session );
customer.setName( "NewJBoss" );
ids.customerId = customer.getId();
Set<Contact> contacts = customer.getContacts();
for ( Contact c : contacts ) {
contactIds.add( c.getId() );
}
Contact contact = contacts.iterator().next();
contacts.remove( contact );
contactIds.remove( contact.getId() );
ids.contactIds = contactIds;
contact.setCustomer( null );
session.save( customer );
return ids;
});
}
private void cleanup(SessionFactory sessionFactory) throws Exception {
withTxSession(sessionFactory, session -> {
Customer c = (Customer) session.get(Customer.class, CUSTOMER_ID);
if (c != null) {
Set contacts = c.getContacts();
for (Iterator it = contacts.iterator(); it.hasNext(); ) {
session.delete(it.next());
}
c.setContacts(null);
session.delete(c);
}
// since we don't use orphan removal, some contacts may persist
for (Object contact : session.createCriteria(Contact.class).list()) {
session.delete(contact);
}
});
}
private void assertLoadedFromCache(MyListener listener, Integer custId, Set contactIds) {
assertTrue(
"Customer#" + custId + " was in cache", listener.visited.contains(
"Customer#"
+ custId
)
);
for ( Iterator it = contactIds.iterator(); it.hasNext(); ) {
Integer contactId = (Integer) it.next();
assertTrue(
"Contact#" + contactId + " was in cache", listener.visited.contains(
"Contact#"
+ contactId
)
);
assertTrue(
"Contact#" + contactId + " was in cache", listener.visited.contains(
"Contact#"
+ contactId
)
);
}
assertTrue(
"Customer.contacts" + custId + " was in cache", listener.visited
.contains( "Customer.contacts#" + custId )
);
}
protected static void arriveAndAwait(Phaser phaser) throws TimeoutException, InterruptedException {
try {
phaser.awaitAdvanceInterruptibly(phaser.arrive(), 10, TimeUnit.SECONDS);
} catch (TimeoutException e) {
log.error("Failed to progress: " + Util.threadDump());
throw e;
}
}
@Listener
public static class MyListener {
private static final InfinispanMessageLogger log = InfinispanMessageLogger.Provider.getLog( MyListener.class );
private Set<String> visited = new ConcurrentSet<String>();
private final String name;
public MyListener(String name) {
this.name = name;
}
public void clear() {
visited.clear();
}
public boolean isEmpty() {
return visited.isEmpty();
}
@CacheEntryVisited
public void nodeVisited(CacheEntryVisitedEvent event) {
log.debug( event.toString() );
if ( !event.isPre() ) {
String key = event.getCache().getName() + "#" + event.getKey();
log.debug( "MyListener[" + name + "] - Visiting key " + key );
// String name = fqn.toString();
String token = ".entities.";
int index = key.indexOf( token );
if ( index > -1 ) {
index += token.length();
key = key.substring( index );
log.debug( "MyListener[" + this.name + "] - recording visit to " + key );
visited.add( key );
}
}
}
}
private class IdContainer {
Integer customerId;
Set<Integer> contactIds;
}
private static class HookInterceptor extends BaseCustomInterceptor {
final AtomicReference<Exception> failure;
Phaser phaser;
Thread thread;
private HookInterceptor(AtomicReference<Exception> failure) {
this.failure = failure;
}
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 {
try {
Phaser phaser;
Thread thread;
synchronized (this) {
phaser = this.phaser;
thread = this.thread;
}
if (phaser != null && Thread.currentThread() == thread) {
arriveAndAwait(phaser);
arriveAndAwait(phaser);
}
} catch (Exception e) {
failure.set(e);
throw e;
} finally {
return super.visitGetKeyValueCommand(ctx, command);
}
}
}
}