The Problem: PreStop Hook Lifecycle Paradox & S6 Race Conditions
The root cause of our 15+ minute deployment blocks centered on a misunderstanding of how the Kubernetes container engine manages
preStop hooks alongside the s6-overlay process supervisor. Because Kubernetes hooks are strictly blocking, the container engine entirely delays sending the global SIGTERM signal until the script completes execution.
# Orchestration configuration allowing a massive 50-minute deadlock window
spec:
terminationGracePeriodSeconds: 3000
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "/scripts/finish.sh > /proc/1/fd/1"]
The Execution Failure Flow:
-
The Blocking Loop: The legacy
finish.shscript looped checking database activity viapg_stat_activity. The moment transactions cleared or the internal timeout hit, it firedpg_ctl stop -m fastand instantly exited. -
The S6 Super-Loop Race: As the script exited, the database began its internal shutdown. However, because the
s6process supervisor layer was still fully active, it detected that the core database PID dropped. Acting as designed to ensure high availability,s6instantly spawned a brand-new instance of PostgreSQL. -
The Signal Ghost: Simultaneously, the exited script unblocked Kubernetes, causing it to issue its global
SIGTERM. This signal was intended for the dying database, but instead hit the newly spawned zombie instance. Because that new process was wrapped in a legacy login shell wrapper (su - postgres -c ...), it swallowed or misrouted the incomingSIGTERM.
The container would sit completely unresponsive in an infinite loop, entirely deaf to the orchestrator for up to 3,000 seconds (50 minutes) until hitting a hard infrastructure SIGKILL.
