Fix worker crash-loop that failed and slowed BLAST jobs¶
Motivation¶
Most jobs on /blast/jobs were failing and even the successful ones were slow.
Investigation (App Insights appi-elb-dashboard + worker console logs on
ca-elb-dashboard) found the elb-worker Celery prefork pool in a permanent
crash loop:
ForkPoolWorkerindices had climbed past 8200 (thousands of children spawned and killed).- The master repeatedly logged
Timed out waiting for UP message from <ForkProcess(...)>immediately followed byProcess '...' exited with 'signal 9 (SIGKILL)'andWorkerLostError: Worker exited prematurely: signal 9 (SIGKILL). tracesshowedazure monitor opentelemetry initialised for role=worker~349 times in one hour (≈ one child re-init every ~10 s).
Root cause chain:
init_telemetryruns inside every prefork child via theworker_process_initCelery signal.- It called
configure_azure_monitor(enable_live_metrics=True). Live Metrics (QuickPulse) opens a dedicated streaming exporter + thread per child — one stream for each of the 6 prefork children (main concurrency 4 + artifact 2). - On the worker sidecar's 0.5 vCPU budget, that per-child boot work exceeded
billiard's
worker_proc_alive_timeout(~4 s default), so the master declared each child lost,SIGKILL'd it, and forked a replacement — forever. - With
task_acks_late=True+task_reject_on_worker_lost=True, any BLAST task a dying child was holding was failed withWorkerLostError(or requeued and bounced between crashing children) — exactly the "most jobs fail, the rest are slow" symptom.
User-facing change¶
- BLAST submit/monitor tasks now run on a stable worker pool. Jobs stop failing
with
WorkerLostErrorand complete without the crash-loop latency.
API/IaC diff summary¶
api/app/telemetry.py: new_resolve_live_metrics_enabled(role)— Live Metrics defaults ON forapi(single uvicorn process, cheap + useful) and OFF forworker/beat(prefork, one QuickPulse stream per child is the boot cost). Overridable withAZURE_MONITOR_DISABLE_LIVE_METRICS/AZURE_MONITOR_ENABLE_LIVE_METRICS.api/celery_app.py: setworker_proc_alive_timeout(envCELERY_WORKER_PROC_ALIVE_TIMEOUT, default raised to 30 s) so a slightly slow child boot on 0.5 vCPU is not SIGKILL'd.infra/modules/containerAppControl.bicep: addAZURE_MONITOR_DISABLE_LIVE_METRICS=trueto theworkerandbeatcontainers so the fix persists acrossazd provisioneven before the new image ships.
Validation evidence¶
- Immediate live mitigation (no image rebuild — deployed image already honoured
the env var):
az containerapp update --container-name worker --set-env-vars AZURE_MONITOR_DISABLE_LIVE_METRICS=trueproduced revisionca-elb-dashboard--0000028(RunningAtMaxScale). - Post-fix worker console: SIGKILL / "Timed out waiting for UP message" count =
0;
ForkPoolWorkerindices reset to a stable 1–4; tasks (servicebus.drain_and_resubmit,publish_transitions,reconcile_stale_jobs) succeed. uv run ruff check api/app/telemetry.py api/celery_app.py api/tests/test_telemetry_init.py: passed.uv run pytest -q api/tests/test_telemetry_init.py: 12 passed (incl. new worker default-off + explicit-enable-override tests).uv run pytest -q api/tests/test_telemetry_init.py api/tests/test_blast_tasks.py: 157 passed.az bicep build --file infra/modules/containerAppControl.bicep: clean.