HHH-16911 Introduce a testing utility to spot memory leaks
This commit is contained in:
parent
1642119648
commit
306fd195a2
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.Test;
|
||||
|
||||
/**
|
||||
* Tests the testing utility PhantomReferenceLeakDetector
|
||||
*/
|
||||
public class LeakUtilitySelfTest {
|
||||
|
||||
@Test
|
||||
public void verifyLeakUtility() {
|
||||
PhantomReferenceLeakDetector.assertActionNotLeaking( LeakUtilitySelfTest::notALeak );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyLeakUtilitySpotsLeak() {
|
||||
Assert.assertFalse( PhantomReferenceLeakDetector.verifyActionNotLeaking( LeakUtilitySelfTest::troubleSomeLeak, 2, 1 ) );
|
||||
}
|
||||
|
||||
private static SomeSpecialObject notALeak() {
|
||||
return new SomeSpecialObject();
|
||||
}
|
||||
|
||||
private static SomeSpecialObject troubleSomeLeak() {
|
||||
final SomeSpecialObject specialThing = new SomeSpecialObject();
|
||||
tl.set( specialThing );
|
||||
return specialThing;
|
||||
}
|
||||
|
||||
private static final ThreadLocal tl = new ThreadLocal<>();
|
||||
|
||||
static class SomeSpecialObject {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "this is some hypothetical critical object";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.ref.PhantomReference;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.Assert;
|
||||
|
||||
/**
|
||||
* Utility to help verify that a certain object is free
|
||||
* to be garbage collected (that we're not leaking it).
|
||||
* This is particularly useful with Classloaders.
|
||||
*
|
||||
* @author Sanne Grinovero (C) 2023 Red Hat Inc.
|
||||
*/
|
||||
public class PhantomReferenceLeakDetector {
|
||||
|
||||
/**
|
||||
* A single second should be more than enough; this might need
|
||||
* to be tuned for particularly slow systems to avoid
|
||||
* flaky tests, but I personally believe it's very
|
||||
* large: we default to a very generous amount as we won't
|
||||
* normally wait for this long, unless there is a problem.
|
||||
* So consider carefully if there's not a deeper
|
||||
* problem before setting these to even larger amounts.
|
||||
*/
|
||||
private static final int MAX_TOTAL_WAIT_SECONDS = 180;
|
||||
private static final int GC_ATTEMPTS = MAX_TOTAL_WAIT_SECONDS * 5;
|
||||
|
||||
/**
|
||||
* Asserts that a certain operation won't be leaking
|
||||
* a particular object of type T.
|
||||
* The operation being tested needs to implement {@link Supplier} and
|
||||
* return a reference to the object to monitor; we expect
|
||||
* this object to be eligible for garbage collection soon after the
|
||||
* action is completed, and therefore great care must be taken
|
||||
* for the test itself to not leak a reference to such object
|
||||
* either, including on the caller's stack; this implies it
|
||||
* might be necessary to explicitly null local variables
|
||||
* if the test infrastructure is referring to the critical object.
|
||||
* For an object to not be considered leaked, it must be
|
||||
* garbage collected in a reasonable time after the action; since
|
||||
* we rely on the GC operation, which is asynchronous and not deterministic,
|
||||
* it's possible that this test could fail even without a real leak;
|
||||
* to prevent flaky tests we use a very generously sized timeout and
|
||||
* we might trigger multiple GC events.
|
||||
* This approach implies that a failing assertion might not necessarily
|
||||
* signal that there definitively is a leak, but the test not failing
|
||||
* should imply we're definitively fine.
|
||||
* If a test using this utility were to suddenly start failing
|
||||
* beware of raising the timeouts without investigating: if the object
|
||||
* is eventually garbage collected but taking an unusual amount of time,
|
||||
* that's also a sign of something not being quite right.
|
||||
*/
|
||||
public static <T> void assertActionNotLeaking(Supplier<T> action) {
|
||||
Assert.assertTrue("Operation apparently leaked the critical resource",
|
||||
verifyActionNotLeaking( action,
|
||||
GC_ATTEMPTS,
|
||||
MAX_TOTAL_WAIT_SECONDS )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for self-testing w/o having to wait for the regular timeout
|
||||
*/
|
||||
static <T> boolean verifyActionNotLeaking(Supplier<T> action, final int gcAttempts, final int totalWaitSeconds ) {
|
||||
T criticalReference = action.get();
|
||||
final ReferenceQueue<T> referenceQueue = new ReferenceQueue<>();
|
||||
final PhantomReference<T> reference = new PhantomReference<>( criticalReference, referenceQueue );
|
||||
//Ignore IDE's suggestion to remove the following line: we really need it!
|
||||
// (it could be inlined above, but I prefer this style so that it serves as an example for
|
||||
// future maintenance of how this works)
|
||||
criticalReference = null;
|
||||
return verifyCollection( referenceQueue, gcAttempts, totalWaitSeconds );
|
||||
}
|
||||
|
||||
private static <T> boolean verifyCollection(final ReferenceQueue<T> referenceQueue, final int gcAttempts, final int totalWaitSeconds) {
|
||||
final int millisEachAttempt = Math.round((float) TimeUnit.SECONDS.toMillis( totalWaitSeconds ) / gcAttempts );
|
||||
for ( int i = 0; i < gcAttempts; i++ ) {
|
||||
Runtime.getRuntime().gc();
|
||||
try {
|
||||
Reference<?> ref = referenceQueue.remove( millisEachAttempt );
|
||||
if ( ref != null ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch ( InterruptedException e ) {
|
||||
//let's try another GC: if there's complex finalizers on the path to the object
|
||||
//that needs to be tested a single GC cycle might not be enough for it to get collected.
|
||||
//(We don't expect any complex finalizer so we won't be waiting too much either..)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue