测试虚拟线程应用程序
在前一篇博文中,我们了解了如何在 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 中。