发布

  • fix(gradle): prevent Gradle and Maven daemon accumulation during project graph recalculation (#35143)

    frostbyte_neo 发布于 2026-04-02 19:27:56 +00:00

    Current Behavior

    When file changes trigger rapid project graph recalculations (e.g.,
    during development with nx graph or a dev server running), each call
    to populateProjectGraph spawns a new Gradle process via
    execGradleAsync. If the previous Gradle daemon is still busy
    processing the prior request, Gradle spawns a new daemon. These
    daemons persist for 3 hours by default (Gradle's idle timeout), leading
    to dozens of orphaned java.exe processes consuming significant memory.

    Maven has a similar issue — runMavenAnalysis spawns a long-lived
    process with no timeout or cancellation support at all.

    Root Cause Analysis

    The daemon explosion happens due to three compounding issues:

    1. No cancellation of in-flight processes: When a newer project
      graph request arrives, the previous invocation continues running. Each
      concurrent invocation finds the existing daemon busy and spawns a new
      one.

    2. Windows process tree issue: On Windows, execFile with shell: true runs cmd.exe → gradlew.bat → java.exe. Node's AbortSignal only
      terminates cmd.exe (the immediate child process), leaving java.exe
      running as an orphan.

    3. No timeout for Maven: Maven analysis could run indefinitely with
      no way to cancel or time out.

    Reproduction

    1. Run nx graph in a workspace with @nx/gradle registered
    2. Rapidly modify a build.gradle.kts file (e.g., while true; do echo "// tick" >> build.gradle.kts; sleep 0.01; done)
    3. Watch java.exe processes accumulate: tasklist | grep java
      (Windows) or ps aux | grep java (Unix)
    4. Without this fix: 15+ Gradle daemons within seconds, persisting
      for 3 hours each
    5. With this fix: 1 Gradle daemon remains stable under the same
      conditions

    Expected Behavior

    When rapid file changes trigger multiple project graph recalculations:

    • The previous invocation is cancelled before starting a new one
    • Cancelled calls that have already spawned a process get their entire
      process tree killed (not just the shell wrapper)
    • Only 1 daemon remains active at any time, rather than accumulating
      dozens
    • Both Gradle and Maven have configurable timeouts with clear error
      messages

    Changes

    Gradle

    1. Self-contained cancellation in get-project-graph-lines.ts

    Moved the AbortController from
    get-project-graph-from-gradle-plugin.ts into
    get-project-graph-lines.ts, closer to where processes are spawned.
    getNxProjectGraphLines now manages its own abort controller —
    cancelling any in-flight request before starting a new one. Uses
    abort('cancelled') reason to distinguish external cancellation from
    timeout.

    2. Tree-kill on abort (exec-gradle.ts)

    Instead of passing the AbortSignal directly to Node's execFile
    (which only kills the immediate child process), we intercept the signal
    and use tree-kill to terminate the entire process tree. This ensures
    java.exe is killed along with cmd.exe and gradlew.bat on Windows.

    Maven

    3. Timeout and cancellation support for maven-analyzer.ts

    Added the same abort controller + tree-kill + timeout pattern to Maven
    analysis:

    • Configurable timeout via NX_MAVEN_ANALYSIS_TIMEOUT env var (default:
      120s local, 600s CI)
    • cancelPendingMavenAnalysis() cancels in-flight processes on repeated
      calls
    • tree-kill ensures the entire Maven process tree is killed on abort
    • Clear timeout error messages with actionable steps

    Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>

    下载附件