Forked Process Timeout Extension Guide
Since 3.6.0, Surefire and Failsafe expose an extension point that fires when a forked test JVM is killed because it exceeded forkedProcessTimeoutInSeconds. Use it to capture diagnostic information (thread dumps, heap dumps, JFR recordings, notifications, …) about a hung test process before it is destroyed.
This page walks through:
- Enabling the built-in
jstackextension - Writing your own extension
- Passing configuration to extensions
- Lifecycle, threading and error handling
A concise API reference is also available at examples/timeout-extension.html.
Built-in jstack extension
Surefire ships with a built-in JstackTimeoutExtension. When a forked JVM hits the timeout it captures a jstack thread dump of that process and writes it to a .txt file just before the kill signal is sent — so the dump reflects the actual hung state.
Step 1 — set a sensible timeout
The extension only runs when the kill path is triggered, so make sure a timeout is configured:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkedProcessTimeoutInSeconds>600</forkedProcessTimeoutInSeconds>
</configuration>
</plugin>
Step 2 — enable it
The extension is registered via META-INF/services but is disabled by default. Enable it either through the POM…
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkedProcessTimeoutInSeconds>600</forkedProcessTimeoutInSeconds>
<forkedProcessTimeoutExtensionContext>
<jstack.enabled>true</jstack.enabled>
</forkedProcessTimeoutExtensionContext>
</configuration>
</plugin>
…or per-build via a system property:
mvn verify -Dsurefire.timeout.jstack.enabled=true
Either source enables the extension; if both are set, the property still wins (it is processed first).
Step 3 — (optional) choose where dumps land
By default the file is written under the configured reports directory as
target/surefire-reports/surefire-timeout-jstack-<forkNumber>-<pid>.txt
To override the destination, add the jstack.output.location key to the same map:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkedProcessTimeoutInSeconds>600</forkedProcessTimeoutInSeconds>
<forkedProcessTimeoutExtensionContext>
<jstack.enabled>true</jstack.enabled>
<jstack.output.location>${project.build.directory}/jstacks</jstack.output.location>
</forkedProcessTimeoutExtensionContext>
</configuration>
</plugin>
Requirements
jstackmust be available — Surefire looks for it in${java.home}/bin/, the parent JDKbin/,$JAVA_HOME/bin/and finally onPATH. Building with a JRE (Java 8) will fail to find it.- The PID of the forked JVM must be resolvable; on Java 8 there is no public
Process.pid()API and the extension will log a warning and skip the dump. - When the call fails for any reason (timeout > 20 s, non-zero exit, etc.) a warning is logged and the test result is unaffected.
Writing your own extension
The SPI is a single interface in the surefire-extensions-api artifact:
package org.apache.maven.surefire.extensions;
public interface ForkedProcessTimeoutExtension {
// Called BEFORE the KILL signal — the forked JVM is still alive,
// ideal place to run jstack, jcmd, capture a JFR snapshot, …
void onTimeoutDetected(ForkedProcessTimeoutContext context) throws Exception;
// Called AFTER the forked JVM has exited — good place to upload
// artifacts, send notifications, etc.
void onForkExited(ForkedProcessTimeoutContext context, RunResult runResult) throws Exception;
}
Step 1 — create the extension module
A standalone Maven module is the simplest distribution unit:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.surefire</groupId>
<artifactId>my-timeout-extension</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-extensions-api</artifactId>
<version>3.6.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
scope=provided keeps the dependency out of the published JAR — the SPI classes are always supplied by the running Surefire plugin.
Step 2 — implement the interface
package com.example.surefire;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.surefire.api.suite.RunResult;
import org.apache.maven.surefire.extensions.ForkedProcessTimeoutContext;
import org.apache.maven.surefire.extensions.ForkedProcessTimeoutExtension;
public class HeapDumpTimeoutExtension implements ForkedProcessTimeoutExtension {
private static final String OUTPUT_KEY = "heapdump.output.location";
@Override
public void onTimeoutDetected(ForkedProcessTimeoutContext context) throws Exception {
ConsoleLogger log = context.getConsoleLogger();
long pid = context.getPid();
if (pid <= 0L) {
log.warning("HeapDumpTimeoutExtension: unknown PID, skipping fork " + context.getForkNumber());
return;
}
String dir = context.getExtensionContext().getOrDefault(
OUTPUT_KEY, context.getReportsDirectory().getAbsolutePath());
Path out = Path.of(dir, "heap-fork-" + context.getForkNumber() + "-" + pid + ".hprof");
Files.createDirectories(out.getParent());
// Spawn jcmd ${pid} GC.heap_dump
new ProcessBuilder("jcmd", Long.toString(pid), "GC.heap_dump", out.toString())
.redirectErrorStream(true)
.inheritIO()
.start()
.waitFor();
log.info("HeapDumpTimeoutExtension: wrote " + out);
}
@Override
public void onForkExited(ForkedProcessTimeoutContext context, RunResult runResult) {
// optional: upload to S3, post to Slack, etc.
}
}
Step 3 — register via ServiceLoader
Create the file src/main/resources/META-INF/services/org.apache.maven.surefire.extensions.ForkedProcessTimeoutExtension with one fully-qualified class name per line:
com.example.surefire.HeapDumpTimeoutExtension
Step 4 — declare the extension in the consumer project
Add the extension JAR as a <dependency> of maven-surefire-plugin (or maven-failsafe-plugin) — not as a project test dependency. Surefire discovers extensions on the plugin classpath:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkedProcessTimeoutInSeconds>600</forkedProcessTimeoutInSeconds>
<forkedProcessTimeoutExtensionContext>
<heapdump.output.location>${project.build.directory}/heap-dumps</heapdump.output.location>
</forkedProcessTimeoutExtensionContext>
</configuration>
<dependencies>
<dependency>
<groupId>com.example.surefire</groupId>
<artifactId>my-timeout-extension</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</plugin>
That's it — when a fork is killed for timeout, both your extension and any other registered extension (including the built-in jstack one if enabled) are invoked.
Passing configuration to extensions
Surefire and Failsafe expose a single Mojo parameter for all timeout extensions:
<forkedProcessTimeoutExtensionContext>
<jstack.output.location>${project.build.directory}/jstacks</jstack.output.location>
<heapdump.output.location>${project.build.directory}/heap-dumps</heapdump.output.location>
<slack.webhook>https://hooks.slack.com/services/…</slack.webhook>
</forkedProcessTimeoutExtensionContext>
The map is exposed to every extension via ForkedProcessTimeoutContext.getExtensionContext(). Keys are implementation-specific — pick a unique prefix for each extension to avoid collisions.
Context API
| Method | Description |
|---|---|
getPid() |
OS PID of the forked JVM, or -1 if unavailable (Java 8) |
getForkNumber() |
1-based fork number assigned by Surefire |
getJavaExecutable() |
Path to the java binary used by the fork (may be null) |
getReportsDirectory() |
Surefire reports directory |
getTimeoutSeconds() |
Configured forkedProcessTimeoutInSeconds |
getConsoleLogger() |
Logger writing to the Maven console |
getExtensionContext() |
User-supplied Map<String,String> from the Mojo parameter |
Supported built-in keys
| Key | Used by | Description |
|---|---|---|
jstack.enabled |
JstackTimeoutExtension |
Set to true to enable the built-in jstack extension from the POM. |
jstack.output.location |
JstackTimeoutExtension |
Directory for surefire-timeout-jstack-*.txt. Defaults to the reports dir. |
Lifecycle, threading and error handling
- Order of callbacks —
onTimeoutDetectedis invoked synchronously the first time Surefire decides to kill a fork, before the KILL signal is dispatched.onForkExitedis invoked once the OS has reaped the process. - Single fire per fork — both callbacks fire at most once per forked JVM, even if the timeout poll observes the condition repeatedly.
- Bounded execution — each callback is invoked on an internal cached thread pool and cancelled after 30 seconds. A misbehaving extension cannot stall test execution.
- Isolation — any
Throwablethrown by an extension is logged at warn-level and never affects the test result. - Classpath — extensions are loaded with
ServiceLoader.load(ForkedProcessTimeoutExtension.class, ForkedProcessTimeoutExtension.class.getClassLoader()), i.e. the plugin classloader, not the project test classpath. - Failsafe — the SPI works identically for
maven-failsafe-plugin. Register the extension as a dependency of the failsafe plugin and use the sameforkedProcessTimeoutExtensionContextparameter.

