Serve external/Service Bus result downloads from Storage after cluster auto-stop¶
Date: 2026-06-21
Closes the proxy-only download gap tracked in #61.
Motivation¶
A Service Bus completion event's download_url (and the Results "download"
button) stopped working once the AKS cluster auto-stopped: the external-job file
download proxied through the elb-openapi pod, which is gone when the cluster is
stopped, so a consumer following the download_url after the idle window got an
error. The result bytes are durably in Storage — only the serving path was
coupled to cluster uptime, defeating the "complete and download later" model for
external/Service Bus consumers.
User-facing change¶
- A completed external/Service Bus job's result file now downloads even after
the cluster auto-stops: when the openapi proxy is unreachable the
apisidecar streams the result blob straight from the trusted workload Storage account via the managed identity (never a SAS — charter §9). - Jobs that completed before this change have no stored manifest, so their
offline download returns a clear
404 result_unavailable_offlineinstead of a confusing error; they still download normally while the cluster is up.
Code change summary¶
api/services/blast/external_job_projection.py—_external_result_filesnow carries the sibling'sblob_path(relative toresults/{job_id}/).api/services/state/job_state.py+api/services/state/repository.py—JobStategains a durableresult_manifestJSON column ([{file_id, blob_path}]);update()accepts it via a MERGE patch (no canonical recompute).api/tasks/servicebus/tasks.py—_persist_result_manifestcaptures thefile_id → blob_pathmanifest into the column at the succeeded transition (cluster up, the openapi detail withresult.files[].blob_pathin hand). Best-effort: a failure never blocks the completion event.api/services/external_blast.py—stream_result_file_from_storageresolvesfile_id → blob_pathfrom the manifest and streamsresults/{job_id}/{blob_path}from the job'sstorage_accountvia the sharedstream_blob_byteshelper (path-traversal validated, §9 concurrency gated).api/routes/elastic_blast.py— the download route falls back to the Storage helper only whenstream_fileraises theopenapi_unreachable503; every other error (incl. the fallback's own offline 404) propagates unchanged.
The blob path format was confirmed from the sibling
(elastic-blast-azure docker-openapi/app/main.py): results_url =
{account}/results/{job_id}, azcopy ls yields paths relative to it, and the
sibling's own download uses {results_url}/{blob_path}.
No IaC change. The new Table column is additive and schemaless.
Validation¶
uv run pytest -q api/tests— 4156 passed, 3 skipped. New tests:test_external_result_files_preserves_blob_pathtest_job_state_round_trips_result_manifest_columntest_persist_result_manifest_writes_column/_noop_without_blob_pathstest_stream_result_file_from_storage_*(manifest hit, no manifest, unknown file_id, no account)test_download_route_falls_back_to_storage_when_openapi_unreachable/_propagates_non_openapi_errors- Live (moonchoi, cluster stopped): the offline
404 result_unavailable_offlinepath is verified against pre-feature job9ca72c6092b0on revision 0000632 — the route catches the openapi-down 503 (bothopenapi_unreachableand theopenapi_not_configuredvariant a redeploy-while-stopped surfaces, which a first live run caught the fallback was missing) and the Storage fallback returns the honest offline 404 because that job predates the manifest. The success path (a real result byte-stream after auto-stop) activates for jobs that complete after this deploy and is covered by the unit tests above.