HHH-16911 Introduce a testing utility to spot ClassLoader leaks
This commit is contained in:
parent
306fd195a2
commit
187e637b68
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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.orm.test.bootstrap.registry.classloading;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.Assert;
|
||||
|
||||
/**
|
||||
* Utility to test for classloader leaks.
|
||||
*
|
||||
* @author Sanne Grinovero (C) 2023 Red Hat Inc.
|
||||
*/
|
||||
public final class ClassLoaderLeakDetector {
|
||||
|
||||
/**
|
||||
* Utility to verify if executing a certain action will
|
||||
* result in a classloader leak.
|
||||
* @param fullClassnameOfRunnableAction the fully qualified classname
|
||||
* of some action; it needs to implement {@link Runnable}.
|
||||
* The assertion will not fail if it's able to verify that no leak was induced.
|
||||
* @see PhantomReferenceLeakDetector#assertActionNotLeaking(Supplier)
|
||||
*/
|
||||
public static void assertNotLeakingAction(String fullClassnameOfRunnableAction) {
|
||||
Assert.assertTrue( "It seems the action might have leaked the classloader",
|
||||
ClassLoaderLeakDetector.verifyActionNotLeakingClassloader( fullClassnameOfRunnableAction ) );
|
||||
}
|
||||
|
||||
static boolean verifyActionNotLeakingClassloader(String fullClassnameOfRunnableAction) {
|
||||
Objects.requireNonNull( fullClassnameOfRunnableAction );
|
||||
return PhantomReferenceLeakDetector.verifyActionNotLeaking( () -> actionInClassloader( fullClassnameOfRunnableAction ) );
|
||||
}
|
||||
|
||||
public static ClassLoader actionInClassloader(final String actionName) {
|
||||
final Thread currentThread = Thread.currentThread();
|
||||
final ClassLoader initialClassloader = currentThread.getContextClassLoader();
|
||||
final IsolatedClassLoader newClassLoader = new IsolatedClassLoader( initialClassloader );
|
||||
currentThread.setContextClassLoader( newClassLoader );
|
||||
try {
|
||||
runAction( actionName, newClassLoader );
|
||||
}
|
||||
finally {
|
||||
currentThread.setContextClassLoader( initialClassloader );
|
||||
}
|
||||
return newClassLoader;
|
||||
}
|
||||
|
||||
private static void runAction(final String actionName, final IsolatedClassLoader classLoader) {
|
||||
final Runnable action = loadRunnable( actionName, classLoader );
|
||||
action.run();
|
||||
}
|
||||
|
||||
private static Runnable loadRunnable(final String actionName, final IsolatedClassLoader classLoader) {
|
||||
final Class<?> aClass = loadClass( actionName, classLoader );
|
||||
final Constructor<?> constructor = getConstructor( aClass );
|
||||
final Object instance = invokeConstructor( constructor );
|
||||
return (Runnable) instance;
|
||||
}
|
||||
|
||||
private static Object invokeConstructor(final Constructor<?> constructor) {
|
||||
try {
|
||||
return constructor.newInstance();
|
||||
}
|
||||
catch ( InstantiationException e ) {
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
catch ( IllegalAccessException e ) {
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
catch ( InvocationTargetException e ) {
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
}
|
||||
|
||||
private static Constructor<?> getConstructor(Class<?> aClass) {
|
||||
try {
|
||||
return aClass.getDeclaredConstructor();
|
||||
}
|
||||
catch ( NoSuchMethodException e ) {
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> loadClass(final String actionName, final IsolatedClassLoader classLoader) {
|
||||
try {
|
||||
return classLoader.findClass( actionName );
|
||||
}
|
||||
catch ( ClassNotFoundException e ) {
|
||||
throw new RuntimeException( e );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.orm.test.bootstrap.registry.classloading;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Verifies basic operations of {@link ClassLoaderLeakDetector}.
|
||||
*/
|
||||
public class ClassLoaderLeaksUtilityTest {
|
||||
|
||||
@Test
|
||||
public void testClassLoaderLeaksDetected() {
|
||||
Assert.assertFalse( ClassLoaderLeakDetector.verifyActionNotLeakingClassloader( "org.hibernate.orm.test.bootstrap.registry.classloading.LeakingTestAction" ) );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassLoaderLeaksNegated() {
|
||||
Assert.assertTrue( ClassLoaderLeakDetector.verifyActionNotLeakingClassloader( "org.hibernate.orm.test.bootstrap.registry.classloading.NotLeakingTestAction" ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ class IsolatedClassLoader extends ClassLoader {
|
|||
private final ClassLoader resourceSource;
|
||||
|
||||
IsolatedClassLoader(ClassLoader resourceSource) {
|
||||
super( getTopLevelClassLoader( resourceSource ) );
|
||||
super( "TestIsolatedIsolatedClassLoader", getTopLevelClassLoader( resourceSource ) );
|
||||
this.resourceSource = resourceSource;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.orm.test.bootstrap.registry.classloading;
|
||||
|
||||
/**
|
||||
* This runnable will intentionally leak the owning classloader:
|
||||
* useful to test our leak detection utilities.
|
||||
* @see ClassLoaderLeaksUtilityTest
|
||||
*/
|
||||
public final class LeakingTestAction extends NotLeakingTestAction {
|
||||
|
||||
private final ThreadLocal tl = new ThreadLocal();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
super.run();
|
||||
tl.set( this );
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.orm.test.bootstrap.registry.classloading;
|
||||
|
||||
/**
|
||||
* A Runnable used to test ClassLoaderLeakDetector
|
||||
* @see ClassLoaderLeaksUtilityTest
|
||||
*/
|
||||
public class NotLeakingTestAction implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final ClassLoader owningClassloader = getClass().getClassLoader();
|
||||
if ( !owningClassloader.getName().equals( "TestIsolatedIsolatedClassLoader" ) ) {
|
||||
throw new IllegalStateException( "Not being loaded by the expected classloader" );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -62,12 +62,14 @@ public class PhantomReferenceLeakDetector {
|
|||
*/
|
||||
public static <T> void assertActionNotLeaking(Supplier<T> action) {
|
||||
Assert.assertTrue("Operation apparently leaked the critical resource",
|
||||
verifyActionNotLeaking( action,
|
||||
GC_ATTEMPTS,
|
||||
MAX_TOTAL_WAIT_SECONDS )
|
||||
verifyActionNotLeaking( action )
|
||||
);
|
||||
}
|
||||
|
||||
static <T> boolean verifyActionNotLeaking(Supplier<T> action) {
|
||||
return verifyActionNotLeaking( action, GC_ATTEMPTS, MAX_TOTAL_WAIT_SECONDS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for self-testing w/o having to wait for the regular timeout
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue