DB readiness honors copy_status.phase end-to-end¶
Motivation¶
Production scenario (2026-05-27): the operator triggered prepare-db core_nt
on the deployed dashboard. prepare-db writes the core_nt-metadata.json
blob the moment it starts so it can update progress, which means
/api/blast/databases immediately surfaces a core_nt entry whose
copy_status.phase is copying. Multiple surfaces treated "the DB shows up
in the listing" as "the DB is ready":
- Warmup card showed
Storage DB readyand an enabled Warm button. - New Search listed
core_ntas a selectable database in Standard databases, counted it in the category tab badge, and Submit was reachable. - Backend warmup task (
api.tasks.storage.warmup_database) only gated onfile_count > 0, so a click against an in-flight DB would queue auto-shard - node warmup against incomplete volumes (cryptic per-pod failures minutes later).
The hardening that introduced copy_status.phase (see
2026-05-22-blast-db-download-hardening.md) already exposed the authoritative
signal but only BlastDbRow / useBlastDb consumed it. Other call sites kept
their pre-hardening heuristic and drifted.
User-facing change¶
- Warmup card
- The Storage pill now reads
Downloading · 30/800 files,Partial copy · …,Copy init failed,Updating to BLAST_DB-2026-05-20, etc. whencopy_status.phaseis notcompleted. Tone is loading/blocked/ok accordingly (newStatusPillloading/blockedtones). - The detail line surfaces the same readiness reason.
- Warm is disabled while the DB is mid-copy / mid-update;
canWarmrequires the strict readiness verdict. - In-flight DBs stay visible in the panel so users see progress instead of the row disappearing.
- New Search → Choose Search Set
- Category tabs count only ready DBs; tab title shows
N ready · M preparingwhen a copy is in-flight. - The DB row stays visible for in-flight copies but is dimmed, the radio is
disabled, and the status column shows the readiness label (Downloading 30/800,Partial copy 750/800,Updating to …). - Auto-select on category change skips in-flight DBs (the previous behaviour silently picked the first DB and left Submit blocked with no message).
- Suggested chips (
!form.db) only suggest ready DBs. - A new warning banner appears when the selected DB exists but is not ready yet (mirrors the existing "not in storage" banner).
- Submit
deriveSubmitValidationexposesdbNotReady/dbNotReadyReason; Submit is disabled with the human reason in themissing[]list and the Database step indicator turns off.
API / IaC diff summary¶
api/services/blast/task_config.py- New
validate_blast_database_ready(*, storage_account, database): wrapsvalidate_blast_database_availablethen readsblast-db/{db}-metadata.json(single capped blob GET, 1 MiB max). RaisesBlastDatabaseAvailabilityError(code="database_not_ready")whencopy_status.phase != "completed"andcode="database_updating"whenupdate_in_progressis set. Falls back to availability semantics for legacy DBs (no metadata.json) and for transient Storage errors. - New per-process 5s readiness cache +
reset_blast_database_readiness_cache()for tests. Same TTL as the existing submit-gates cache so we never serve a stale verdict longer than 5s after prepare-db completes. api/services/blast/submit_gates.py_gate_blast_databasenow callsvalidate_blast_database_ready. New error codes (database_not_ready,database_updating) carry friendly action hints (Wait for download,Wait for update).api/routes/blast/preflight.py- The
databasecheck row now uses the ready-aware validator. The check title differentiatesBLAST Database Preparing/BLAST Database Updatingfrom generic missing/found, and addserror_codefor SPA-side mapping. api/tasks/storage/warmup.pywarmup_databaserejects withfailed+ structured message when the selected DB'scopy_status.phase != "completed"orupdate_in_progress. Defense in depth against scripted callers, beat scheduling, or stale UI that bypass the SPA gates.api/services/storage/database_list.py/api/blast/databasesresponse gains derivedready: bool+not_ready_reason: str | nullfields on each row. Mirrors the SPA'sgetBlastDbReadinessso any consumer (BLAST orchestrator, OpenAPI, audit tools) can read one boolean instead of re-deriving the contract.
Frontend diff summary¶
- New
web/src/utils/blastDbReady.ts+.test.ts— single source of truth (getBlastDbReadiness,isBlastDbReady,blastDbReadinessLabel,blastDbReadinessTone,blastDbBlockedReason). 10 unit tests covering all fivecopy_statusphases, update_in_progress, legacy file_count fallback, unknown phase forward-compat, undefined/null inputs. web/src/components/cards/storage/useBlastDb.ts— localisDbReadynow delegates to the shared util (kills duplication / drift risk).web/src/components/warmupSection/helpers.ts—buildWarmupRowsuses shared util; newstorageReady/storageTonerow fields; in-flight DBs stay visible;canWarmrequires strict readiness.web/src/components/WarmupSection.tsx—StatusPillaccepts newloading/blockedtones; storage pill driven byrow.storageToneinstead of hard-coded label match.web/src/pages/blastSubmit/DatabaseSection.tsx— readiness-aware category counts, disabled-row rendering for in-flight DBs, readiness-aware suggested chips;firstDatabasePathForCategoryskips not-ready DBs.web/src/pages/blastSubmit/submitValidation.ts+types.ts+BlastSubmit.tsx— newdbNotReady/dbNotReadyReasonplumbed through.web/src/pages/blastSubmit/DatabaseSection.test.ts— fixture update + three new cases (skips in-flight, only-in-flight returns empty, skips update_in_progress).web/src/components/warmupSection/helpers.test.ts— five new cases (copying, partial, updating, legacy ready, completed ready).web/src/pages/blastSubmit/submitValidation.test.ts— fixture update + one new case (in-flight DB blocks submit).
Performance notes¶
validate_blast_database_readyadds one metadata-blob GET per submit attempt, sized at 1 MiB max (metadata.json blobs are <16 KiB in practice). The result is cached per-process for 5s, so a UI retry burst from a single click sends at most one extra GET per (account, db) tuple.- Frontend predicate is pure / O(1) per DB.
buildWarmupRowsand category counters keep their existing O(n) shape (n ≈ 10 DBs). - No extra network calls in the SPA — readiness is derived from the
existing
/api/blast/databasespayload.
Validation evidence¶
# Backend
$ uv run pytest -q api/tests/test_blast_database_readiness.py \
api/tests/test_warmup_database_readiness.py \
api/tests/test_blast_database_availability.py \
api/tests/test_blast_submit_gates.py \
api/tests/test_response_contracts.py \
api/tests/test_storage_data.py \
api/tests/test_auto_warmup.py
79 passed in 6.93s
$ uv run pytest -q api/tests
1617 passed; 1 unrelated flake (test_terminal_exec.py timeout on `az --version`)
that passes when re-run in isolation.
$ uv run ruff check api/services/blast/task_config.py \
api/services/blast/submit_gates.py \
api/services/blast_task_config.py \
api/routes/blast/preflight.py \
api/tasks/storage/warmup.py \
api/services/storage/database_list.py \
api/tests/test_blast_database_readiness.py \
api/tests/test_warmup_database_readiness.py
All checks passed!
# Frontend
$ cd web && npm test -- --run
Test Files 49 passed (49)
Tests 357 passed (357)
$ npm run build
✓ built in 7.99s
$ npx eslint <touched files>
clean
Deployment notes¶
- Both
apiandfrontendsidecar images need a rebuild because the preflight gate, submit-gate path, and warmup task all changed. Thequick-deploy.sh frontend apitwo-target rebuild is sufficient — no Bicep / Container App template change. - Backwards compatible: legacy DBs prepared before the hardening (no
metadata.json or no
copy_statuskey) still validate via the existing availability +file_count > 0heuristic.