测试虚拟线程应用程序

在前一篇博文中,我们了解了如何在 Quarkus 中使用虚拟线程实现 CRUD 应用程序。下面的视频展示了如何测试此应用程序,特别是如何检测固定(pinning)。

应用程序和测试的完整代码可在 virtual threads demos repository 中找到。

固定和垄断

虚拟线程结合了命令式开发模型和响应式执行模式。它可以提供一种简单的方法来提高应用程序的并发性。然而,情况并非总是如此。

另一篇博文所述,存在一些限制,包括垄断和固定载体线程。发生这种情况时,应用程序的性能会急剧下降,内存使用量也会增加。短时间的固定是可以容忍的,但在负载下可能会非常糟糕。

虽然目前还没有可靠的方法来检测垄断,但有一些机制可以检测固定。

载体线程被固定时打印堆栈跟踪

假设您有应用程序,并且您的代码库包含测试。您可以配置 Surefire(或您用于执行测试的插件),以便在虚拟线程即将固定载体线程时(而不是顺利解除绑定时)转储堆栈跟踪。您必须将 jdk.tracePinnedThreads 系统属性设置为实现此目的。对于 Surefire Maven 插件,请将 argLine 参数添加到配置中

<argLine>-Djdk.tracePinnedThreads</argLine>

通过此配置,当在运行测试时,载体线程被固定时,堆栈跟踪将转储到控制台

2023-09-15 07:51:18,312 INFO  [org.acm.cru.TodoResource] (quarkus-virtual-thread-0) Called on VirtualThread[#140,quarkus-virtual-thread-0]/runnable@ForkJoinPool-1-worker-1
Thread[#141,ForkJoinPool-1-worker-1,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:185)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:631)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:803)
    java.base/java.lang.Thread.sleep(Thread.java:507)
    org.acme.crud.TodoResource.pinTheCarrierThread(TodoResource.java:93) <== monitors:1
    org.acme.crud.TodoResource.getAll(TodoResource.java:32)
    org.acme.crud.TodoResource$quarkusrestinvoker$getAll_0e9c86666ef01bafda5560c4bf86952c6cf09993.invoke(Unknown Source)
    org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
    io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
    org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
    io.quarkus.virtual.threads.ContextPreservingExecutorService$1.run(ContextPreservingExecutorService.java:36)
    java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)
    java.base/java.lang.VirtualThread.run(VirtualThread.java:311)

分析应用程序日志将告诉您应用程序是否被固定。此外,仔细查看堆栈跟踪将给出原因。在我们的示例中,pinTheCarrierThread 方法正在获取锁。这由 monitors:1 文本指示

    org.acme.crud.TodoResource.pinTheCarrierThread(TodoResource.java:93) <== monitors:1

此方法也可用于生产环境(不仅仅是在测试中)。您还可以通过将固定的堆栈跟踪与其他日志事件(例如同一线程中紧随其后的事件)进行关联,来确定载体线程被阻塞的时间。

测试失败

当日志已经很长时,转储堆栈跟踪可能不太方便。幸运的是,我们发布了一个小型的 Junit 5 扩展,允许您在检测到固定时使测试失败。当您集成第三方库并需要了解其虚拟线程友好性(以决定是使用常规工作线程还是虚拟线程)时,这非常有用。

loom-unit Junit5 扩展目前是一个独立的项目。我们正在将其集成到 Quarkus 测试框架中(名称为 junit5-virtual-threads),因此下面提到的一些步骤将不再必要或会稍作更改。

要使用此扩展,请确保您的项目中包含 loom-unit 扩展

<dependency>
    <groupId>me.escoffier.loom</groupId> <!-- Will become io.quarkus.junit5 -->
    <artifactId>loom-unit</artifactId>   <!-- Will become junit5-virtual-threads -->
    <version>0.3.0</version>             <!-- Will become unnecessary -->
    <scope>test</scope>
</dependency>

然后,在您的测试中,使用 @ExtendWith 启用该扩展

@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(LoomUnitExtension.class) // <--- Enable the extension (will become @VirtualThreadUnit)
@ShouldNotPin  // <--- Detect pinning
class TodoResourceTest {
    // ...
}

最后,使用 @ShouldNotPin 注释来指示如果测试用例中的任何方法固定了载体线程,则使测试失败。您也可以在方法上使用 @ShouldNotPin 注释。

如果在测试执行期间捕获到固定事件,则测试将失败。事件的堆栈跟踪将附加到测试失败信息中。

java.lang.AssertionError: The test testInitialItems() was expected to NOT pin the carrier thread, but we collected 1 event(s)
* Pinning event captured:
	java.lang.VirtualThread.parkOnCarrierThread(java.lang.VirtualThread.java:687)
	java.lang.VirtualThread.parkNanos(java.lang.VirtualThread.java:646)
	java.lang.VirtualThread.sleepNanos(java.lang.VirtualThread.java:803)
	java.lang.Thread.sleep(java.lang.Thread.java:507)
	org.acme.crud.TodoResource.pinTheCarrierThread(org.acme.crud.TodoResource.java:93)
	org.acme.crud.TodoResource.getAll(org.acme.crud.TodoResource.java:32)
	org.acme.crud.TodoResource$quarkusrestinvoker$getAll_0e9c86666ef01bafda5560c4bf86952c6cf09993.invoke(org.acme.crud.TodoResource$quarkusrestinvoker$getAll_0e9c86666ef01bafda5560c4bf86952c6cf09993.java:-1)
	org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(org.jboss.resteasy.reactive.server.handlers.InvocationHandler.java:29)
	io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.java:141)
	org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.java:147)
	io.quarkus.virtual.threads.ContextPreservingExecutorService$1.run(io.quarkus.virtual.threads.ContextPreservingExecutorService$1.java:36)
	java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.java:314)
	java.lang.VirtualThread.runWith(java.lang.VirtualThread.java:341)
	java.lang.VirtualThread.run(java.lang.VirtualThread.java:311)
	java.lang.VirtualThread$VThreadContinuation$1.run(java.lang.VirtualThread$VThreadContinuation$1.java:192)
	jdk.internal.vm.Continuation.enter0(jdk.internal.vm.Continuation.java:320)
	jdk.internal.vm.Continuation.enter(jdk.internal.vm.Continuation.java:312)
	jdk.internal.vm.Continuation.enterSpecial(jdk.internal.vm.Continuation.java:-1)

项目仓库 或其 Quarkus 兄弟项目 中查找有关 loom-unit 扩展的更多信息。

总结

本博文介绍了如何在运行测试时检测固定事件。首先,您可以将堆栈跟踪转储到日志中。其次,您可以使用 @ShouldNotPin 注释在捕获到固定事件时使测试失败。得益于此 PR,loom-unit 扩展将被集成到 @QuarkusTest 中,以提供更简单的开发体验。它将在下一个发行版(3.5.x)中包含在 Quarkus 中。