package org.infinispan.util; import java.io.File; import java.lang.ref.Reference; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.infinispan.Cache; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.global.GlobalConfigurationBuilder; import org.infinispan.configuration.global.GlobalJmxStatisticsConfigurationBuilder; import org.infinispan.eviction.EvictionStrategy; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.test.AbstractInfinispanTest; import org.infinispan.test.TestingUtil; import org.infinispan.test.fwk.TestResourceTracker; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; /** * Tests whether certain cache set ups result in thread local leaks. * * @author Galder ZamarreƱo * @since 5.3 */ @Test(groups = "functional", testName = "util.ThreadLocalLeakTest") public class ThreadLocalLeakTest extends AbstractInfinispanTest { private static final Pattern THREAD_LOCAL_FILTER = Pattern.compile("org\\.infinispan\\..*"); // Some static thread locals we cannot remove, because private static final Set<String> ACCEPTED_THREAD_LOCALS = new HashSet<String>(Arrays.asList(new String[]{ "org.infinispan.commons.util.concurrent.jdk8backported.EquivalentConcurrentHashMapV8$CounterHashCode" })); private String tmpDirectory; @BeforeClass protected void setUpTempDir() { tmpDirectory = TestingUtil.tmpDirectory(this.getClass()); } @AfterClass protected void clearTempDir() { org.infinispan.commons.util.Util.recursiveFileRemove(tmpDirectory); new File(tmpDirectory).mkdirs(); } public void testCheckThreadLocalLeaks() throws Exception { final ConfigurationBuilder builder = new ConfigurationBuilder(); builder .eviction().strategy(EvictionStrategy.LRU).maxEntries(4096) .locking().concurrencyLevel(2048) .persistence().passivation(false) .addSingleFileStore().location(tmpDirectory).shared(false).preload(true); Future<Map<String, Map<ThreadLocal<?>, Object>>> result = fork( new Callable<Map<String, Map<ThreadLocal<?>, Object>>>() { @Override public Map<String, Map<ThreadLocal<?>, Object>> call() throws Exception { TestResourceTracker.testThreadStarted(ThreadLocalLeakTest.this); Thread forkedThread = doStuffWithCache(builder); beforeGC(); System.gc(); Thread.sleep(500); System.gc(); afterGC(); // Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); List<Thread> threadSet = Arrays.asList(Thread.currentThread(), forkedThread); Map<String, Map<ThreadLocal<?>, Object>> allThreadLocals = new HashMap<String, Map<ThreadLocal<?>, Object>>(); for (Thread thread : threadSet) { Map<ThreadLocal<?>, Object> threadLocalLeaks = findThreadLocalLeaks(thread); if (threadLocalLeaks != null && !threadLocalLeaks.isEmpty()) allThreadLocals.put(thread.getName(), threadLocalLeaks); } return allThreadLocals; } }); Map<String, Map<ThreadLocal<?>, Object>> allThreadLocals = result.get(30, TimeUnit.SECONDS); if (!allThreadLocals.isEmpty()) throw new IllegalStateException("Thread locals still present: " + allThreadLocals); } private Thread doStuffWithCache(ConfigurationBuilder builder) { GlobalJmxStatisticsConfigurationBuilder globalBuilder = new GlobalConfigurationBuilder().nonClusteredDefault() .globalJmxStatistics().allowDuplicateDomains(true); final EmbeddedCacheManager[] cm = {new DefaultCacheManager(globalBuilder.build(), builder.build(), true)}; Thread forkedThread = null; try { final Cache<Object, Object> c = cm[0].getCache(); c.put("key1", "value1"); forkedThread = inNewThread(new Runnable() { @Override public void run() { Cache<Object, Object> c = cm[0].getCache(); c.put("key2", "value2"); c = null; TestingUtil.sleepThread(2000); } }); c.put("key3", "value3"); } finally { TestingUtil.killCacheManagers(cm); } cm[0] = null; return forkedThread; } private void beforeGC() { // do nothing } private void afterGC() { // do nothing } private Map<ThreadLocal<?>, Object> findThreadLocalLeaks(Thread thread) throws Exception { // Get a reference to the thread locals table of the current thread Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); threadLocalsField.setAccessible(true); Object threadLocalTable = threadLocalsField.get(thread); // Get a reference to the array holding the thread local variables inside the // ThreadLocalMap of the current thread Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); Field tableField = threadLocalMapClass.getDeclaredField("table"); tableField.setAccessible(true); Object table = null; try { table = tableField.get(threadLocalTable); } catch (NullPointerException e) { // Ignore return null; } Class<?> entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry"); Field valueField = entryClass.getDeclaredField("value"); valueField.setAccessible(true); Map<ThreadLocal<?>, Object> threadLocals = new HashMap<ThreadLocal<?>, Object>(); for (int i=0; i < Array.getLength(table); i++) { // Each entry in the table array of ThreadLocalMap is an Entry object // representing the thread local reference and its value Reference<ThreadLocal<?>> entry = (Reference) Array.get(table, i); if (entry != null) { // Get a reference to the thread local object ThreadLocal<?> threadLocal = entry.get(); Object value = valueField.get(entry); if (threadLocal != null) { if (filterThreadLocals(threadLocal, value) && !ACCEPTED_THREAD_LOCALS.contains(threadLocal.getClass().getCanonicalName())) { log.error("Thread local leak: " + threadLocal); threadLocals.put(threadLocal, value); // threadLocal.remove(); } } else { log.warn("Thread local is not accessible, but it wasn't removed either: " + value); } } } return threadLocals; } private boolean filterThreadLocals(ThreadLocal<?> tl, Object value) { String tlClassName = tl.getClass().getName(); String valueClassName = value != null ? value.getClass().getName() : ""; log.tracef("Checking thread-local %s = %s", tlClassName, valueClassName); if (!THREAD_LOCAL_FILTER.matcher(tlClassName).find() && !THREAD_LOCAL_FILTER.matcher(valueClassName).find()) { return false; } return !ACCEPTED_THREAD_LOCALS.contains(tlClassName) && !ACCEPTED_THREAD_LOCALS.contains(valueClassName); } }