fix(tail): flush stdin write buffer on SIGINT/SIGTERM - #307
Open
detail-app[bot] wants to merge 1 commit into
Open
detail-app[bot] wants to merge 1 commit into
detail-app[bot] wants to merge 1 commit into
Conversation
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — The fix addresses a clear stdin flush bug and adds strong EOF, SIGINT, SIGTERM, and FIFO coverage. However, it also replaces the existing stdin path with new worker-thread, signal-mask, and cooperative-shutdown coordination, creating enough concurrency and shutdown complexity to warrant human review. You can add or adjust custom eligibility rules. Learn more. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detail bug report: View on Detail
Fixes ENG-708
Bug
edge-tailin stdin mode (edge-tail/edge-tail -) lost up to ~64 KiB of output onSIGINT/SIGTERM. The stdin path (runStdinToOutput→Runtime.runStream) never installed a signal handler and only flushed the write buffer afterframer.pumpreturned (i.e. on stdin EOF). On Ctrl-C / supervisor stop the process was hard-killed with default signal disposition before that loneoutput.flush()could run, dropping the write-buffer residual (bounded bywrite_buf, 64 KiB default). The file-tailing path was thought to be unaffected, but its signal handling is also broken (see out-of-scope note below).During investigation I found the report's "mirror the file loop" sketch wouldn't have worked: (1) the Threaded Io backend only cancels syscalls on registered worker threads (
Thread.currentis set solely inThreaded.worker), so a blockingreadvon the main thread can't be interrupted — the pump must run as a lifecycle task; (2)File.Reader.readVecStreamingconverts the cancel'serror.Canceledintoerror.ReadFailed, andreadSliceShortfills the full buffer (blocking mid-fill on a held-open pipe) while stranding a prefetched ~64 KiB in the reader's internal buffer — a second loss term the report didn't account for.Fix
Apply structured shutdown to the stdin path, mirroring the file loop's pattern but with two corrections the file loop is missing:
lifecycletask (StdinLoop) on a worker thread soGroup.cancelcan interrupt the blocking read.SIGINT/SIGTERM/SIGUSR1viainstallSignalWaiterbefore spawning the worker so the worker inherits the blocked mask; otherwise the signal lands on the worker with default disposition and hard-kills the process beforesigwaitcan act.framer.pumpFileStreamingreading stdin viafile.readStreamingdirectly — one shortreadvper iteration, no reader-side internal-buffer prefetch, anderror.Canceledpropagates natively (not converted toReadFailed). This eliminates both the read-buffer prefetch loss and theReadFailedmisclassification.requestShutdown(sigwait thread) → main threadawaitShutdown→group.cancelinterrupts the readv → pump unwinds → main threadoutput.flush()drains the residual. On EOF the pump completes and wakes the main via a newLifecycle.requestShutdownQuiet(clean stderr for the commonecho | edge-tail -case); the main cancels/joins viagroup.cancel(no "all tasks drained" log).Testing
pumpFileStreaming(short reads across line boundaries → byte-exact EOF output) and one for theStdinLooplifecycle coordination (spawn → awaitShutdown → cancel → main-thread flush → bytes on disk).bench/logging/tests/test_stdin.py: SIGINT mid-stream flushes residual, SIGTERM mid-stream flushes residual, and the FIFO (named-pipe, write-end held open) SIGINT scenario from the bug report — all assertexit == 0and output == input (0 bytes lost).zig build test(521 passed, 1 pre-existing skip),zig fmt --check,ziglint,zig build(all targets), and a fresh ReleaseSafezig build tailall pass.lost=0, exit=0: EOF baseline (clean stderr, full data), SIGINT and SIGTERM mid-stream on an anonymous pipe, an input-size sweep (32 KiB–1 MiB), and the FIFO scenario. Pre-fix the same scenarios gaveexit=-2with ~64 KiB lost.test_checkpoint_recoverywith SIGTERM/SIGKILL,test_concurrency_stress, plus append/output/startup/glob/policy/rotation/perf-guards modules) all pass — the file-tailing path is unchanged by this fix.signalWaiterThread'scount==2 → std.process.exit(1)): POSIX standard signals coalesce when blocked viasigprocmask, and the now-reliable ~5 ms cooperative shutdown completes before a deliberately-delayed second signal is consumed separately, so every double-kill trial exits 0. The code path is structurally unchanged (src/tail/runtime.zig:72-85) and remains the safety valve for stuck shutdowns.test_over_limit_line_is_truncated,test_path_missing_then_reappears) confirmed viagit stash+ rebuild to fail identically on the unmodified tree — not regressions.Out of scope
The file-tailing loop (
runFilesLoopBackend) spawns its workers before blocking signals, so itsSIGINT/SIGTERMhandling has the same hard-kill defect (verified:edge-tail <growing-file>dies with exit -2 on SIGINT). The bug report assumed the file loop worked; it does not. Fixing it requires reordering signal blocking beforecheckpoint.start/lifecycle.spawnthere — a separate change.Automatic Fixes PRs can be configured here.