When to Use
Invoke this skill when operating live trading engines and market data workers subject to planned deployments, rolling restarts, or container terminations (SIGTERM). Abruptly killing worker threads drops in-flight tick events, leaves database batch writes incomplete, and leaves message-consumer offsets in a state that either replays or skips data on restart. This skill traps OS termination signals, closes the ingress gate, drains queued ticks to the sink, commits offsets in the correct order, and exits with a deterministic status code.
When NOT to Use
- Open orders or live positions are in flight. This skill drains data queues, not order state. A process holding working orders needs an explicit unwind/cancel decision first — see
strategy-decommissioning-and-position-unwind-procedureandexecution-algorithm-kill-switch-integration. - Unplanned failure. Power loss, OOM-kill and
SIGKILLdeliver no signal and run no handler. Durability under those conditions comes from sink write-ahead behaviour and offset semantics, not from this skill. - The queue is the system of record. If ticks are only in process memory, a drain timeout still loses them. Bound the exposure upstream with
backpressure-drop-degrade-policy.
Prerequisites
- Ingestion pipeline with queue worker threads or async event loops.
- A shutdown entry point on the main thread of the main interpreter — Python executes signal handlers only there, and
signal.signal()raisesValueErrorif called from any other thread. - Known supervisor grace period, because it — not the process — decides when
SIGKILLlands. Defaults: KubernetesterminationGracePeriodSeconds30s,docker stop10s (Linux containers), systemdDefaultTimeoutStopSec90s. - Max drain timeout $T_{\text{max_drain}}$ derived from that grace period, not picked arbitrarily: use
resolve_drain_timeout().
Workflow
-
Register OS Signal Traps (main thread only):
- Intercept
SIGINT(Ctrl+C) andSIGTERM(K8s/Docker/systemd shutdown). - Check the return value. If registration fails you are running unsupervised — fail the deployment rather than logging and continuing.
- The handler must only set flags. Do not flush, write or block inside it.
- Intercept
-
Size the Drain Budget Against the Supervisor:
- Compute $T_{\text{max_drain}}$ = grace period − preStop − exit reserve. On Kubernetes the grace-period countdown starts before the preStop hook runs and the hook must finish before
SIGTERMis delivered, so preStop time comes out of the same budget. - If the drain needs longer than the platform default, raise
terminationGracePeriodSeconds— do not raise the drain timeout alone, orSIGKILLwill cut the drain mid-flush.
- Compute $T_{\text{max_drain}}$ = grace period − preStop − exit reserve. On Kubernetes the grace-period countdown starts before the preStop hook runs and the hook must finish before
-
Close the Ingress Gate:
- Transition state to
DRAININGand reject new external ticks viais_accepting_ingress(). - Do not assume the signal means traffic has stopped: Kubernetes removes the Pod from EndpointSlices at the same time as the kubelet starts graceful shutdown, so ticks can still arrive for as long as endpoint removal takes to propagate to kube-proxy and load balancers. A
preStopsleep is what actually closes that window.
- Transition state to
-
Drain In-Flight Queue Buffers:
- Process remaining items until the queue reaches 0 or the deadline expires, measured on a monotonic clock.
- Remove items from the queue only after the sink accepts them, so a failed flush is retried rather than discarded.
- If producer threads are still touching the queue, detach batches under the lock those producers hold.
-
Flush Sinks, Then Commit Offsets — In That Order:
- Commit consumer offsets only after the sink flush has fully succeeded. Flush-then-commit gives at-least-once (a restart replays, possibly duplicating). Commit-then-flush gives at-most-once — a crash in between silently loses those ticks forever.
- If the drain was incomplete, skip the commit. Uncommitted offsets are what make the unflushed ticks recoverable on restart.
-
Deterministic Process Exit:
- Exit
0only on a fully drained queue with committed offsets; exit1on an incomplete drain, so the orchestrator and post-deploy checks can see the data-loss event.
- Exit
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Reporting a clean exit after the sink refused the data. Popping a batch off the queue and then calling the sink means a raising flush callback destroys that batch — the queue is empty, so a naive
is_clean_exit = len(queue) == 0check reports success while every in-flight tick is gone. Remove items only after the write is accepted. - Committing offsets before the flush succeeds. This converts a recoverable restart into permanent, silent data loss, and it is invisible in logs because both operations "succeeded" independently.
- Timing the drain with
time.time(). Wall clock steps under NTP correction, DST and VM resume. A backward step extends the drain past the grace period intoSIGKILL; a forward step aborts a healthy drain early. Usetime.monotonic(). - Assuming
SIGTERMmeans traffic has already stopped. On Kubernetes endpoint removal is concurrent with the signal, not before it, so new work can still arrive immediately after the handler fires. - Registering handlers from a worker thread.
signal.signal()raisesValueErroroutside the main thread of the main interpreter, and Python runs handlers only in that thread. Swallowing that error leaves the process with no graceful path at all. - A drain timeout larger than the platform grace period. A 60s drain under the 30s Kubernetes default never completes; the process is killed mid-flush every deploy.
- Unbounded drain waiting. Blocking forever on a stuck worker guarantees a hard kill and loses more than a bounded drain would.
- No escalation path. An operator watching a wedged drain needs a second
SIGINTto force exit rather than waiting out the timeout. - Long C-level calls delaying the handler. Python runs handlers at bytecode boundaries, so a long-running C call (large regex, blocking native driver call) defers shutdown until it returns.
Verification
- Enqueue 50 items, trigger a simulated
SIGTERM, verify all 50 reach the downstream sink before exit and the queue is empty. - With a sink callback that always raises, verify the items remain queued,
is_clean_exitisFalse,exit_codeis1, and offsets are not committed. - With a sink that fails twice then succeeds, verify the drain recovers within the deadline and tick order is preserved.
- Verify
resolve_drain_timeout()rejects a budget the grace period cannot cover. - Verify handler registration fails cleanly (returns
False) when attempted off the main thread. - Run
python -m unittest discover -s skills/graceful-shutdown-draining-in-flight-ticks/scriptsand confirm a 100% pass rate.
Related Skills
producer-consumer-tick-pipelinebackpressure-drop-degrade-policykafka-based-tick-distribution-at-scalesystemd-supervision-for-trading-botsblue-green-deployment-for-live-strategy-updatesstrategy-decommissioning-and-position-unwind-procedureadaptive-batch-size-tuning-under-loadstructured-logging-for-post-incident-forensics