/* * JBoss, Home of Professional Open Source * Copyright 2009 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.stress; import org.infinispan.Cache; import org.infinispan.config.Configuration; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.test.TestingUtil; import org.infinispan.test.fwk.TestCacheManagerFactory; import org.testng.annotations.Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Verifies the atomic semantic of Infinispan's implementations of * java.util.concurrent.ConcurrentMap<K, V>.putIfAbsent(K key, V value); which is an interesting * concurrent locking case. * * @since 4.0 * @see java.util.concurrent.ConcurrentMap#putIfAbsent(Object, Object) * @author Sanne Grinovero */ @Test(groups = "stress", testName = "stress.PutIfAbsentStressTest", enabled = false, description = "Since this test is slow to run, it should be disabled by default and run by hand as necessary.") public class PutIfAbsentStressTest { private static final int NODES_NUM = 5; private static final int THREAD_PER_NODE = 12; private static final long STRESS_TIME_MINUTES = 2; private static final String SHARED_KEY = "thisIsTheKeyForConcurrentAccess"; /** * Purpose is not testing JDK's ConcurrentHashMap but ensuring the test is correct. It's also * interesting to compare performance. */ public void testonConcurrentHashMap() throws Exception { System.out.println("Running test on ConcurrentHashMap:"); ConcurrentMap<String, String> map = new ConcurrentHashMap<String, String>(); testConcurrentLocking(map); } /** * Testing putIfAbsent's behaviour on a Local cache. */ public void testonInfinispanLocal() throws Exception { System.out.println("Running test on Infinispan, LOCAL:"); EmbeddedCacheManager cm = TestCacheManagerFactory.createLocalCacheManager(false); ConcurrentMap<String, String> map = cm.getCache(); try { testConcurrentLocking(map); } finally { TestingUtil.killCacheManagers(cm); } } /** * Testing putIfAbsent's behaviour in DIST_SYNC cache. */ public void testonInfinispanDIST_SYNC() throws Exception { System.out.println("Running test on Infinispan, DIST_SYNC:"); Configuration c = new Configuration() .fluent().mode(Configuration.CacheMode.DIST_SYNC).build(); testConcurrentLockingOnMultipleManagers(c); } /** * Testing putIfAbsent's behaviour in DIST_SYNC cache, disabling L1 */ public void testonInfinispanDIST_NOL1() throws Exception { System.out.println("Running test on Infinispan, DIST_SYNC, disabling L1:"); Configuration c = new Configuration() .fluent().mode(Configuration.CacheMode.DIST_SYNC).l1().disable().build(); testConcurrentLockingOnMultipleManagers(c); } /** * Testing putIfAbsent's behaviour in REPL_SYNC cache. */ public void testonInfinispanREPL_SYNC() throws Exception { System.out.println("Running test on Infinispan, REPL_SYNC:"); Configuration c = new Configuration() .fluent().mode(Configuration.CacheMode.REPL_SYNC).build(); testConcurrentLockingOnMultipleManagers(c); } /** * Testing putIfAbsent's behaviour in REPL_ASYNC cache. */ public void testonInfinispanREPL_ASYNC() throws Exception { System.out.println("Running test on Infinispan, REPL_ASYNC:"); Configuration c = new Configuration() .fluent().mode(Configuration.CacheMode.REPL_ASYNC).build(); testConcurrentLockingOnMultipleManagers(c); } /** * Adapter to run the test on any configuration */ private void testConcurrentLockingOnMultipleManagers(Configuration cfg) throws InterruptedException { List<EmbeddedCacheManager> cacheContainers = new ArrayList<EmbeddedCacheManager>(NODES_NUM); List<Cache<String, String>> caches = new ArrayList<Cache<String, String>>(); List<ConcurrentMap<String, String>> maps = new ArrayList<ConcurrentMap<String, String>>(NODES_NUM * THREAD_PER_NODE); for (int nodeNum = 0; nodeNum < NODES_NUM; nodeNum++) { EmbeddedCacheManager cm = TestCacheManagerFactory.createClusteredCacheManager(cfg); cacheContainers.add(cm); Cache<String, String> cache = cm.getCache(); caches.add(cache); for (int threadNum = 0; threadNum < THREAD_PER_NODE; threadNum++) { maps.add(cache); } } TestingUtil.blockUntilViewsReceived(10000, caches); try { testConcurrentLocking(maps); } finally { TestingUtil.killCacheManagers(cacheContainers); } } /** * Adapter for tests sharing a single Cache instance */ private void testConcurrentLocking(ConcurrentMap<String, String> map) throws InterruptedException { int size = NODES_NUM * THREAD_PER_NODE; List<ConcurrentMap<String, String>> maps = new ArrayList<ConcurrentMap<String, String>>(size); for (int i = 0; i < size; i++) { maps.add(map); } testConcurrentLocking(maps); } /** * Drives the actual test on an Executor and verifies the result * * @param maps the caches to be tested */ private void testConcurrentLocking(List<ConcurrentMap<String, String>> maps) throws InterruptedException { SharedStats stats = new SharedStats(); ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(NODES_NUM); List<StressingThread> threads = new ArrayList<StressingThread>(); int i=0; for (ConcurrentMap<String, String> map : maps) { StressingThread thread = new StressingThread(stats, map, i++); threads.add(thread); executor.execute(thread); } executor.shutdown(); Thread.sleep(5000); int putsAfter5Seconds = stats.succesfullPutsCounter.get(); System.out.println("\nSituation after 5 seconds:"); System.out.println(stats.toString()); executor.awaitTermination(STRESS_TIME_MINUTES, TimeUnit.MINUTES); stats.globalQuit = true; executor.awaitTermination(10, TimeUnit.SECONDS); // give some time to awake and quit executor.shutdownNow(); System.out.println("\nFinal situation:"); System.out.println(stats.toString()); assert !stats.seenFailures : "at least one thread has seen unexpected state"; assert stats.succesfullPutsCounter.get() > 0 : "the lock should have been taken at least once"; assert stats.succesfullPutsCounter.get() > putsAfter5Seconds : "the lock count didn't improve since the first 5 seconds. Deadlock?"; assert stats.succesfullPutsCounter.get() == stats.lockReleasedCounter.get() : "there's a mismatch in acquires and releases count"; assert stats.lockOwnersCounter.get() == 0 : "the lock is still held at test finish"; } private static class StressingThread implements Runnable { private final SharedStats stats; private final ConcurrentMap<String, String> cache; private final String ourValue; public StressingThread(SharedStats stats, ConcurrentMap<String, String> cache, int threadId) { this.stats = stats; this.cache = cache; this.ourValue = "v" + threadId; } @Override public void run() { while (!(stats.seenFailures || stats.globalQuit || Thread.interrupted())) { doCycle(); } } private void doCycle() { String beforePut = cache.putIfAbsent(SHARED_KEY, ourValue); if (beforePut != null) { stats.canceledPutsCounter.incrementAndGet(); } else { final String currentCacheValue = cache.get(SHARED_KEY); boolean lockIsFine = stats.lockOwnersCounter.compareAndSet(0, 1) && ourValue.equals(currentCacheValue); stats.succesfullPutsCounter.incrementAndGet(); checkIsTrue(lockIsFine, "I got the lock, some other thread is owning the lock AS WELL."); lockIsFine = stats.lockOwnersCounter.compareAndSet(1, 0); checkIsTrue(lockIsFine, "Some other thread changed the lock count while I was having it!"); cache.remove(SHARED_KEY); stats.lockReleasedCounter.incrementAndGet(); } } private void checkIsTrue(boolean assertion, String message) { if (assertion == false) { stats.seenFailures = true; System.out.println(message); } } } /** * Common state to verify cache behaviour */ public static class SharedStats { final AtomicInteger canceledPutsCounter = new AtomicInteger(0); final AtomicInteger succesfullPutsCounter = new AtomicInteger(0); final AtomicInteger lockReleasedCounter = new AtomicInteger(0); final AtomicInteger lockOwnersCounter = new AtomicInteger(0); Throwable throwable = null; volatile boolean globalQuit = false; // when it's true the threads quit volatile boolean seenFailures = false; // set to true by a thread if it has experienced // illegal state public String toString() { return "\n\tCanceled puts count:\t" + canceledPutsCounter.get() + "\n\tSuccesfull puts count:\t" + succesfullPutsCounter.get() + "\n\tRemoved count:\t" + lockReleasedCounter.get() + "\n\tIllegal state detected:\t" + seenFailures; } } }