Skip to content

fix(cli): discard log writes to closed pipes during shutdown#26082

Open
EhabY wants to merge 1 commit into
mainfrom
fix/clilog-discard-closed-pipe-writes
Open

fix(cli): discard log writes to closed pipes during shutdown#26082
EhabY wants to merge 1 commit into
mainfrom
fix/clilog-discard-closed-pipe-writes

Conversation

@EhabY
Copy link
Copy Markdown
Contributor

@EhabY EhabY commented Jun 4, 2026

What

Add clilog.DiscardOnPipeError, a small io.Writer wrapper that drops writes which fail because the destination pipe is gone (io.ErrClosedPipe, syscall.EPIPE), and apply it to the log sinks coder attaches to CLI stdio: the clilog /dev/stdout and /dev/stderr sinks, and the port-forward -v verbose sink.

Why

Background goroutines keep logging after a CLI command starts tearing down, once the reader on the log destination has gone away. The most visible case is coder port-forward -v, whose tailnet goroutines log into inv.Stdout after the consumer is gone (a closed in-memory pipe in tests, a broken OS pipe in real use such as coder port-forward -v | head).

slog reports those failed writes to stderr, which is noise and, through its syncwriter, can interleave with and corrupt go test/test2json output, so a passing test gets misreported as failed. We saw this in a real CI run where a passing TestRetryWithInterval was marked unknown:

sloghuman: failed to write entry: io: read/write on closed pipe--- PASS: TestRetryWithInterval (0.00s)

The slog-side fix for the corruption is coder/slog#225 / coder/slog#226. Per maintainer guidance there, the suppression of these benign "reader gone" errors belongs in coder (the consumer that owns the closing writer), not inside slog, so a user-assigned sink failing is never hidden globally.

Approach

The discard logic lives in one place (clilog.DiscardOnPipeError) and is applied at the two strategic sinks. os.ErrClosed and every other write error are still returned, so a write to a writer we closed ourselves is not hidden. This is intentionally not wrapped globally on inv.Stdout/inv.Stderr, which would change normal CLI pipe semantics (for example, coder list | head should stop on a broken pipe rather than silently keep writing). Other ad-hoc sloghuman.Sink(inv.Std*) sites can opt in with the same one-call wrap if they show the same noise.

Testing

  • TestDiscardOnPipeError: discards io.ErrClosedPipe/syscall.EPIPE (including wrapped), still reports os.ErrClosed and other errors, and passes through on success.
  • go build ./cli/..., go vet, gofmt, and golangci-lint run ./cli/ ./cli/clilog/ are clean.
Why discard rather than report

These writes only fail during shutdown, after the reader is already gone, so the entries had nowhere to land regardless. The change is scoped to log sinks coder attaches to stdio, not slog's global behavior, and it does not affect normal command output. os.ErrClosed stays reported to avoid masking a genuine write to an already-closed writer.


Generated by Coder Agents on behalf of @EhabY.

Background goroutines keep logging after a CLI command starts tearing
down, when the reader on the log destination has already gone away. The
most visible case is "coder port-forward -v", whose tailnet goroutines
log into inv.Stdout after the consumer is gone, but the same race exists
for any verbose CLI sink (a closed in-memory pipe in tests, a broken OS
pipe in real use).

slog reports those failed writes to stderr, which is noise and, via its
syncwriter, can interleave with and corrupt go test/test2json output (a
passing test gets misreported as failed). See coder/slog#225.

Add clilog.DiscardOnPipeError, a writer wrapper that drops writes failing
with io.ErrClosedPipe or syscall.EPIPE while still returning every other
error (including os.ErrClosed, which usually means a write to a writer we
closed ourselves). Apply it to the clilog stdout/stderr sinks and the
port-forward verbose sink. Other ad-hoc sinks can opt in the same way.

This keeps the opt-out in coder rather than hiding sink errors inside
slog.
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.

1 participant