/* * 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; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.hibernate.FlushMode; import org.hibernate.LockMode; import org.hibernate.cache.infinispan.util.InfinispanMessageLogger; import org.hibernate.stat.SecondLevelCacheStatistics; 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.TestInfinispanRegionFactory; import org.hibernate.test.cache.infinispan.util.TestTimeService; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; /** * @author nikita_tovstoles@mba.berkeley.edu * @author Galder ZamarreƱo */ public class ConcurrentWriteTest extends SingleNodeTest { private static final InfinispanMessageLogger log = InfinispanMessageLogger.Provider.getLog( ConcurrentWriteTest.class ); private static final boolean trace = log.isTraceEnabled(); /** * when USER_COUNT==1, tests pass, when >4 tests fail */ private static final int USER_COUNT = 5; private static final int ITERATION_COUNT = 150; private static final int THINK_TIME_MILLIS = 10; private static final long LAUNCH_INTERVAL_MILLIS = 10; private static final Random random = new Random(); private static final TestTimeService TIME_SERVICE = new TestTimeService(); /** * kill switch used to stop all users when one fails */ private static volatile boolean TERMINATE_ALL_USERS = false; /** * collection of IDs of all customers participating in this test */ private Set<Integer> customerIDs = new HashSet<Integer>(); @Override public List<Object[]> getParameters() { return getParameters(true, true, false, true); } @Override protected void prepareTest() throws Exception { super.prepareTest(); TERMINATE_ALL_USERS = false; } @Override protected void addSettings(Map settings) { super.addSettings(settings); settings.put(TestInfinispanRegionFactory.TIME_SERVICE, TIME_SERVICE); } @Override protected void cleanupTest() throws Exception { try { super.cleanupTest(); } finally { cleanup(); } } @Test public void testPingDb() throws Exception { withTxSession(s -> s.createQuery( "from " + Customer.class.getName() ).list()); } @Test public void testSingleUser() throws Exception { // setup sessionFactory().getStatistics().clear(); // wait a while to make sure that timestamp comparison works after invalidateRegion TIME_SERVICE.advance(1); Customer customer = createCustomer( 0 ); final Integer customerId = customer.getId(); getCustomerIDs().add( customerId ); // wait a while to make sure that timestamp comparison works after collection remove (during insert) TIME_SERVICE.advance(1); assertNull( "contact exists despite not being added", getFirstContact( customerId ) ); // check that cache was hit SecondLevelCacheStatistics customerSlcs = sessionFactory() .getStatistics() .getSecondLevelCacheStatistics( Customer.class.getName() ); assertEquals( 1, customerSlcs.getPutCount() ); assertEquals( 1, customerSlcs.getElementCountInMemory() ); assertEquals( 1, customerSlcs.getEntries().size() ); log.infof( "Add contact to customer {0}", customerId ); SecondLevelCacheStatistics contactsCollectionSlcs = sessionFactory() .getStatistics() .getSecondLevelCacheStatistics( Customer.class.getName() + ".contacts" ); assertEquals( 1, contactsCollectionSlcs.getPutCount() ); assertEquals( 1, contactsCollectionSlcs.getElementCountInMemory() ); assertEquals( 1, contactsCollectionSlcs.getEntries().size() ); final Contact contact = addContact( customerId ); assertNotNull( "contact returned by addContact is null", contact ); assertEquals( "Customer.contacts cache was not invalidated after addContact", 0, contactsCollectionSlcs.getElementCountInMemory() ); assertNotNull( "Contact missing after successful add call", getFirstContact( customerId ) ); // read everyone's contacts readEveryonesFirstContact(); removeContact( customerId ); assertNull( "contact still exists after successful remove call", getFirstContact( customerId ) ); } // Ignoring the test as it's more of a stress-test: this should be enabled manually @Ignore @Test public void testManyUsers() throws Throwable { try { // setup - create users for ( int i = 0; i < USER_COUNT; i++ ) { Customer customer = createCustomer( 0 ); getCustomerIDs().add( customer.getId() ); } assertEquals( "failed to create enough Customers", USER_COUNT, getCustomerIDs().size() ); final ExecutorService executor = Executors.newFixedThreadPool( USER_COUNT ); CyclicBarrier barrier = new CyclicBarrier( USER_COUNT + 1 ); List<Future<Void>> futures = new ArrayList<Future<Void>>( USER_COUNT ); for ( Integer customerId : getCustomerIDs() ) { Future<Void> future = executor.submit( new UserRunner( customerId, barrier ) ); futures.add( future ); Thread.sleep( LAUNCH_INTERVAL_MILLIS ); // rampup } barrier.await( 2, TimeUnit.MINUTES ); // wait for all threads to finish log.info( "All threads finished, let's shutdown the executor and check whether any exceptions were reported" ); for ( Future<Void> future : futures ) { future.get(); } executor.shutdown(); log.info( "All future gets checked" ); } catch (Throwable t) { log.error( "Error running test", t ); throw t; } } public void cleanup() throws Exception { getCustomerIDs().clear(); String deleteContactHQL = "delete from Contact"; String deleteCustomerHQL = "delete from Customer"; withTxSession(s -> { s.createQuery(deleteContactHQL).setFlushMode(FlushMode.AUTO).executeUpdate(); s.createQuery(deleteCustomerHQL).setFlushMode(FlushMode.AUTO).executeUpdate(); }); } private Customer createCustomer(int nameSuffix) throws Exception { return withTxSessionApply(s -> { Customer customer = new Customer(); customer.setName( "customer_" + nameSuffix ); customer.setContacts( new HashSet<Contact>() ); s.persist( customer ); return customer; }); } /** * read first contact of every Customer participating in this test. this forces concurrent cache * writes of Customer.contacts Collection cache node * * @return who cares * @throws java.lang.Exception */ private void readEveryonesFirstContact() throws Exception { withTxSession(s -> { for ( Integer customerId : getCustomerIDs() ) { if ( TERMINATE_ALL_USERS ) { markRollbackOnly(s); return; } Customer customer = s.load( Customer.class, customerId ); Set<Contact> contacts = customer.getContacts(); if ( !contacts.isEmpty() ) { contacts.iterator().next(); } } }); } /** * -load existing Customer -get customer's contacts; return 1st one * * @param customerId * @return first Contact or null if customer has none */ private Contact getFirstContact(Integer customerId) throws Exception { assert customerId != null; return withTxSessionApply(s -> { Customer customer = s.load(Customer.class, customerId); Set<Contact> contacts = customer.getContacts(); Contact firstContact = contacts.isEmpty() ? null : contacts.iterator().next(); if (TERMINATE_ALL_USERS) { markRollbackOnly(s); } return firstContact; }); } /** * -load existing Customer -create a new Contact and add to customer's contacts * * @param customerId * @return added Contact */ private Contact addContact(Integer customerId) throws Exception { assert customerId != null; return withTxSessionApply(s -> { final Customer customer = s.load(Customer.class, customerId); Contact contact = new Contact(); contact.setName("contact name"); contact.setTlf("wtf is tlf?"); contact.setCustomer(customer); customer.getContacts().add(contact); // assuming contact is persisted via cascade from customer if (TERMINATE_ALL_USERS) { markRollbackOnly(s); } return contact; }); } /** * remove existing 'contact' from customer's list of contacts * * @param customerId * @throws IllegalStateException * if customer does not own a contact */ private void removeContact(Integer customerId) throws Exception { assert customerId != null; withTxSession(s -> { Customer customer = s.load( Customer.class, customerId ); Set<Contact> contacts = customer.getContacts(); if ( contacts.size() != 1 ) { throw new IllegalStateException( "can't remove contact: customer id=" + customerId + " expected exactly 1 contact, " + "actual count=" + contacts.size() ); } Contact contact = contacts.iterator().next(); // H2 version 1.3 (without MVCC fails with deadlock on Contacts/Customers modification, therefore, // we have to enforce locking Contacts first s.lock(contact, LockMode.PESSIMISTIC_WRITE); contacts.remove( contact ); contact.setCustomer( null ); // explicitly delete Contact because hbm has no 'DELETE_ORPHAN' cascade? // getEnvironment().getSessionFactory().getCurrentSession().delete(contact); //appears to // not be needed // assuming contact is persisted via cascade from customer if ( TERMINATE_ALL_USERS ) { markRollbackOnly(s); } }); } /** * @return the customerIDs */ public Set<Integer> getCustomerIDs() { return customerIDs; } private String statusOfRunnersToString(Set<UserRunner> runners) { assert runners != null; StringBuilder sb = new StringBuilder( "TEST CONFIG [userCount=" + USER_COUNT + ", iterationsPerUser=" + ITERATION_COUNT + ", thinkTimeMillis=" + THINK_TIME_MILLIS + "] " + " STATE of UserRunners: " ); for ( UserRunner r : runners ) { sb.append( r.toString() ).append( System.lineSeparator() ); } return sb.toString(); } class UserRunner implements Callable<Void> { private final CyclicBarrier barrier; final private Integer customerId; private int completedIterations = 0; private Throwable causeOfFailure; public UserRunner(Integer cId, CyclicBarrier barrier) { assert cId != null; this.customerId = cId; this.barrier = barrier; } private boolean contactExists() throws Exception { return getFirstContact( customerId ) != null; } public Void call() throws Exception { // name this thread for easier log tracing Thread.currentThread().setName( "UserRunnerThread-" + getCustomerId() ); log.info( "Wait for all executions paths to be ready to perform calls" ); try { for ( int i = 0; i < ITERATION_COUNT && !TERMINATE_ALL_USERS; i++ ) { contactExists(); if ( trace ) { log.trace( "Add contact for customer " + customerId ); } addContact( customerId ); if ( trace ) { log.trace( "Added contact" ); } thinkRandomTime(); contactExists(); thinkRandomTime(); if ( trace ) { log.trace( "Read all customers' first contact" ); } // read everyone's contacts readEveryonesFirstContact(); if ( trace ) { log.trace( "Read completed" ); } thinkRandomTime(); if ( trace ) { log.trace( "Remove contact of customer" + customerId ); } removeContact( customerId ); if ( trace ) { log.trace( "Removed contact" ); } contactExists(); thinkRandomTime(); ++completedIterations; if ( trace ) { log.tracef( "Iteration completed %d", completedIterations ); } } } catch (Throwable t) { TERMINATE_ALL_USERS = true; log.error( "Error", t ); throw new Exception( t ); } finally { log.info( "Wait for all execution paths to finish" ); barrier.await(); } return null; } public boolean isSuccess() { return ITERATION_COUNT == getCompletedIterations(); } public int getCompletedIterations() { return completedIterations; } public Throwable getCauseOfFailure() { return causeOfFailure; } public Integer getCustomerId() { return customerId; } @Override public String toString() { return super.toString() + "[customerId=" + getCustomerId() + " iterationsCompleted=" + getCompletedIterations() + " completedAll=" + isSuccess() + " causeOfFailure=" + (this.causeOfFailure != null ? getStackTrace( causeOfFailure ) : "") + "] "; } } public static String getStackTrace(Throwable throwable) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter( sw, true ); throwable.printStackTrace( pw ); return sw.getBuffer().toString(); } /** * sleep between 0 and THINK_TIME_MILLIS. * * @throws RuntimeException if sleep is interrupted or TERMINATE_ALL_USERS flag was set to true i n the * meantime */ private void thinkRandomTime() { try { Thread.sleep( random.nextInt( THINK_TIME_MILLIS ) ); } catch (InterruptedException ex) { throw new RuntimeException( "sleep interrupted", ex ); } if ( TERMINATE_ALL_USERS ) { throw new RuntimeException( "told to terminate (because a UserRunner had failed)" ); } } }