/* * 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.stress; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import javax.transaction.TransactionManager; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cache.infinispan.InfinispanRegionFactory; import org.hibernate.cfg.Environment; import org.hibernate.mapping.Collection; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.RootClass; import org.hibernate.test.cache.infinispan.stress.entities.Address; import org.hibernate.test.cache.infinispan.stress.entities.Family; import org.hibernate.test.cache.infinispan.stress.entities.Person; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.infinispan.util.concurrent.ConcurrentHashSet; import static org.infinispan.test.TestingUtil.withTx; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * Stress test for second level cache. * * TODO Various: * - Switch to a JDBC connection pool to avoid too many connections created * (as well as consuming memory, it's expensive to create) * - Use barrier associated execution tasks at the beginning and end to track * down start/end times for runs. * * @author Galder Zamarreño * @since 4.1 */ @Ignore public class SecondLevelCacheStressTestCase { static final int NUM_THREADS = 10; static final long WARMUP_TIME = TimeUnit.SECONDS.toNanos(Integer.getInteger("warmup-time", 1) * 5); static final long RUNNING_TIME = TimeUnit.SECONDS.toNanos(Integer.getInteger("time", 1) * 60); static final boolean PROFILE = Boolean.getBoolean("profile"); static final boolean ALLOCATION = Boolean.getBoolean("allocation"); static final int RUN_COUNT_LIMIT = Integer.getInteger("count", 1000); // max number of runs per operation static final Random RANDOM = new Random(12345); String provider; ConcurrentHashSet<Integer> updatedIds; Queue<Integer> removeIds; SessionFactory sessionFactory; TransactionManager tm; volatile int numEntities; @Before public void beforeClass() { provider = getProvider(); updatedIds = new ConcurrentHashSet<Integer>(); removeIds = new ConcurrentLinkedQueue<Integer>(); StandardServiceRegistryBuilder ssrb = new StandardServiceRegistryBuilder().enableAutoClose() .applySetting( Environment.USE_SECOND_LEVEL_CACHE, "true" ) .applySetting( Environment.USE_QUERY_CACHE, "true" ) .applySetting( Environment.DRIVER, "com.mysql.jdbc.Driver" ) .applySetting( Environment.URL, "jdbc:mysql://localhost:3306/hibernate" ) .applySetting( Environment.DIALECT, "org.hibernate.dialect.MySQL5InnoDBDialect" ) .applySetting( Environment.USER, "root" ) .applySetting( Environment.PASS, "password" ) .applySetting( Environment.HBM2DDL_AUTO, "create-drop" ); // Create database schema in each run applyCacheSettings( ssrb ); StandardServiceRegistry registry = ssrb.build(); Metadata metadata = buildMetadata( registry ); sessionFactory = metadata.buildSessionFactory(); tm = com.arjuna.ats.jta.TransactionManager.transactionManager(); } protected String getProvider() { return "infinispan"; } protected void applyCacheSettings(StandardServiceRegistryBuilder ssrb) { ssrb.applySetting( Environment.CACHE_REGION_FACTORY, "org.hibernate.cache.infinispan.InfinispanRegionFactory" ); ssrb.applySetting( Environment.JTA_PLATFORM, "org.hibernate.service.jta.platform.internal.JBossStandAloneJtaPlatform" ); ssrb.applySetting( InfinispanRegionFactory.INFINISPAN_CONFIG_RESOURCE_PROP, "stress-local-infinispan.xml" ); } @After public void afterClass() { sessionFactory.close(); } @Test public void testEntityLifecycle() throws InterruptedException { if (!PROFILE) { System.out.printf("[provider=%s] Warming up\n", provider); doEntityLifecycle(true); // Recreate session factory cleaning everything afterClass(); beforeClass(); } System.out.printf("[provider=%s] Testing...\n", provider); doEntityLifecycle(false); } void doEntityLifecycle(boolean isWarmup) { long runningTimeout = isWarmup ? WARMUP_TIME : RUNNING_TIME; TotalStats insertPerf = runEntityInsert(runningTimeout); numEntities = countEntities().intValue(); printResult(isWarmup, "[provider=%s] Inserts/s %10.2f (%d entities)\n", provider, insertPerf.getOpsPerSec("INSERT"), numEntities); TotalStats updatePerf = runEntityUpdate(runningTimeout); List<Integer> updateIdsSeq = new ArrayList<Integer>(updatedIds); printResult(isWarmup, "[provider=%s] Updates/s %10.2f (%d updates)\n", provider, updatePerf.getOpsPerSec("UPDATE"), updateIdsSeq.size()); TotalStats findUpdatedPerf = runEntityFindUpdated(runningTimeout, updateIdsSeq); printResult(isWarmup, "[provider=%s] Updated finds/s %10.2f\n", provider, findUpdatedPerf.getOpsPerSec("FIND_UPDATED")); TotalStats findQueryPerf = runEntityFindQuery(runningTimeout, isWarmup); printResult(isWarmup, "[provider=%s] Query finds/s %10.2f\n", provider, findQueryPerf.getOpsPerSec("FIND_QUERY")); TotalStats findRandomPerf = runEntityFindRandom(runningTimeout); printResult(isWarmup, "[provider=%s] Random finds/s %10.2f\n", provider, findRandomPerf.getOpsPerSec("FIND_RANDOM")); // Get all entity ids List<Integer> entityIds = new ArrayList<Integer>(); for (int i = 1; i <= numEntities; i++) entityIds.add(i); // Shuffle them Collections.shuffle(entityIds); // Add them to the queue delete consumption removeIds.addAll(entityIds); TotalStats deletePerf = runEntityDelete(runningTimeout); printResult(isWarmup, "[provider=%s] Deletes/s %10.2f\n", provider, deletePerf.getOpsPerSec("DELETE")); // TODO Print 2LC statistics... } static void printResult(boolean isWarmup, String format, Object... args) { if (!isWarmup) System.out.printf(format, args); } Long countEntities() { try { return withTx(tm, new Callable<Long>() { @Override public Long call() throws Exception { Session s = sessionFactory.openSession(); Query query = s.createQuery("select count(*) from Family"); Object result = query.list().get(0); s.close(); return (Long) result; } }); } catch (Exception e) { throw new RuntimeException(e); } } TotalStats runEntityInsert(long runningTimeout) { return runSingleWork(runningTimeout, "insert", insertOperation()); } TotalStats runEntityUpdate(long runningTimeout) { return runSingleWork(runningTimeout, "update", updateOperation()); } TotalStats runEntityFindUpdated(long runningTimeout, List<Integer> updatedIdsSeq) { return runSingleWork(runningTimeout, "find-updated", findUpdatedOperation(updatedIdsSeq)); } TotalStats runEntityFindQuery(long runningTimeout, boolean warmup) { return runSingleWork(runningTimeout, "find-query", findQueryOperation(warmup)); } TotalStats runEntityFindRandom(long runningTimeout) { return runSingleWork(runningTimeout, "find-random", findRandomOperation()); } TotalStats runEntityDelete(long runningTimeout) { return runSingleWork(runningTimeout, "remove", deleteOperation()); } TotalStats runSingleWork(long runningTimeout, final String name, Operation op) { final TotalStats perf = new TotalStats(); ExecutorService exec = Executors.newFixedThreadPool( NUM_THREADS, new ThreadFactory() { volatile int i = 0; @Override public Thread newThread(Runnable r) { return new Thread(new ThreadGroup(name), r, "worker-" + name + "-" + i++); } }); try { List<Future<Void>> futures = new ArrayList<Future<Void>>(NUM_THREADS); CyclicBarrier barrier = new CyclicBarrier(NUM_THREADS + 1); for (int i = 0; i < NUM_THREADS; i++) futures.add(exec.submit( new WorkerThread(runningTimeout, perf, op, barrier))); try { barrier.await(); // wait for all threads to be ready barrier.await(); // wait for all threads to finish // Now check whether anything went wrong... for (Future<Void> future : futures) future.get(); } catch (Exception e) { throw new RuntimeException(e); } return perf; } finally { exec.shutdown(); } } <T> T captureThrowables(Callable<T> task) throws Exception { try { return task.call(); } catch (Throwable t) { t.printStackTrace(); if (t instanceof Exception) throw (Exception) t; else throw new RuntimeException(t); } } Operation insertOperation() { return new Operation("INSERT") { @Override boolean call(final int run) throws Exception { return captureThrowables(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return withTx(tm, new Callable<Boolean>() { @Override public Boolean call() throws Exception { Session s = sessionFactory.openSession(); s.getTransaction().begin(); String name = "Zamarreño-" + run; Family family = new Family(name); s.persist(family); s.getTransaction().commit(); s.close(); return true; } }); } }); } }; } Operation updateOperation() { return new Operation("UPDATE") { @Override boolean call(final int run) throws Exception { return captureThrowables(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return withTx(tm, new Callable<Boolean>() { @Override public Boolean call() throws Exception { Session s = sessionFactory.openSession(); s.getTransaction().begin(); // Update random entity that has been inserted int id = RANDOM.nextInt(numEntities) + 1; Family family = (Family) s.load(Family.class, id); String newSecondName = "Arrizabalaga-" + run; family.setSecondName(newSecondName); s.getTransaction().commit(); s.close(); // Cache updated entities for later read updatedIds.add(id); return true; } }); } }); } }; } Operation findUpdatedOperation(final List<Integer> updatedIdsSeq) { return new Operation("FIND_UPDATED") { @Override boolean call(final int run) throws Exception { return captureThrowables(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Session s = sessionFactory.openSession(); int id = updatedIdsSeq.get(RANDOM.nextInt( updatedIdsSeq.size())); Family family = (Family) s.load(Family.class, id); String secondName = family.getSecondName(); assertNotNull(secondName); assertTrue("Second name not expected: " + secondName, secondName.startsWith("Arrizabalaga")); s.close(); return true; } }); } }; } private Operation findQueryOperation(final boolean isWarmup) { return new Operation("FIND_QUERY") { @Override boolean call(final int run) throws Exception { return captureThrowables(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Session s = sessionFactory.openSession(); Query query = s.createQuery("from Family") .setCacheable(true); int maxResults = isWarmup ? 10 : 100; query.setMaxResults(maxResults); List<Family> result = (List<Family>) query.list(); assertEquals(maxResults, result.size()); s.close(); return true; } }); } }; } private Operation findRandomOperation() { return new Operation("FIND_RANDOM") { @Override boolean call(final int run) throws Exception { return captureThrowables(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Session s = sessionFactory.openSession(); int id = RANDOM.nextInt(numEntities) + 1; Family family = (Family) s.load(Family.class, id); String familyName = family.getName(); // Skip ñ check in order to avoid issues... assertTrue("Unexpected family: " + familyName , familyName.startsWith("Zamarre")); s.close(); return true; } }); } }; } private Operation deleteOperation() { return new Operation("DELETE") { @Override boolean call(final int run) throws Exception { return captureThrowables(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return withTx(tm, new Callable<Boolean>() { @Override public Boolean call() throws Exception { Session s = sessionFactory.openSession(); s.getTransaction().begin(); // Get each id and remove it int id = removeIds.poll(); Family family = (Family) s.load(Family.class, id); String familyName = family.getName(); // Skip ñ check in order to avoid issues... assertTrue("Unexpected family: " + familyName , familyName.startsWith("Zamarre")); s.delete(family); s.getTransaction().commit(); s.close(); return true; } }); } }); } }; } public static Class[] getAnnotatedClasses() { return new Class[] {Family.class, Person.class, Address.class}; } private static Metadata buildMetadata(StandardServiceRegistry registry) { final String cacheStrategy = "transactional"; MetadataSources metadataSources = new MetadataSources( registry ); for ( Class entityClass : getAnnotatedClasses() ) { metadataSources.addAnnotatedClass( entityClass ); } Metadata metadata = metadataSources.buildMetadata(); for ( PersistentClass entityBinding : metadata.getEntityBindings() ) { if (!entityBinding.isInherited()) { ( (RootClass) entityBinding ).setCacheConcurrencyStrategy( cacheStrategy); } } for ( Collection collectionBinding : metadata.getCollectionBindings() ) { collectionBinding.setCacheConcurrencyStrategy( cacheStrategy ); } return metadata; } private static abstract class Operation { final String name; Operation(String name) { this.name = name; } abstract boolean call(int run) throws Exception; } private class WorkerThread implements Callable<Void> { private final long runningTimeout; private final TotalStats perf; private final Operation op; private final CyclicBarrier barrier; public WorkerThread(long runningTimeout, TotalStats perf, Operation op, CyclicBarrier barrier) { this.runningTimeout = runningTimeout; this.perf = perf; this.op = op; this.barrier = barrier; } @Override public Void call() throws Exception { // TODO: Extend barrier to capture start time barrier.await(); try { long startNanos = System.nanoTime(); long endNanos = startNanos + runningTimeout; int runs = 0; long missCount = 0; while (callOperation(endNanos, runs)) { boolean hit = op.call(runs); if (!hit) missCount++; runs++; } // TODO: Extend barrier to capture end time perf.addStats(op.name, runs, System.nanoTime() - startNanos, missCount); } finally { barrier.await(); } return null; } private boolean callOperation(long endNanos, int runs) { if (ALLOCATION) { return runs < RUN_COUNT_LIMIT; } else { return (runs & 0x400) != 0 || System.nanoTime() < endNanos; } } } private static class TotalStats { private ConcurrentHashMap<String, OpStats> statsMap = new ConcurrentHashMap<String, OpStats>(); public void addStats(String opName, long opCount, long runningTime, long missCount) { OpStats s = new OpStats(opName, opCount, runningTime, missCount); OpStats old = statsMap.putIfAbsent(opName, s); boolean replaced = old == null; while (!replaced) { old = statsMap.get(opName); s = new OpStats(old, opCount, runningTime, missCount); replaced = statsMap.replace(opName, old, s); } } public double getOpsPerSec(String opName) { OpStats s = statsMap.get(opName); if (s == null) return 0; return s.opCount * 1000000000. / s.runningTime * s.threadCount; } public double getTotalOpsPerSec() { long totalOpCount = 0; long totalRunningTime = 0; long totalThreadCount = 0; for (Map.Entry<String, OpStats> e : statsMap.entrySet()) { OpStats s = e.getValue(); totalOpCount += s.opCount; totalRunningTime += s.runningTime; totalThreadCount += s.threadCount; } return totalOpCount * 1000000000. / totalRunningTime * totalThreadCount; } public double getHitRatio(String opName) { OpStats s = statsMap.get(opName); if (s == null) return 0; return 1 - 1. * s.missCount / s.opCount; } public double getTotalHitRatio() { long totalOpCount = 0; long totalMissCount = 0; for (Map.Entry<String, OpStats> e : statsMap.entrySet()) { OpStats s = e.getValue(); totalOpCount += s.opCount; totalMissCount += s.missCount; } return 1 - 1. * totalMissCount / totalOpCount; } } private static class OpStats { public final String opName; public final int threadCount; public final long opCount; public final long runningTime; public final long missCount; private OpStats(String opName, long opCount, long runningTime, long missCount) { this.opName = opName; this.threadCount = 1; this.opCount = opCount; this.runningTime = runningTime; this.missCount = missCount; } private OpStats(OpStats base, long opCount, long runningTime, long missCount) { this.opName = base.opName; this.threadCount = base.threadCount + 1; this.opCount = base.opCount + opCount; this.runningTime = base.runningTime + runningTime; this.missCount = base.missCount + missCount; } } }