Skip to content

Commit

Permalink
Force cleanup of ThreadLocal (#501)
Browse files Browse the repository at this point in the history
* Force cleanup of ThreadLocal

* Fix the checkstyle errors

---------

Co-authored-by: Scott M Stark <starksm64@gmail.com>
Co-authored-by: Scott M Stark <starksm@starkinternational.com>
  • Loading branch information
3 people authored Jun 29, 2024
1 parent edc84f9 commit c3b7703
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.jboss.arquillian.core.spi.Manager;
import org.jboss.arquillian.core.spi.NonManagedObserver;
import org.jboss.arquillian.core.spi.ObserverMethod;
import org.jboss.arquillian.core.spi.ThreadLocalUtil;
import org.jboss.arquillian.core.spi.Validate;
import org.jboss.arquillian.core.spi.context.ApplicationContext;
import org.jboss.arquillian.core.spi.context.Context;
Expand Down Expand Up @@ -279,6 +280,9 @@ public void shutdown() {
runtimeLogger.clear();

handledThrowables.remove();

//Force cleanup ThreadLocal:
ThreadLocalUtil.forceCleanupThreadLocal(handledThrowables);
}
if (shutdownException != null) {
UncheckedThrow.throwUnchecked(shutdownException);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.jboss.arquillian.core.spi;

import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ThreadLocalUtil {
private static Logger log = Logger.getLogger(ThreadLocalUtil.class.getName());

/**
* Force cleanup of a thread local variable: using reflection, all entries for the "ThreadLocalMap" are removed.
* The entries are searched for all active threads.
*
* @param threadLocal This is the threadlocal variable to be cleaned up.
*/
public static void forceCleanupThreadLocal(ThreadLocal<?> threadLocal) {
//Get all threads:
ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
ThreadGroup parentGroup;
while ((parentGroup = rootGroup.getParent()) != null) {
rootGroup = parentGroup;
}
Thread[] threads = new Thread[rootGroup.activeCount()];
while (rootGroup.enumerate(threads, true ) == threads.length) {
threads = new Thread[threads.length * 2];
}
//Cleanup for each thread:
for (Thread thread : threads)
{
//The "threads" array has NULL entries...
if (thread != null)
{
cleanThreadLocals(thread, threadLocal);
}
}
}

/**
* Performs cleanup of the ThreadLocal entry for a specific thread.
*
* @param thread
* @param threadLocal
*/
private static void cleanThreadLocals(Thread thread, ThreadLocal<?> threadLocal) {
try {
Class<?> threadLocalClass = Class.forName("java.lang.ThreadLocal");
Method getMapMethod = threadLocalClass.getDeclaredMethod("getMap", Thread.class);
getMapMethod.setAccessible(true);

Object threadLocalMap = getMapMethod.invoke(threadLocal, thread);

if (threadLocalMap != null)
{
Class<?> threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Method removeMethod = threadLocalMapClass.getDeclaredMethod("remove", ThreadLocal.class);
removeMethod.setAccessible(true);

removeMethod.invoke(threadLocalMap, threadLocal);
}
} catch(Exception e) {
// We will tolerate an exception here and just log it
log.log(Level.SEVERE, "Arquillian failed to cleanup threadlocals - did the Java API change?", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import org.jboss.arquillian.core.spi.ThreadLocalUtil;
import org.jboss.arquillian.core.spi.Validate;

/**
Expand Down Expand Up @@ -105,10 +107,15 @@ public void clearAll() {
deactivateAll();
}
activeStore.remove();

//Force cleanup ThreadLocal:
ThreadLocalUtil.forceCleanupThreadLocal(activeStore);

for (Map.Entry<T, ObjectStore> entry : stores.entrySet()) {
entry.getValue().clear();
}
stores.clear();

}
}

Expand Down

0 comments on commit c3b7703

Please sign in to comment.