fix(display): stop ttyWriter.Done from hanging after context cancel - #14119
fix(display): stop ttyWriter.Done from hanging after context cancel#14119glours wants to merge 1 commit into
Conversation
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
The core fix is correct and well-structured: replacing the blocking channel send with a close()-based doneSignal (using sync.Once for idempotency) solves the deadlock, the per-cycle fresh signal design prevents double-close panics, and the switch to unexported create/start/push in run.go/publish.go correctly avoids double-bracketing.
Two medium-severity issues were found in the changed code.
Lower-confidence findings (not posted inline)
- [low] cmd/display/tty.go:199 — TOCTOU race:
w.donecan be replaced betweenw.print()and the mutex snapshot inDone()(confidence: 🟠 weak 52/100). Requires concurrentStart()+Done()on the same writer outside the sequentialRun()wrapper — unlikely in practice but structurally present.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Fixes docker#14114: Done() blocked forever once the render goroutine had already exited via ctx.Done() (e.g. after SIGTERM), since it sent on an unbuffered channel with nobody left to receive. Closing the channel instead of sending never blocks, but a bare close() panics the moment ttyWriter runs a second Start/Done cycle on the same shared bus - which already happens today (run's nested create/start, publish's nested push, rm --stop's sequential stop then remove). Start now hands out a fresh, once-only-closable signal each cycle instead of a single shared channel, and run/publish stop opening a redundant nested bracket around calls already inside one. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
337ec55 to
a8ad922
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The fix is well-designed and correct. The core changes — replacing the shared unbuffered done channel with a per-cycle doneSignal (wrapping sync.Once) and switching from a blocking send to a non-blocking close() — soundly fix the #14114 hang without introducing any new races or panics.
Key design points verified:
sync.OnceindoneSignal.close()prevents double-close panics across sequential and nestedStart/Donecycles.Start()holdsw.mtxwhile replacingw.done/w.tickerand captures them as local variables in the goroutine, so a laterStart()reassigning the struct fields never affects the running goroutine's wait.Done()readsw.doneandw.tickerunder lock into locals before callingclose(), so it always operates on the correct cycle's signal even if a concurrentStart()races in.- Switching
run.goandpublish.goto the unexportedcreate/start/pushavoids the nestedStart/Donebracket on the shared bus, withstrings.ToLowerpreserved for parity with the public methods. - The new tests (
TestDoneAfterContextCancelDoesNotHang,TestNestedStartDoneDoesNotPanic,TestSequentialStartDoneEachGetFreshChannel,TestDoneBeforeStartDoesNotPanic) provide solid regression coverage, including goroutine-leak detection viagoleak.VerifyNone.
What I did
Fixes #14114: Done() blocked forever once the render goroutine had already exited via ctx.Done() (e.g. after SIGTERM), since it sent on an unbuffered channel with nobody left to receive.
Closing the channel instead of sending never blocks, but a bare close() panics the moment ttyWriter runs a second Start/Done cycle on the same shared bus - which already happens today (run's nested create/start, publish's nested push, rm --stop's sequential stop then remove). Start now hands out a fresh, once-only-closable signal each cycle instead of a single shared channel, and run/publish stop opening a redundant nested bracket around calls already inside one.
Related issue
closes #14114
Supersedes #14115
(not mandatory) A picture of a cute animal, if possible in relation to what you did
