worker: flush captured stderr on abnormal exit, gate trace markers behind env var#1841
Conversation
…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>
|
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. |
|
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; |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Maybe explain what's the mechanism for making sure it only fires on an abnormal exit (removeShutdownHook, I suppose)
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
realStdErrlocal 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.