HHH-16911 Introduce a testing utility to spot ClassLoader leaks

This commit is contained in:
Sanne Grinovero 2023-07-13 18:58:16 +01:00 committed by Sanne Grinovero
parent a03c947b5d
commit 9973c80a76
6 changed files with 182 additions and 4 deletions

View File

@ -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 );
}
}
}

View File

@ -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" ) );
}
}

View File

@ -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;
}

View File

@ -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 );
}
}

View File

@ -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" );
}
}
}

View File

@ -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
*/