diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/ClassLoaderLeakDetector.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/ClassLoaderLeakDetector.java new file mode 100644 index 0000000000..e43ff5adc1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/ClassLoaderLeakDetector.java @@ -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 ); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/ClassLoaderLeaksUtilityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/ClassLoaderLeaksUtilityTest.java new file mode 100644 index 0000000000..dd8027b522 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/ClassLoaderLeaksUtilityTest.java @@ -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" ) ); + } + +} + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/IsolatedClassLoader.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/IsolatedClassLoader.java index 5a15e7ebfe..5304166eea 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/IsolatedClassLoader.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/IsolatedClassLoader.java @@ -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; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/LeakingTestAction.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/LeakingTestAction.java new file mode 100644 index 0000000000..962103792f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/LeakingTestAction.java @@ -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 ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/NotLeakingTestAction.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/NotLeakingTestAction.java new file mode 100644 index 0000000000..66ed653243 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/NotLeakingTestAction.java @@ -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" ); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/PhantomReferenceLeakDetector.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/PhantomReferenceLeakDetector.java index 5c726fca89..d7edc8c19c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/PhantomReferenceLeakDetector.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/registry/classloading/PhantomReferenceLeakDetector.java @@ -62,12 +62,14 @@ public class PhantomReferenceLeakDetector { */ public static void assertActionNotLeaking(Supplier action) { Assert.assertTrue("Operation apparently leaked the critical resource", - verifyActionNotLeaking( action, - GC_ATTEMPTS, - MAX_TOTAL_WAIT_SECONDS ) + verifyActionNotLeaking( action ) ); } + static boolean verifyActionNotLeaking(Supplier action) { + return verifyActionNotLeaking( action, GC_ATTEMPTS, MAX_TOTAL_WAIT_SECONDS ); + } + /** * Exposed for self-testing w/o having to wait for the regular timeout */