Date-tiered results layout for external (SB / OpenAPI) BLAST jobs¶
Motivation¶
The date-tiered storage layout (STORAGE_DATE_LAYOUT_ENABLED, issue #67) wrote
results under results/YYYY/MM/DD/<job_id>/ only for native dashboard jobs
(POST /api/blast/jobs stamps JobState.results_prefix). External jobs
(Service Bus queue + OpenAPI /v1/jobs) stayed flat at results/<job_id>/
because the sibling OpenAPI service owns their result write path: in Mode B
(inline FASTA, the dashboard's submit shape) main.py hardcoded
results_url = {blob_base}/results/{job_id} and ignored any caller-supplied
results field. So enabling the flag had no effect on the customer's actual
(SB-driven) workflow.
User-facing change¶
External SB / OpenAPI jobs now land under the same YYYY/MM/DD/ date
directory as native jobs when the layout is enabled. The dashboard forwards a
results_prefix of the shape YYYY/MM/DD/ on every external submit; the sibling
appends its own job id and writes results/<YYYY/MM/DD>/<openapi_job_id>/....
Reads are unaffected — the sibling lists/streams from its stored results URL,
so write and read follow the same path automatically. With the flag off
(default) nothing changes (flat layout).
API / IaC diff summary¶
Dashboard (elb-dashboard)
api/services/storage/job_prefix.py: newdated_results_subdir()returning theYYYY/MM/DD/date directory (no job id — the sibling appends its own).api/services/external_blast.py:submit_job(the single choke point every external submit surface flows through — SB drain, the XML/api/v1/elastic-blast/submitdirect path, and the canonical external submit) injectsresults_prefix = dated_results_subdir()whendate_layout_enabled()and the caller has not already set one. Best-effort: never fails a submit.
Sibling (elastic-blast-azure, separate repo — coordinated change)
docker-openapi/app/schemas.py:JobSubmitRequestgains an optionalresults_prefixfield.docker-openapi/app/main.py: new_validate_results_prefix()(accepts only an exactYYYY/MM/DD/shape — rejects../ absolute / extra segments). Mode Bresults_urlbecomes{blob_base}/results/{prefix}{job_id}; an empty prefix keeps the flat layout. The_discover_elb_job_id_from_submit_outputregex is made date-aware (itsjob-<32hex>fallback already covered it).
No managed-DB / Service Bus / SAS changes. Storage stays private.
Analytics read-path fix (post-ship hardening)¶
Enabling the date layout for external jobs surfaced a read-side regression:
the dashboard's result analytics (build_result_aggregate_payload →
list_parseable_result_blobs, the success-marker check, and
runtime_failure.read_blast_runtime_failure) resolve the results-container
prefix from resolve_results_prefix(openapi_job_id). For an external job that
was written under results/YYYY/MM/DD/<id>/, the resolver returned the flat
<id>/ fallback (no stored prefix on the row), so blob listing found nothing and
the hits table / aggregate rendered no_results even though download (sibling-
delegated) worked. Root cause: the write path stamped the date prefix on the
sibling, but the dashboard never recorded it on its own JobState row, and
the sibling's status/list snapshots do not echo the results URL — so the
dashboard had no authoritative source for the prefix at read time.
Fix (dashboard-only, the data already lands on the date path):
api/tasks/servicebus/tasks.py: the drain stamps theYYYY/MM/DD/prefix once (before submit) into the v1 payload —submit_jobhonours a caller-set prefix, so the same value reaches both the sibling and the durable row, with no recompute drifting across a midnight boundary._persist_drain_row_and_tracerecordsresults_prefix = "<prefix><openapi_job_id>/"on the external row.api/services/blast/external_jobs.py:_sync_external_jobs_to_tablepersists thatresults_prefixonto the newJobState(and backfills it onto an existing row that was first created without one — never overwriting a row that already carries a prefix).resolve_results_prefix(flag ON) then reads the stored prefix back via its Table lookup, so analytics/marker/runtime-failure reads list the date path.
Known limitation (accepted): flipping STORAGE_DATE_LAYOUT_ENABLED off after
dated jobs exist makes the resolver skip its Table lookup (a zero-I/O
optimisation for the all-flat case), so those jobs' analytics read the flat
fallback until the flag is re-enabled. The blob data is untouched; this only
affects the in-dashboard analytics view during a feature rollback.
Permanence (deploy wiring)¶
STORAGE_DATE_LAYOUT_ENABLED is wired through both deploy paths as a default-OFF
gate with a per-deployment override (mirrors SERVICEBUS_ENABLED):
infra/control-plane-env.json: added toapi/worker/beatwith the repo default"false"(charter §12a Rule 4).scripts/dev/quick-deploy.shemits it as--set-env-varson every api/worker/beat PATCH; a process/azd-env override oftruewins over the JSON default.infra/modules/containerAppControl.bicep: newstorageDateLayoutEnabledparam +effectiveStorageDateLayoutvar + aSTORAGE_DATE_LAYOUT_ENABLEDenv entry on all three python sidecars, so a fullazd provisionapplies the same value.infra/main.bicep+infra/main.parameters.json: the param flows from the azd env varSTORAGE_DATE_LAYOUT_ENABLED.
A deployment pins it on with azd env set STORAGE_DATE_LAYOUT_ENABLED true; the
repo default stays OFF so other environments are unaffected.
Validation evidence¶
- Sibling:
python -m pytest tests/test_results_prefix.py tests/test_external_payload_hardening.py -q→ 22 passed (new validator: valid date / empty-flat / traversal-rejection cases). - Dashboard:
uv run pytest -q api/tests/test_external_date_layout.py→ 5 passed (inject-when-enabled / no-inject-when-disabled / caller-prefix-wins / respects-explicit-empty-prefix / read-chain-resolves-stored-dated-prefix) +test_storage_job_prefix.pydated_results_subdircase. - Analytics fix:
uv run pytest -q api/tests/test_external_blast_api.py -k results_prefix→ 2 passed (persist-on-create / backfill-on-update, never-overwrite). - Regression sweep:
uv run pytest -q api/tests/test_external_blast_api.py api/tests/test_servicebus_tasks.py api/tests/test_external_date_layout.py api/tests/test_storage_job_prefix.py→ 213 passed;uv run ruff check apiclean. - Live end-to-end (SB outfmt-7 jobs land on the date path + download) recorded after the customer deploy; analytics aggregate re-verified on a fresh pin-less job (job
25f185162962,results/2026/06/23/25f185162962): aggregate200withfiles_parsed: 1,total_hits: 50(was0/no_resultsbefore the fix), result download200.