Stale warmup / prepare_db jobstate rows reach a terminal status (#34)¶
Motivation¶
warmup and prepare_db_* jobstate rows could leak in an active status
(queued / running) forever when their owning work died without writing
a terminal status:
api.tasks.blast.reconcile_stale_jobsonly scansjob_type="blast".reconcile_orphaned_prepare_dbreconciles Storage{db}-metadata.json, never the jobstate Table rows.- Synchronous DB-admin ops (
prepare_db_cancel/prepare_db_delete) recorded their audit row asqueuedeven though the op had already finished inside the request — there was no later writer to terminalise them.
Live evidence (2026-06-13, elb-cluster-01): 1 stuck warmup (running ~10 h),
15 prepare_db_aks, 10 prepare_db_cancel, 2 prepare_db_delete, 2
prepare_db — all queued/running for days while their K8s Jobs had
completed hours earlier.
User-facing change¶
- Stale dbops/warmup rows no longer linger as phantom active work in the job
list / audit log; they are driven to
completedorfailed(worker_lost). - An in-flight AKS-fanout download (
prepare_db_aks) now correctly keeps an otherwise-idle AKS cluster alive (auto-stop prefix match), instead of being ignored because its type did not exactly equalprepare_db.
Implementation¶
- Source fix —
api.services.db.ops_audit.record_db_opgained optionalstatus/phaseparams (defaultqueued). The synchronousprepare_db_cancel/prepare_db_deleteroutes now recordstatus="completed"so their audit rows are born terminal. The appended history event mirrors the status (completedvsstarted). - New reconciler —
api.services.db.stale_dbops(classify_dbops_rowpure decision +reconcile_dbopsorchestrator) wrapped by the Celery taskapi.tasks.storage.reconcile_stale_dbops_jobs, beat-scheduled every 300 s on thereconcilequeue. Per-type policy: - synchronous ops (
prepare_db_cancel/prepare_db_delete) →completedregardless of age (mops up pre-existing rows); - async ops (
warmup/prepare_db/prepare_db_aks/shard/oracle) →completedon CelerySUCCESS,failed/task_failedonFAILURE/REVOKED, andfailed/worker_lostonly after a generous per-type quiet threshold (STALE_DBOPS_WARMUP_SECONDS=7200,STALE_DBOPS_PREPARE_DB_SECONDS=21600) with no live Celery record. The prepare-db threshold (6 h) comfortably exceeds theprepare_db_akstask hard time limit (~4 h 45 m) so a live download is never aged out. - Kill-switch
STALE_DBOPS_RECONCILE_ENABLED(default on). - Accepted simplification: a Celery
SUCCESSalways maps the audit row tocompleted; a partial download's true state lives in{db}-metadata.json/ the Storage card, not this coarse audit row. - Auto-stop prefix match —
api.services.auto_stop_evaluatoradds_row_type_blocks_autostop, which matchesACTIVE_JOB_TYPESexactly plus theprepare_dbprefix family. The_ACTIVE_ROW_STALE_SECONDSzombie cap still backstops a crashedprepare_db_aksrow at 2 h.
API / IaC diff summary¶
- No API surface change. New env knobs (all optional, safe defaults):
STALE_DBOPS_RECONCILE_ENABLED,STALE_DBOPS_WARMUP_SECONDS,STALE_DBOPS_PREPARE_DB_SECONDS,CELERY_BEAT_STALE_DBOPS_SECONDS. - New beat entry
stale-dbops-reconcileinapi/celery_app.py.
Validation evidence¶
uv run pytest -q api/tests/test_stale_dbops_reconcile.py— 15 passed (pure-decision matrix, IO glue, orchestrator tally, kill-switch).uv run pytest -q api/tests/test_db_ops_audit_status.py— sync/async status contract.uv run pytest -q api/tests/test_auto_stop_evaluator.py— prefix-match guards- existing zombie age-out unchanged.
- Consumer sweep (146 tests across
test_prepare_db_routes,test_prepare_db_aks_route,test_peering_nsg_audit,test_storage_shared_taxonomy,test_settings_vnet_peering,test_auto_warmup,test_warmup_route,test_azure_tasks,test_tasks_facade_contract) — green. uv run ruff checkclean on every touched path.- Note: the full
pytest -q api/testsrun trips a pre-existing, unrelated cross-test-pollution hang inblast_job_get→external_blast.get_job(a real httpx call against a leaked external OpenAPI base URL that waits out its timeout). That path is not touched by this change;test_blast_jobs_routes.pypasses in isolation (15/15).