#10226 assert using awaitility and fix heap dump cleanup when a leak is detected

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Ludovic Orban 2023-08-23 17:32:58 +02:00
parent 01f8812dbb
commit 44aa6036b3
4 changed files with 106 additions and 27 deletions

View File

@ -22,6 +22,8 @@
<argLine> <argLine>
@{argLine} ${jetty.surefire.argLine} @{argLine} ${jetty.surefire.argLine}
--add-reads org.eclipse.jetty.fcgi.server=org.eclipse.jetty.logging --add-reads org.eclipse.jetty.fcgi.server=org.eclipse.jetty.logging
--add-reads org.eclipse.jetty.fcgi.server=java.management
--add-reads org.eclipse.jetty.fcgi.server=jdk.management
</argLine> </argLine>
</configuration> </configuration>
</plugin> </plugin>
@ -61,5 +63,10 @@
<artifactId>jetty-unixdomain-server</artifactId> <artifactId>jetty-unixdomain-server</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -13,6 +13,15 @@
package org.eclipse.jetty.fcgi.server; package org.eclipse.jetty.fcgi.server;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.management.MBeanServer;
import org.awaitility.Awaitility;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI; import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI;
@ -28,8 +37,9 @@ import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestInfo;
import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.fail;
public abstract class AbstractHttpClientServerTest public abstract class AbstractHttpClientServerTest
{ {
@ -67,14 +77,36 @@ public abstract class AbstractHttpClientServerTest
} }
@AfterEach @AfterEach
public void dispose() public void dispose(TestInfo testInfo) throws Exception
{ {
try try
{ {
if (serverBufferPool != null) if (serverBufferPool != null)
assertThat("Server Leaks: " + serverBufferPool.getLeaks(), serverBufferPool.getLeaks().size(), Matchers.is(0)); {
try
{
Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> serverBufferPool.getLeaks().size(), Matchers.is(0));
}
catch (Exception e)
{
String className = testInfo.getTestClass().orElseThrow().getName();
dumpHeap("server-" + className);
fail(e.getMessage() + "\n---\nServer Leaks: " + serverBufferPool.dumpLeaks() + "---\n");
}
}
if (clientBufferPool != null) if (clientBufferPool != null)
assertThat("Client Leaks: " + clientBufferPool.getLeaks(), clientBufferPool.getLeaks().size(), Matchers.is(0)); {
try
{
Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> clientBufferPool.getLeaks().size(), Matchers.is(0));
}
catch (Exception e)
{
String className = testInfo.getTestClass().orElseThrow().getName();
dumpHeap("client-" + className);
fail(e.getMessage() + "\n---\nClient Leaks: " + clientBufferPool.dumpLeaks() + "---\n");
}
}
} }
finally finally
{ {
@ -82,4 +114,26 @@ public abstract class AbstractHttpClientServerTest
LifeCycle.stop(server); LifeCycle.stop(server);
} }
} }
private static void dumpHeap(String testMethodName) throws Exception
{
Path targetDir = Path.of("target/leaks");
if (Files.exists(targetDir))
{
try (Stream<Path> stream = Files.walk(targetDir))
{
stream.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(java.io.File::delete);
}
}
Files.createDirectories(targetDir);
String dumpName = targetDir.resolve(testMethodName + ".hprof").toString();
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
Class<?> mxBeanClass = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
Object mxBean = ManagementFactory.newPlatformMXBeanProxy(
server, "com.sun.management:type=HotSpotDiagnostic", mxBeanClass);
mxBeanClass.getMethod("dumpHeap", String.class, boolean.class).invoke(mxBean, dumpName, true);
}
} }

View File

@ -240,6 +240,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest
String paramValue2 = fields.getValue(paramName2); String paramValue2 = fields.getValue(paramName2);
assertEquals("", paramValue2); assertEquals("", paramValue2);
Content.Sink.write(response, true, UTF_8.encode("empty")); Content.Sink.write(response, true, UTF_8.encode("empty"));
callback.succeeded();
return true; return true;
} }
}); });
@ -274,6 +275,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest
} }
String paramValue2 = fields.getValue(paramName2); String paramValue2 = fields.getValue(paramName2);
Content.Sink.write(response, true, UTF_8.encode(paramValue2)); Content.Sink.write(response, true, UTF_8.encode(paramValue2));
callback.succeeded();
return true; return true;
} }
}); });
@ -650,6 +652,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest
@Test @Test
public void testLongPollIsAbortedWhenClientIsStopped() throws Exception public void testLongPollIsAbortedWhenClientIsStopped() throws Exception
{ {
AtomicReference<Callback> callbackRef = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
start(new Handler.Abstract() start(new Handler.Abstract()
{ {
@ -657,26 +660,38 @@ public class HttpClientTest extends AbstractHttpClientServerTest
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback)
{ {
latch.countDown(); latch.countDown();
// Do not complete the callback. // Do not complete the callback, but store it aside for
// releasing the buffer later on.
callbackRef.set(callback);
return true; return true;
} }
}); });
CountDownLatch completeLatch = new CountDownLatch(1); try
client.newRequest("localhost", connector.getLocalPort()) {
.scheme(scheme) CountDownLatch completeLatch = new CountDownLatch(1);
.send(result -> client.newRequest("localhost", connector.getLocalPort())
{ .scheme(scheme)
if (result.isFailed()) .send(result ->
completeLatch.countDown(); {
}); if (result.isFailed())
completeLatch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS)); assertTrue(latch.await(5, TimeUnit.SECONDS));
// Stop the client, the complete listener must be invoked. // Stop the client, the complete listener must be invoked.
client.stop(); client.stop();
assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
}
finally
{
// Release the buffer.
Callback callback = callbackRef.get();
if (callback != null)
callback.succeeded();
}
} }
@Test @Test
@ -757,6 +772,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest
{ {
Content.Sink.write(response, false, UTF_8.encode("A")); Content.Sink.write(response, false, UTF_8.encode("A"));
Content.Sink.write(response, true, UTF_8.encode("B")); Content.Sink.write(response, true, UTF_8.encode("B"));
callback.succeeded();
return true; return true;
} }
}); });

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.test.client.transport; package org.eclipse.jetty.test.client.transport;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.net.URI; import java.net.URI;
@ -28,7 +27,6 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.management.MBeanServer; import javax.management.MBeanServer;
import com.sun.management.HotSpotDiagnosticMXBean;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
@ -158,22 +156,26 @@ public class AbstractTest
} }
} }
private static void dumpHeap(String testMethodName) throws IOException private static void dumpHeap(String testMethodName) throws Exception
{ {
Path targetDir = Path.of("target/leaks"); Path targetDir = Path.of("target/leaks");
try (Stream<Path> stream = Files.walk(targetDir)) if (Files.exists(targetDir))
{ {
stream.sorted(Comparator.reverseOrder()) try (Stream<Path> stream = Files.walk(targetDir))
.map(Path::toFile) {
.forEach(java.io.File::delete); stream.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(java.io.File::delete);
}
} }
Files.createDirectories(targetDir); Files.createDirectories(targetDir);
String dumpName = targetDir.resolve(testMethodName + ".hprof").toString(); String dumpName = targetDir.resolve(testMethodName + ".hprof").toString();
MBeanServer server = ManagementFactory.getPlatformMBeanServer(); MBeanServer server = ManagementFactory.getPlatformMBeanServer();
HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy( Class<?> mxBeanClass = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class); Object mxBean = ManagementFactory.newPlatformMXBeanProxy(
mxBean.dumpHeap(dumpName, true); server, "com.sun.management:type=HotSpotDiagnostic", mxBeanClass);
mxBeanClass.getMethod("dumpHeap", String.class, boolean.class).invoke(mxBean, dumpName, true);
} }
protected void start(Transport transport, Handler handler) throws Exception protected void start(Transport transport, Handler handler) throws Exception