-
Notifications
You must be signed in to change notification settings - Fork 633
UN-3695 [FIX] PG consumer — supervisor shutdown grace = VT, single shared join deadline #2153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
muhammad-ali-e
merged 5 commits into
feat/UN-3445-pg-queue-integration
from
fix/UN-3695-supervisor-grace-vt
Jul 8, 2026
+334
−11
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d879e8a
UN-3695 [FIX] PG consumer — supervisor shutdown grace = VT, single sh…
muhammad-ali-e baa0199
UN-3695 [DOC] PG queue — reference & glossary (terms + config knobs)
muhammad-ali-e 20462e0
UN-3695 [FIX] address review — validate grace override, fix shared-de…
muhammad-ali-e b38f8b3
Merge branch 'feat/UN-3445-pg-queue-integration' into fix/UN-3695-sup…
muhammad-ali-e 77e0f52
UN-3695 [FIX] SIGKILL warning log uses %.0fs to match the startup log…
muhammad-ali-e File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| # PG Queue — Reference & Glossary | ||
|
|
||
| A quick-reference dictionary for the bespoke Postgres-backed work queue that can | ||
| stand in for Celery/RabbitMQ (epic UN-3445). For the *design* of a full-execution | ||
| migration see [`9e-design.md`](./9e-design.md); this file is the **terminology + config | ||
| lookup**. | ||
|
|
||
| The transport is chosen **per org** by the Flipt flag `pg_queue_enabled` | ||
| (fail-closed to Celery), so the system runs **with and without** PG unchanged. | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Configuration — quick reference | ||
|
|
||
| All knobs are environment variables. Consumer knobs are read via | ||
| `consumer_env("<SUFFIX>", …)` → `WORKER_PG_QUEUE_CONSUMER_<SUFFIX>`. Authoritative | ||
| defaults live in the code constants (`consumer.py`, `pg_queue_consumer/supervisor.py`, | ||
| `reaper.py`); cloud overrides live in the Helm chart `values.yaml`. | ||
|
|
||
| **Units:** every duration is in **seconds**, and the **Default column shows the raw | ||
| value you set** — a plain number, e.g. `30` means 30 seconds (set `…=30`, not `30s`). | ||
| The `*_SECONDS` suffix names the unit; `POLL_INTERVAL` and `BACKOFF_MAX` are also | ||
| seconds (floats allowed). Counts (`CONCURRENCY`, `BATCH`, `MAX_ATTEMPTS`) are plain | ||
| integers. | ||
|
|
||
| ### Consumer (`WORKER_PG_QUEUE_CONSUMER_*`) | ||
| | Env suffix | One-line | Default | | ||
| |---|---|---| | ||
| | `CONCURRENCY` | Prefork consumer children per pod (1 = plain single process) | `1` | | ||
| | `VT_SECONDS` | **Visibility timeout** — how long a claimed message stays hidden before it can redeliver | `30` (chart: `9060` for file-processing ≈ 2.5h) | | ||
| | `SHUTDOWN_GRACE_SECONDS` | Graceful-drain budget (shared across all children) on SIGTERM before SIGKILL | `= VT` (floored at `30`) | | ||
| | `QUEUE` | Queue name(s) this consumer polls (comma-separated) | `default` | | ||
| | `BATCH` | Messages claimed per poll | `1` | | ||
| | `POLL_INTERVAL` | Time between polls when the queue is empty | `0.1` | | ||
| | `BACKOFF_MAX` | Max empty-queue poll backoff | `2.0` | | ||
| | `MAX_ATTEMPTS` | Max deliveries before a message is dropped as **poison** | `5` | | ||
| | `HEALTH_PORT` | Liveness HTTP port (unset → probe disabled) | unset | | ||
| | `HEALTH_STALE_SECONDS` | A poll loop idle beyond this is reported unhealthy | `60` | | ||
| | `WORKER_TYPE` | Which source worker's tasks this consumer registers (bootstrap) | — | | ||
|
|
||
| ### Reaper (`WORKER_PG_REAPER_*`) | ||
| | Env | One-line | Default | | ||
| |---|---|---| | ||
| | `INTERVAL_SECONDS` | Reaper cycle cadence (stuck-batch / orphan detection) | `5` | | ||
| | `SWEEP_SECONDS` | Retention-sweep cadence (expired `pg_task_result`, etc.) | `300` (5 min) | | ||
| | `HEALTH_PORT` / `HEALTH_STALE_SECONDS` | Reaper liveness probe | stale `30` | | ||
|
|
||
| ### Orchestration / barrier / connection | ||
| | Env | One-line | | ||
| |---|---| | ||
| | `WORKER_PG_BATCH_STUCK_TIMEOUT_SECONDS` | A barrier whose `last_progress_at` hasn't advanced this long is fast-failed by the reaper (UN-3661) | | ||
| | `WORKER_PG_ORCHESTRATOR_LEASE_SECONDS` | Leader-election lease duration for the single-orchestrator role | | ||
| | `WORKER_PG_DEDUP_RETENTION_SECONDS` | Retention for `pg_batch_dedup` rows | | ||
| | `WORKER_PG_QUEUE_CONNECT_RETRIES` / `_BACKOFF` | Reconnect attempts / backoff on a stale or broken DB connection | | ||
|
|
||
| ### Routing / flag | ||
| | Env / flag | One-line | | ||
| |---|---| | ||
| | `pg_queue_enabled` (Flipt, `PG_QUEUE_FLAG_KEY`) | Per-org gate that routes dispatch to PG; **fail-closed to Celery** on any Flipt error | | ||
| | `WORKER_PG_QUEUE_ENABLED_TASKS` | Comma-separated task names eligible for PG routing (empty → everything stays on Celery) | | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Concepts — glossary | ||
|
|
||
| **Visibility timeout (VT)** — when a consumer *claims* a message it becomes invisible | ||
| to other consumers for `VT` seconds. If the worker finishes and deletes (acks) it, it's | ||
| gone; if the worker dies without acking, the VT expires and the message **redelivers**. | ||
| VT is the queue's only "worker died" signal (there's no live broker connection like | ||
| RabbitMQ), so recovery latency ≈ VT. | ||
|
|
||
| **Claim** — an atomic `SELECT … FOR UPDATE SKIP LOCKED` that hides up to `BATCH` | ||
| ready rows for `VT` seconds and hands them to one consumer. `SKIP LOCKED` distributes | ||
| work across children and replicas without contention. | ||
|
|
||
| **Redelivery / at-least-once** — a message can be delivered more than once (VT expiry | ||
| after a crash, or a poison re-park). Handlers must tolerate re-execution. | ||
|
|
||
| **Near-exactly-once** — writes to an *external* system (e.g. a customer destination | ||
| DB) can't be transactional with our Postgres markers, so a crash in the tiny | ||
| write→mark gap can duplicate. Guards (FileHistory, etc.) reduce this to a narrow | ||
| window, but it isn't strictly exactly-once. | ||
|
|
||
| **Poison message / re-park** — a message that fails `MAX_ATTEMPTS` times. Rather than | ||
| redeliver forever it is dropped (or re-parked with `POISON_REPARK_VT_SECONDS`, bounded | ||
| by a budget) so a permanently-bad row can't wedge the queue. | ||
|
|
||
| **Prefork supervisor / fleet** — `WORKER_PG_QUEUE_CONSUMER_CONCURRENCY > 1` forks N | ||
| isolated consumer child processes (the PG analogue of Celery `--pool=prefork | ||
| --concurrency=N`). The *supervisor* owns the liveness port, re-forks crashed children | ||
| (rate-limited), and drains them on shutdown. The *fleet* is the set of children. | ||
|
|
||
| **Shutdown grace** — on SIGTERM the supervisor waits up to `SHUTDOWN_GRACE_SECONDS` | ||
| (= VT) for children to finish their in-flight batch, using a **single shared | ||
| deadline**, then SIGKILLs stragglers. Must be ≤ the pod's | ||
| `terminationGracePeriodSeconds` (the chart sets `grace = VT + buffer`). | ||
|
|
||
| **Liveness / heartbeat** — each child stamps its last-poll time into a shared array; | ||
| the supervisor reports the *oldest* child's staleness on `/health`. Frozen during a | ||
| long task, so a wedged child goes stale and trips the probe. | ||
|
|
||
| **Reaper** — a singleton (leader-elected) sweeper that recovers **stranded** work: | ||
| fast-fails a barrier whose `last_progress_at` stalled (UN-3661), cascades a terminal | ||
| execution to its files, and sweeps expired retention rows. | ||
|
|
||
| **Barrier (`PgBarrierState`)** — the fan-in counter for a batched execution: each | ||
| file-batch decrements `remaining`; when it hits 0 the aggregating callback fires. It | ||
| carries `last_progress_at` (the reaper's liveness signal for stuck detection). | ||
|
|
||
| **`last_progress_at`** — timestamp bumped whenever a batch makes progress | ||
| (UN-3661). Lets the reaper fast-fail a stalled batch in ~stuck-timeout instead of the | ||
| full VT/6h horizon. | ||
|
|
||
| **Leader election / orchestrator lock (`pg_orchestrator_lock`)** — a DB lease that | ||
| elects a single reaper/orchestrator across replicas; released on graceful shutdown so a | ||
| standby takes over promptly. | ||
|
|
||
| **Request-reply (`reply_key`)** — a *blocking* dispatch: the caller enqueues with a | ||
| `reply_key`, the consumer stores the result under it in `pg_task_result`, and the | ||
| caller polls for it (the PG analogue of Celery's `AsyncResult`). Mutually exclusive | ||
| with the callback form. | ||
|
|
||
| **Callback dispatch (`dispatch_with_callback`, `on_success` / `on_error`)** — | ||
| fire-and-forget: after the task runs, the consumer **self-chains** the success or error | ||
| continuation onto its queue via `_chain_continuation` (the PG analogue of Celery | ||
| `link` / `link_error`). No blocking caller. | ||
|
|
||
| **Fairness** — a header carried on a dispatch (`org_id`, workload type, priority) so a | ||
| PG-routed run mirrors Celery's fair scheduling. | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Tables (`backend/pg_queue/models.py`) | ||
|
|
||
| | Table | Purpose | | ||
| |---|---| | ||
| | `pg_queue_message` | The queue itself — one row per message; claimed via `SKIP LOCKED` + VT | | ||
| | `pg_task_result` | Request-reply results / terminal task status, keyed by reply/task id; TTL'd (`expires_at`) + reaper-swept, `ON CONFLICT DO NOTHING` | | ||
| | `pg_barrier_state` | Fan-in barrier counter for batched executions (`remaining`, `last_progress_at`) | | ||
| | `pg_batch_dedup` | Batch-level dedup guard (prevents double-dispatch of a batch) | | ||
| | `pg_orchestration_claim` | Per-execution orchestration claim (`last_progress_at` for stuck detection) | | ||
| | `pg_orchestrator_lock` | Leader-election lease for the singleton reaper/orchestrator | | ||
| | `pg_periodic_schedule` | Periodic-schedule store (the Celery-Beat replacement) | | ||
|
|
||
| All PG-queue tables are **droppable side tables** that never touch | ||
| `WorkflowExecution` (UN-3533) — the PG transport can be turned off and its tables | ||
| dropped without affecting the Celery path. | ||
|
|
||
| --- | ||
|
|
||
| *Defaults here reflect the code constants at time of writing; the code | ||
| (`consumer.py` / `supervisor.py` / `reaper.py`) and the chart `values.yaml` are the | ||
| source of truth.* |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[med] New regression: a clean-exited child iterated after the shared deadline elapses is spuriously SIGKILLed and logged as "did not drain".
With one wedged child consuming the whole shared window, every subsequent child hits
_wait_for_exit(pid, deadline)withdeadlinealready in the past._wait_for_exit's loop iswhile time.monotonic() < deadline:— so it returnsFalsewithout ever callingwaitpid, even for a child that exited cleanly at T+5s._join_childrenthen SIGKILLs it and logs"did not drain within the shared %ss grace".The SIGKILL to the (already-zombie) child is harmless, but the WARNING is a false "batch was hard-killed" alarm — ironic for the PR whose whole point is to stop hard-killing in-flight batches, and it can trip exactly that alert. This is new: the old per-child code recomputed
time.monotonic() + graceper iteration, so the deadline was never already-elapsed at entry.Fix in
_wait_for_exit— pollwaitpidat least once regardless of the deadline: