prepare-db AKS: cancel→re-Get Job-name race fix + pod progress logging¶
Motivation¶
A user cancelled an in-flight nt prepare-db run and clicked Get again, then
reported "the download doesn't seem to work — there are no logs in the pod either".
Investigation produced two findings:
-
The download was, in fact, healthy. Live inspection of the resubmitted Indexed Job
prepare-db-nt-260526010501showed all 10 shard podsRunning, pod 0 actively streamingnt_viruses.10.nsqfrom NCBI throughazcopy copy ... --from-to=PipeBlob, eth0 RX advancing ~16 MB/s, and 3728ntblobs already staged. The perceived stall was opaque logging: the per-file copy runs with--log-level=ERROR >/dev/null, so a healthy multi-GB shard emits zero log lines for minutes between "azcopy installed" and the final summary. Silence looked like failure. -
A real latent race existed in the cancel→resubmit path. prepare-db Jobs are deterministically named per
(db, source_version). If a cancel (propagationPolicy: Background) has not finished collecting the old Job when the resubmit fires,_create_job_if_absentpreviously saw the still-terminating Job viaGETand reported it as a healthyexistingduplicate — so the Celery task polled a dying Job and never spawned fresh pods. This did not fire in the reported incident (the resubmit created the Job cleanly), but it is a genuine defect worth closing.
User-facing change¶
- Pod logs now show progress.
PREPARE_DB_AKS_SCRIPTlogs a throttled heartbeat: - one
[<idx>/<total>] copy <file>line immediately before each file'scurl | azcopycopy (turns opaque silence into a visibly-advancing counter — operators can now tell slow-but-healthy from stalled); - one
[<idx>/<total>] scanned; <skip> already staged, <ok> copiedline every 50 skipped files (so a long resume that re-checks thousands of already-staged blobs still looks alive while it scans). - Dashboard no longer looks frozen during large-volume downloads. The
BLAST Databases row derives its
N / total filescounter and MB/s speed from the committed blob listing, but azcopy--from-to=PipeBlobcommits each blob only when its whole file finishes. While every shard pod streams a multi-GB.nsqsequence volume (which can take minutes each), the committed count and the bytes-derived speed stay flat even though the network is at full throughput — reading as a stall.BlastDbRownow detects a committed count that has not advanced for >20 s during an active copy and shows a reassuring· transferring large volumes…note (with an explanatory tooltip) in place of the misleading file-count-rate ETA, which over-promised because the remaining files are the largest. - Cancel→re-Get no longer collides with a terminating Job.
_create_job_if_absentnow distinguishes a healthy duplicate from a Job carryingmetadata.deletionTimestamp: it waits (default 60 s, polling every 2 s) for the terminating Job to disappear, then creates the new one. If the old Job never clears within the deadline it returns an honest{"status": "error", "terminating": True}which the task surfaces as a partial "retry shortly" state (clearingupdate_in_progressso the user can re-Get) instead of silently polling a dead Job.
API / IaC diff summary¶
api/services/k8s/prepare_db_jobs.pyimport time; new constantsDEFAULT_TERMINATING_WAIT_SECONDS = 60.0,DEFAULT_TERMINATING_POLL_SECONDS = 2.0._create_job_if_absent(..., terminating_wait_seconds, poll_interval_seconds)rewritten to detectmetadata.deletionTimestamp, wait for 404, and handle create-time 409 by re-evaluating. Healthy-duplicate contract (status: existing) unchanged.PREPARE_DB_AKS_SCRIPT: added the per-file copy heartbeat and the throttled skip heartbeat described above. No change to the copy command itself (curl | azcopy copy ... --from-to=PipeBlob).web/src/components/cards/storage/BlastDbRow.tsx- Added a committed-count stall detector (
lastAdvanceMsRef/lastProgressRef+transferringLargeVolumes) and rendered thetransferring large volumes…hint, suppressing the file-count ETA while that hint is shown. Pure presentation; no change to the copy/transfer path or to any API contract. - No IaC change. The new script ships in the per-job ConfigMap built at submit time and takes effect on the next prepare-db job after the api/worker images are next deployed — no redeploy was performed for this change.
Validation evidence¶
uv run ruff check api/services/k8s/prepare_db_jobs.py api/tests/test_prepare_db_aks_job_create.py→ All checks passed.uv run pytest -q api/tests -k "prepare_db or k8s"→ 204 passed.- New focused suite
api/tests/test_prepare_db_aks_job_create.py(6 cases): absent→created; healthy 200→existing; terminating→404→created; terminating never clears→error+terminating; create 409→re-eval→existing; GET 500→error. - Live cluster evidence (moonchoi sub,
elb-cluster-02): all 10 shard pods ofprepare-db-nt-260526010501Running, eachcurl-streaming a distinct multi-GB.nsqvolume (nt.066,nt.058,nt_euk.000, …) intoazcopy copy PipeBlob; pod 0 eth0 RX +78,605,516 bytes over 5 s (~14 MB/s, ~140 MB/s aggregate); committedntblob count observed rising 3728 → 3730 — confirming the download was never stuck and that the committed count intentionally lags behind in-flight PipeBlob uploads (the exact reason thetransferring large volumes…hint was added). cd web && npm run build→ built clean;npx vitest run src/components/cards/storage→ 37 passed.