-
feat(gradle): stream batch task results to nx as they finish (#35487)
发布于
2026-05-01 21:09:04 +00:00 Current Behavior
The Gradle batch executor (
@nx/gradle:gradlein batch mode) returns a
Promise<BatchResults>. The Kotlin batch runner serializes the entire
result map to a single JSON blob andprintlns it once at the end of
the run. The Node-side executor accumulates stdout chunks and
JSON.parses them after the JVM exits, so Nx only learns about task
outcomes in one burst when the whole batch is done.The Maven batch executor was migrated to streaming a while ago — it
returns anAsyncGeneratorand the Kotlin runner emits
NX_RESULT:{json}lines as each task finishes — but the Gradle batch
executor was never updated to match.Expected Behavior
The Gradle batch executor now mirrors the Maven batch executor's
streaming protocol, with per-task results streamed live during both
build and test task execution.Kotlin runner (
packages/gradle/batch-runner)ResultEmitterwritesNX_RESULT:{json}lines to stdout, one per
task, with a thread-safe dedupe set so emission can happen from
build/test listeners without double-reporting.runBuildLauncheremits per build task asTaskOutputCapture
detects the next task's> Task :foo:barheader, with an end-of-build
flush for the final task.runTestLauncheremits per Nx test task at its class-level
TestFinishEvent, with aTaskFinishEventfallback for tasks that
never produced a class event (compile failure, exclusion). Method-level
failures are sticky so a later passing method in the same class can't
mask an earlier failure.NxBatchRunner.mainends withexitProcess(0)so lingering
non-daemon threads from the Gradle Tooling API can't keep the JVM alive
after task work completes. The trailingresults.forEach { emit }loop
now only coversfinalizeTaskResults-synthesized entries
(excluded/skipped tasks).
Node executor
(
packages/gradle/src/executors/gradle/gradle-batch.impl.ts)gradleBatchis now anasync function*returningAsyncGenerator<{ task; result: TaskResult }>.streamTasksInBatchspawns the JVM, reads stdout viareadline, and
drainsNX_RESULTlines into an in-memory queue, yielding from the
queue. Yielding inside the readline loop creates back-pressure — slow
consumers blockyield, readline pauses, the OS pipe between Java and
Node fills, and Java'sprintlnblocks on a full pipe. The queue
decouples reading from yielding so back-pressure can no longer deadlock
the JVM.- Stderr stays inherited so Gradle/JUnit progress flows to the terminal
in real time. - Tasks the runner never reports get yielded as failed at the end so Nx
never hangs.
Project graph dependency
packages/gradle/project.jsonadds:gradle-batch-runnerto
implicitDependencies. The gradle package bundles the batch-runner JAR
and references it at runtime viabatchRunnerPath; without this,nx affectedwouldn't pick up gradle when only the runner changed.Bug along the way: className format mismatch
RegexTestParser.ktrecordstestClassNameas the simple class
name (e.g.MyTest), but Gradle's
JvmTestOperationDescriptor.classNameis the fully qualified name
(com.example.MyTest). The exact-match lookup in the test listener was
failing for every class, so per-classTestStartEvent/TestFinishEvent
never matched an Nx task — every Nx task fell through to the
TaskFinishEventfallback at the end of the Gradle test task, all
sharing the same emission time and the cumulative shared output buffer.
resolveNxTaskIdnow looks up by FQN first, then by the suffix after
the last., so events match either format.Known trade-off
Tests under the same Gradle test task share the captured per-Gradle-task
output forterminalOutput. With JUnit--parallelthe bytes
interleave anyway, and Gradle'sTestLauncherdoesn't expose per-test
stdout segmentation throughsetStandardOutput— getting truly
per-classterminalOutputwould require either subscribing to
OperationType.TEST_OUTPUT(which diverts stdout away from the standard
output stream and didn't reliably fire for some setups in testing) or
recording fully-qualified class names in the project-graph plugin so we
can matchTestOutputEventparents precisely. Filed as a follow-up.The on-the-wire change matches the existing Maven contract
(run-batch.tsalready special-casesisAsyncIterator), so no Nx core
changes are needed.Related Issue(s)
下载附件