2026-06-14 — Service Bus unified ingress: Tiers 2-5 of #36¶
Builds on the Tier 1 consumer-as-writer change. All behavioural switches ship default-OFF so the live submit contract only changes by explicit opt-in (charter §12a Rule 4).
Motivation¶
Tier 1 made the Service Bus consumer the single writer of job state. Tiers 2-5 complete the unified-ingress design from #36: let the dashboard funnel its own submits through Service Bus, drain them with low latency, deliver optional push events to external services idempotently, and document the external-consumer contract.
User-facing change¶
- Tier 2 — optional submit-to-SB front door (
ENABLE_SB_SUBMIT_INGRESS, default-OFF). When enabled (and Service Bus is on),POST /api/v1/elastic-blast/submitenqueues the request to Service Bus instead of calling/v1/jobsdirectly, returning the dashboard correlation id immediately. A publish failure falls back to the direct path (break-glass), so a Service Bus blip never drops a submit. When the flag is off the historical direct path is unchanged. - Tier 3 — optional resident consumer (
SERVICEBUS_RESIDENT_CONSUMER, default-OFF). When enabled, a resident long-polling consumer on the worker drains the request queue within ~1 s instead of waiting the 30 s beat. The beat drain task stays registered as the fallback reconcile, so the resident loop is an accelerator, never a single point of failure. The loop is bounded, interruptible (stops promptly), and backs off (capped) on a drain error instead of hot-looping. - Tier 4 — idempotent optional completion events. Every
blast.transitionevent published to a configured completion topic now carriesevent_id(stable percorrelation_id+status) andattempt(1 on first publish, ≥2 on re-publish), so an external subscriber can dedupe an at-least-once redelivery.result_refcontinues to carry pointers only (never result bytes; charter §9). - Tier 5 — external-consumer result contract documented in
architecture/service-bus-integration.md:
pull (poll by
external_correlation_id) plus optional push (subscribe a configured completion topic), dedupe-on-event_id, pointers-not-bytes, and the status poll as the canonical fallback so a missed event is never a lost result.
Design notes (self-critique)¶
- Both runtime switches require the gate env AND
service_bus_enabled()— a half-configured deployment never drops a submit into a void. - The enqueue helper raises on a publish failure (does not swallow) so the route can fall back to the direct submit; a swallowed failure would lose the job.
- The resident loop never raises out of its body, exits promptly on stop, and backs off on error — a stuck/erroring consumer cannot hot-spin or wedge.
event_idis deterministic so dedupe needs no shared state across the subscriber and the dashboard.
API / IaC diff summary¶
No API surface change. New default-OFF env flags (documented in the architecture
page): ENABLE_SB_SUBMIT_INGRESS (api), SERVICEBUS_RESIDENT_CONSUMER (worker).
Internal:
api/services/blast/submit_ingress.py(new) — gate + enqueue helper.api/services/blast/resident_consumer.py(new) — resident drain loop lifecycle.api/tasks/servicebus/tasks.py—_event_id/_transition_eventidempotency builder applied to all three publish points.api/routes/elastic_blast.py— submit route enqueues when gated on, falls back to direct on failure.api/celery_signals.py— worker start/shutdown manage the resident consumer.
Validation evidence¶
uv run pytest -q api/tests→ 3599 passed, 3 skipped.- New tests:
test_submit_ingress.py(6),test_resident_consumer.py(7), plus Tier 4 cases intest_servicebus_tasks.py(_event_iddeterministic, transition event idempotency keys). uv run ruff checkclean;mkdocs build --strictsucceeds; frontmatter guard passes.