Add a Tomcat web application ClassLoader/ThreadLocal leak detection test.

pull/793/merge
Brett Wooldridge 8 years ago
parent 5544a7113f
commit 9efa2f7098

@ -79,7 +79,7 @@ public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseab
int getState();
}
public interface IBagStateListener
public static interface IBagStateListener
{
Future<Boolean> addBagItem();
}

@ -0,0 +1,328 @@
/*
* Copyright (C) 2017 Brett Wooldridge
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zaxxer.hikari.util;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry;
/**
* @author Brett Wooldridge
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TomcatConcurrentBagLeakTest
{
@Test
public void testConcurrentBagForLeaks() throws Exception
{
ClassLoader cl = new FauxWebClassLoader();
Class<?> clazz = cl.loadClass(this.getClass().getName() + "$FauxWebContext");
Object fauxWebContext = clazz.newInstance();
Method createConcurrentBag = clazz.getDeclaredMethod("createConcurrentBag");
createConcurrentBag.invoke(fauxWebContext);
Field failureException = clazz.getDeclaredField("failureException");
Exception ex = (Exception) failureException.get(fauxWebContext);
assertNull(ex);
}
@Test
public void testConcurrentBagForLeaks2() throws Exception
{
ClassLoader cl = this.getClass().getClassLoader();
Class<?> clazz = cl.loadClass(this.getClass().getName() + "$FauxWebContext");
Object fauxWebContext = clazz.newInstance();
Method createConcurrentBag = clazz.getDeclaredMethod("createConcurrentBag");
createConcurrentBag.invoke(fauxWebContext);
Field failureException = clazz.getDeclaredField("failureException");
Exception ex = (Exception) failureException.get(fauxWebContext);
assertNotNull(ex);
}
static class FauxWebClassLoader extends ClassLoader
{
static final byte[] classBytes = new byte[16_000];
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException
{
if (name.startsWith("java") || name.startsWith("org")) {
return super.loadClass(name, true);
}
final String resourceName = "/" + name.replace('.', '/') + ".class";
final URL resource = this.getClass().getResource(resourceName);
try (DataInputStream is = new DataInputStream(resource.openStream())) {
int read = 0;
while (read < classBytes.length) {
final int rc = is.read(classBytes, read, classBytes.length - read);
if (rc == -1) {
break;
}
read += rc;
}
return defineClass(name, classBytes, 0, read);
}
catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
}
public static class PoolEntry implements IConcurrentBagEntry
{
private int state;
@Override
public boolean compareAndSet(int expectState, int newState)
{
this.state = newState;
return true;
}
@Override
public void setState(int newState)
{
this.state = newState;
}
@Override
public int getState()
{
return state;
}
}
public static class FauxWebContext
{
private static final Logger log = LoggerFactory.getLogger(FauxWebContext.class);
public Exception failureException;
public void createConcurrentBag() throws InterruptedException
{
try (ConcurrentBag<PoolEntry> bag = new ConcurrentBag<>(() -> CompletableFuture.completedFuture(Boolean.TRUE))) {
PoolEntry entry = new PoolEntry();
bag.add(entry);
PoolEntry borrowed = bag.borrow(100, MILLISECONDS);
bag.requite(borrowed);
PoolEntry removed = bag.borrow(100, MILLISECONDS);
bag.remove(removed);
}
checkThreadLocalsForLeaks();
}
private void checkThreadLocalsForLeaks()
{
Thread[] threads = getThreads();
try {
// Make the fields in the Thread class that store ThreadLocals
// accessible
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals");
inheritableThreadLocalsField.setAccessible(true);
// Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects
// accessible
Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries");
expungeStaleEntriesMethod.setAccessible(true);
for (int i = 0; i < threads.length; i++) {
Object threadLocalMap;
if (threads[i] != null) {
// Clear the first map
threadLocalMap = threadLocalsField.get(threads[i]);
if (null != threadLocalMap) {
expungeStaleEntriesMethod.invoke(threadLocalMap);
checkThreadLocalMapForLeaks(threadLocalMap, tableField);
}
// Clear the second map
threadLocalMap = inheritableThreadLocalsField.get(threads[i]);
if (null != threadLocalMap) {
expungeStaleEntriesMethod.invoke(threadLocalMap);
checkThreadLocalMapForLeaks(threadLocalMap, tableField);
}
}
}
}
catch (Throwable t) {
log.warn("Failed to check for ThreadLocal references for web application [{}]", t);
failureException = new Exception();
}
}
private Object getContextName()
{
return this.getClass().getName();
}
// THE FOLLOWING CODE COPIED FROM APACHE TOMCAT (2017/01/08)
/**
* Analyzes the given thread local map object. Also pass in the field that
* points to the internal table to save re-calculating it on every
* call to this method.
*/
private void checkThreadLocalMapForLeaks(Object map, Field internalTableField) throws IllegalAccessException, NoSuchFieldException
{
if (map != null) {
Object[] table = (Object[]) internalTableField.get(map);
if (table != null) {
for (int j = 0; j < table.length; j++) {
Object obj = table[j];
if (obj != null) {
boolean keyLoadedByWebapp = false;
boolean valueLoadedByWebapp = false;
// Check the key
Object key = ((Reference<?>) obj).get();
if (this.equals(key) || loadedByThisOrChild(key)) {
keyLoadedByWebapp = true;
}
// Check the value
Field valueField = obj.getClass().getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(obj);
if (this.equals(value) || loadedByThisOrChild(value)) {
valueLoadedByWebapp = true;
}
if (keyLoadedByWebapp || valueLoadedByWebapp) {
Object[] args = new Object[5];
args[0] = getContextName();
if (key != null) {
args[1] = getPrettyClassName(key.getClass());
try {
args[2] = key.toString();
}
catch (Exception e) {
log.warn("Unable to determine string representation of key of type [{}]", args[1], e);
args[2] = "Unknown";
}
}
if (value != null) {
args[3] = getPrettyClassName(value.getClass());
try {
args[4] = value.toString();
}
catch (Exception e) {
log.warn("webappClassLoader.checkThreadLocalsForLeaks.badValue {}", args[3], e);
args[4] = "Unknown";
}
}
if (valueLoadedByWebapp) {
log.error("The web application [{}] created a ThreadLocal with key " +
"(value [{}]) and a value of type [{}] (value [{}]) but failed to remove " +
"it when the web application was stopped. Threads are going to be renewed " +
"over time to try and avoid a probable memory leak.", args);
failureException = new Exception();
}
else if (value == null) {
log.debug("The web application [{}] created a ThreadLocal with key of type [{}] " +
"(value [{}]). The ThreadLocal has been correctly set to null and the " +
"key will be removed by GC.", args);
failureException = new Exception();
}
else {
log.debug("The web application [{}] created a ThreadLocal with key of type [{}] " +
"(value [{}]) and a value of type [{}] (value [{}]). Since keys are only " +
"weakly held by the ThreadLocal Map this is not a memory leak.", args);
failureException = new Exception();
}
}
}
}
}
}
}
private boolean loadedByThisOrChild(Object key)
{
return key.getClass().getClassLoader() == this.getClass().getClassLoader();
}
/*
* Get the set of current threads as an array.
*/
private Thread[] getThreads()
{
// Get the current thread group
ThreadGroup tg = Thread.currentThread().getThreadGroup();
// Find the root thread group
try {
while (tg.getParent() != null) {
tg = tg.getParent();
}
}
catch (SecurityException se) {
log.warn("Unable to obtain the parent for ThreadGroup [{}]. It will not be possible to check all threads for potential memory leaks", tg.getName(), se);
}
int threadCountGuess = tg.activeCount() + 50;
Thread[] threads = new Thread[threadCountGuess];
int threadCountActual = tg.enumerate(threads);
// Make sure we don't miss any threads
while (threadCountActual == threadCountGuess) {
threadCountGuess *= 2;
threads = new Thread[threadCountGuess];
// Note tg.enumerate(Thread[]) silently ignores any threads that
// can't fit into the array
threadCountActual = tg.enumerate(threads);
}
return threads;
}
private String getPrettyClassName(Class<?> clazz)
{
String name = clazz.getCanonicalName();
if (name == null) {
name = clazz.getName();
}
return name;
}
}
}
Loading…
Cancel
Save