Skip to content

DR-099 — processing_queue consumers claim only the job types they can process; type scope is declared at the claim, not policed after the fact

Context. The queue ran two consumers with disjoint handler coverage and unscoped global claims: app/api/cron/process-queue (handles form_draft_all + batch_reclassify, permanently fails everything else via no_handler_registered) and scripts/bid_worker.py (handles template_fill + analyse_form, marks everything else failed via its unknown-type ValueError). Whichever consumer won the claim race destroyed the other’s job — measured: 24 falsely-failed form_draft_all rows on Platform staging (17–26/07/2026, the staging bid-worker’s 2 s poll), the root cause of the 15-day integration-lane redness (id-372) and the production-latent mirror of the S481 corruption class.

Decision. claim_next_job carries p_job_types (include list, empty claims nothing — fails closed) and p_exclude_job_types (migration 20260726231847, applied Platform staging + prod). Every consumer MUST claim with a type scope: special-purpose workers pass their include list (bid_worker.pyWORKER_JOB_TYPES = ['template_fill','analyse_form'], mirrored in lib/queue/worker-job-types.ts, pinned by a py↔ts parity test); the general cron consumer passes the worker list as its EXCLUDE scope so its PermanentJobError default remains the queue’s loud dead-letter for genuinely unhandled types. A new consumer with an unscoped claim re-opens mutual job destruction and is a defect.

Alternatives considered. (a) Include lists on both consumers — rejected: the cron’s list would need hand-maintenance as types are added, and a forgotten entry leaves rows invisibly pending forever instead of dead-lettering loudly. (b) Worker-side type checks without claim scoping — rejected: the row is already claimed by then; failing it is the defect. (c) Separate queues per consumer — disproportionate for two consumers; revisit only if consumer count grows.

Consequences. Both client DBs need 20260726231847 before any client release pin advances past PR #133 (new worker code against the old 1-arg fn is an RPC error loop). The no-arg call remains valid (NULL = unscoped) for migration-window compatibility — the rule that consumers must not USE it is this register entry, deliberately not a hard constraint at the fn level.