Skip to content

worker: flush captured stderr on abnormal exit, gate trace markers behind env var#1841

Open
gergelyfabian wants to merge 1 commit into
bazel-contrib:masterfrom
gergelyfabian:worker-stderr-on-abnormal-exit
Open

worker: flush captured stderr on abnormal exit, gate trace markers behind env var#1841
gergelyfabian wants to merge 1 commit into
bazel-contrib:masterfrom
gergelyfabian:worker-stderr-on-abnormal-exit

Conversation

@gergelyfabian

@gergelyfabian gergelyfabian commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

The persistent-worker loop redirects System.out / System.err into an in-memory buffer (outStream) so the bytes ride back in WorkResponse.output. When the JVM exits before the next WorkResponse is written — direct System.exit(N) from inside work() (scalac's reporter, a compiler plugin, any wrapper that bails via Runtime.exit), uncaught Throwable unwinding the main thread, signal-induced shutdown that still runs hooks — the captured buffer dies in memory and the worker server receives nothing but an EOF on the codec. SIGKILL, native JVM crashes, and Runtime.halt are unavoidably uncovered.

Snapshot System.err into a realStdErr local before any setErr() call and install a JVM shutdown hook that, on every termination path that runs hooks, flushes the in-flight outStream onto realStdErr (still wired to the worker server's pipe). The normal-completion path resets outStream after every request, so a clean stdin-EOF shutdown leaves the buffer empty; the hook is removed from the runtime in the outer finally on normal exit. Track in-flight argv and a completed-request counter so the abnormal-exit diagnostic can name the action that was running. Captured tail is bounded to 64 KB (last bytes wins; the failure-causing output is typically at the end), preventing a runaway scalac from flooding the worker server's stderr ring buffer.

Per-request start/end markers and lifecycle banners are gated behind the RULES_SCALA_WORKER_TRACE env var — off by default, since the shutdown-hook diagnostic already names the in-flight argv. Set to a truthy value (e.g. RULES_SCALA_WORKER_TRACE=1) when debugging worker behavior.

Tests cover the summarizeArgv helper and exercise the shutdown-hook / trace-mode paths via a forked JVM subprocess (testing them in-process would require taking the test JVM down).

The ephemeral (non-persistent) path is unchanged; it never hijacks the streams and so never needed the recovery path.

Motivation

Used to debug #1840.

…hind env var

The persistent-worker loop redirects System.out / System.err into an
in-memory buffer (outStream) so the bytes ride back in WorkResponse.output.
When the JVM exits before the next WorkResponse is written — direct
System.exit(N) from inside work() (scalac's reporter, a compiler plugin,
any wrapper that bails via Runtime.exit), uncaught Throwable unwinding the
main thread, signal-induced shutdown that still runs hooks — the captured
buffer dies in memory and the worker server receives nothing but an EOF on
the codec. SIGKILL, native JVM crashes, and Runtime.halt are unavoidably
uncovered.

Snapshot System.err into a `realStdErr` local before any setErr() call and
install a JVM shutdown hook that, on every termination path that runs
hooks, flushes the in-flight outStream onto realStdErr (still wired to the
worker server's pipe). The normal-completion path resets outStream after
every request, so a clean stdin-EOF shutdown leaves the buffer empty; the
hook is removed from the runtime in the outer finally on normal exit.
Track in-flight argv and a completed-request counter so the abnormal-exit
diagnostic can name the action that was running. Captured tail is bounded
to 64 KB (last bytes wins; the failure-causing output is typically at the
end), preventing a runaway scalac from flooding the worker server's
stderr ring buffer.

Per-request start/end markers and lifecycle banners are gated behind the
RULES_SCALA_WORKER_TRACE env var — off by default, since the shutdown-hook
diagnostic already names the in-flight argv. Set to a truthy value
(e.g. RULES_SCALA_WORKER_TRACE=1) when debugging worker behavior.

Tests cover the summarizeArgv helper and exercise the shutdown-hook /
trace-mode paths via a forked JVM subprocess (testing them in-process
would require taking the test JVM down).

The ephemeral (non-persistent) path is unchanged; it never hijacks the
streams and so never needed the recovery path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gergelyfabian

Copy link
Copy Markdown
Contributor Author

I'm not sure this should land in master, just sending for the record. Very helpful for debugging if anything goes south in error handling.

@PawelLipski PawelLipski changed the title worker: flush captured stderr on abnormal exit, gate trace markers be… worker: flush captured stderr on abnormal exit, gate trace markers behind env var Jun 24, 2026
@PawelLipski

Copy link
Copy Markdown
Collaborator

Hey, thanks for the change - let me add some comments on possible test improvements/additions, otherwise good to go

AtomicReference<String> inFlightArgv,
AtomicLong requestsCompleted,
String prefix) {
final int maxTailBytes = 64 * 1024;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test for this truncation logic as well (AFAICS not covered yet)

String inFlight = inFlightArgv.get();

realStdErr.println();
realStdErr.println(prefix + "abnormal-exit diagnostic");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test for happy path - hook not run, no abnormal-exit diagnostic printed out if the worker shuts down successfully? in case the logic around removeShutdownHook is ever removed

}

/**
* Installs a JVM shutdown hook that, on abnormal exit, flushes the persistent worker's captured

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explain what's the mechanism for making sure it only fires on an abnormal exit (removeShutdownHook, I suppose)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants