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.py →
WORKER_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.