fix(ingest-hub): survive dead conmon pipe instead of dying as a zombie#104
Conversation
Supervisord 4.2.5's logger kills PID 1 when the container's stdout pipe loses its reader (dead conmon): emit() catches the failed write, but handleError() prints the traceback to stderr — the same dead pipe — and the second BrokenPipeError escapes the main loop. Reproduced locally; caused the 2026-07-02 Discord tools outage (see docs/incidents/). Run supervisord with fd 2 on /dev/null so handleError() always succeeds, after a config-validation phase that still has live stderr; merge child stderr into the stdout channel so it still reaches podman logs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1McPhcL927v8qNdsFcLVu
mruwnik
left a comment
There was a problem hiding this comment.
Hygieia (Claude Opus 4.8): Approve. This is a correct, well-scoped, and unusually well-documented fix. I traced the mechanism and the fix end-to-end:
Root cause is right. supervisord 4.2.5's loggers.Handler.emit() catches the stdout-pipe BrokenPipeError and calls handleError(), which traceback.print_exception(..., sys.stderr). With fd 2 also on the dead conmon pipe, that write raises a second BrokenPipeError out of emit(), and the dispatcher's handle_error() → logger.critical() third fault escapes runforever() → PID 1 exits. stdlib logging guards this (except OSError: pass, bpo-5971); supervisor's homegrown logger does not. Confirmed against the 4.2.5 source.
The fix closes it at the root. Pointing fd 2 → /dev/null means handleError()'s write always succeeds, emit() returns normally (no re-raise), and every log-write path — including the logger.critical() in the dispatcher's error handler and supervisord's forwarding of child output to /dev/stdout — degrades to a silently-swallowed failure instead of a cascade. Container becomes logless-but-serving, exactly as claimed.
The two-phase ENTRYPOINT is the right call and correctly built:
- Shell parsing/redirection is correct:
2>/dev/nullbinds only toexec supervisord, so the validationpython3 -cruns with live stderr and surfaces config errors (exit 2), while only the long-lived supervisord gets fd 2 on /dev/null. execkeeps supervisord as PID 1 (SIGTERM handling onpodman stoppreserved).- Explicit
/usr/bin/python3is necessary and consistent: apt installs supervisor into Debian'sdist-packages(reachable from/usr/bin/python3, matching/usr/bin/supervisord's shebang), not the image's/usr/localpython — so validation imports the same supervisor that runs.
supervisor.conf change is consistent. redirect_stderr=true merges child stderr into the stdout channel so fd 2 → /dev/null doesn't discard uvicorn/celery stderr; it still reaches podman logs via /dev/stdout. The forwarding writes are themselves protected by the same guarded logger.
Scope is sensibly limited to "don't die," with zombie auto-remediation and dead-log-capture detection correctly deferred to follow-ups in the incident doc.
Only a non-blocking nit (readability of the inline sh -c) and a request to make the realize()-is-side-effect-free-at-uid-kb invariant explicit — see inline comment. No blocking issues; ready to merge (remember the image rebuild on deploy, compose build ingest-hub).
…y.sh Review nit on #104: the inline sh -c string was dense and untestable; the script form also deduplicates the conf path and documents the ServerOptions.realize no-side-effects invariant where it's relied on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1McPhcL927v8qNdsFcLVu
|
Hygieia (Claude Opus 4.8): Re-reviewed at ef6795f — the entry.sh extraction is clean and both of my earlier points are fully addressed. Still approve. Checked the new
Dockerfile wiring is correct: COPY + The realize()-is-side-effect-free-at-uid-kb invariant is now documented in the script, and your 4.2.5 source trace matches mine. No remaining issues; ready to merge (remember |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1McPhcL927v8qNdsFcLVu
Problem
On 2026-07-02,
chris-ingest-hub-1died silently and podman kept reporting it "Up" (zombie container, dead conmon), taking down all Discord MCP tools for ~84 minutes. Investigation (incident doc included) found the death mechanism and reproduced it locally:Supervisord 4.2.5's homegrown logger kills PID 1 when the container's stdout pipe loses its reader:
/dev/stdoutraisesBrokenPipeError;emit()catches it and callshandleError().handleError()prints the traceback tosys.stderr— the same dead pipe — raising a secondBrokenPipeErrorthat escapesemit().handle_error()callslogger.critical(...)→ thirdBrokenPipeErrorinside the main loop'sexcept:→ escapesrunforever()→ PID 1 exits, taking celery-beat and discord-api with it. No dying words possible by construction.stdlib
loggingguards this exact path (except OSError: pass, bpo-5971); supervisord's logger doesn't, making ingest-hub the only vulnerable container in the stack.Fix
ServerOptions.realize()while stderr is live (config errors still surface at container start, exit 2), thenexecsupervisord with fd 2 →/dev/nullsohandleError()can never double-fault.redirect_stderr=trueon both programs so child stderr reachespodman logsvia the stdout channel instead of being swallowed by the new fd 2.A dead conmon now degrades the container to logless-but-serving instead of a silent whole-container exit.
Verification
redirect_stderr=True,stderr=Noneon both programs).Needs an image rebuild on deploy (
compose build ingest-hub), not just a restart.🤖 Generated with Claude Code
https://claude.ai/code/session_01U1McPhcL927v8qNdsFcLVu