Skip to content

Commit

Permalink
HHH-16911 Introduce a testing utility to spot memory leaks
Browse files Browse the repository at this point in the history
  • Loading branch information
Sanne committed Aug 1, 2023
1 parent 1642119 commit 306fd19
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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";
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}

0 comments on commit 306fd19

Please sign in to comment.