Exit the supervisor loop on SIGTERM instead of spinning until SIGKILL - #67
Exit the supervisor loop on SIGTERM instead of spinning until SIGKILL#67tamalsaha wants to merge 1 commit into
Conversation
scripts/run.sh traps SIGINT/SIGTERM and sets STOP=true, but the loop condition was the literal `true` and STOP was only consulted to decide whether to start postgres again. So on SIGTERM the script correctly stopped restarting postgres and then looped forever. Since this script is the container's main process (tini -> bash run.sh -> postgres), the container could never exit on its own: every termination ended in a SIGKILL at the end of terminationGracePeriodSeconds. Two consequences, both observed live on a DC-DR cluster. Pod deletions blocked for the full grace period, five minutes on a database that sets 300, with the container idle the whole time: postgres had already logged 'database system is shut down' and run.sh had printed 'removing the initial scripts as server is not running' four minutes before the SIGKILL. And with the default 30s grace, a shutdown checkpoint slower than 30s (42s measured on a moderately busy instance) is SIGKILLed midway, leaving a data directory that needs crash recovery, which for a DC-DR primary is exactly the torn-checkpoint shape that causes divergence. The retry behavior the trap was written to protect is unaffected. STOP is only set by a signal delivered to this bash process, which in practice is kubelet stopping the container. The pg-coordinator stops postgres with `pg_ctl stop` exec'd into the container, kills pg_basebackup by matching /proc/*/comm, and signals the postmaster or a backend by pid; it never signals this process, a process group, or pid 1. So a coordinator-driven demote still returns control to the loop, which re-runs the role script when the coordinator writes the next one. Verified live: one container instance served 17 role-script starts with zero container restarts, which is only possible if STOP stayed false throughout. The PITR wait loop gets the same treatment: it too could not be interrupted. Signed-off-by: Tamal Saha <tamal@appscode.com>
Live verification: this change is correct but NOT sufficient on its own, and must not merge aloneVerified on a real cluster (3-replica Postgres, The change works when the signal reaches bashSending SIGTERM directly to the (Note for reviewers: bash defers a trap handler while a foreground child is running, so But pod deletion still takes the full grace periodThree separate deletions on the fixed image: 301s, 300s, 303s. The instrumented run The container is held open by PID 1, not by this script. -int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
+int reap_zombies(const pid_t child_pid) {
if (current_pid == child_pid) {
- ... set *child_exitcode_ptr; tini then exits ...
+ // ... commented out ...So tini never exits with its child. Observed live in-cluster:
The failure mode this change introduces without a tini fixWith the fork's tini, a Proposed complete fix
Until item 2 lands, this PR should stay open. Holding it also has a cost worth stating: |
The bug
scripts/run.shtraps SIGTERM and setsSTOP=true, but the loop condition is theliteral
true;STOPis only consulted to decide whether to start postgres again:So on SIGTERM the script does exactly what commit 06ba93a promised (stop restarting
postgres) and then spins forever. This script is the container's main process
(
tini -> bash run.sh -> postgres), so the container can never exit on its own:every termination ends in a SIGKILL at the end of
terminationGracePeriodSeconds.Impact, observed live on a DC-DR pair
terminationGracePeriodSeconds: 300took 5 minutes to delete, idle almost thewhole time. Its own logs show postgres finished shutting down at 09:03:35 and
run.shimmediately printedremoving the initial scripts as server is not running ...(the loop regaining control) — then nothing until the SIGKILL at09:07:34, exactly the deletion deadline.
shutdown on a moderately busy instance took 42s. Past the ceiling the postmaster
is killed part-way through its shutdown checkpoint, leaving a data directory that
needs crash recovery — for a DC-DR primary, precisely the torn-checkpoint shape
behind timeline divergence incidents.
The fix
while [[ "$STOP" = false ]]; do(same for the PITR wait loop, which had the sameproblem). After SIGTERM the current iteration finishes and the script exits, so the
container stops in about a second once postgres is down, and the grace period goes
back to being a ceiling rather than a fixed cost.
Why this does not break the restart-on-role-change behavior
The concern is real and was checked before changing the line: pg-coordinator stops
postgres on its own (demote, fence, rewind), and the loop must keep retrying so the
next role script gets picked up. It still does, because a bash trap only fires for
signals delivered to that bash process, and no coordinator path signals it:
TerminatePostgres(gRPC + fallback)pg_ctl -m fast|immediate -w stopexec'd into the postgres containerShutDownCleanlypg_ctlpathKillBaseBackupProcess/proc/*/comm, kills only wherecomm == pg_basebackupkill -HUP $(head -1 postmaster.pid),kill -TERM $walreceiver_pidNo
pkill, nokillall, no process-group or pid-1 kill anywhere. Killing a childonly makes bash's
waitreturn, which is the existing retry path.Live proof: a single postgres container instance (restartCount unchanged) served
17
running the initial script ...starts across many coordinator-driven rolechanges. That is only possible if
STOPstayedfalsethe whole time — if any ofthose kills had reached bash, the
[[ "$STOP" = false ]]guard would have blockedevery subsequent start and postgres would never have come back.
Caveat for future readers: this holds because
tiniruns without-g. With groupmode, a container SIGTERM would also hit postgres directly and bypass the
pg_ctlfast-stop ordering.
Testing
bash -nclean.exited on its own after SIGTERM.
in the PR discussion (bootstrap, role transitions, and deletion timing).