Parallel result-blob reads for BLAST export and aggregate¶
Motivation¶
The risk audit flagged three serial blob-read loops as a genuine bottleneck. Each
iterated result_blobs[:RESULTS_MAX_FILES] (up to 20 files) and issued one
Storage round-trip at a time before parsing:
blast_job_results_export(CSV/JSON export hits aggregation),_export_raw_result_text(raw XML/text export),build_result_aggregate_payload(aggregate analytics on cache-miss).
A 20-shard job therefore paid 20 sequential network reads even though the reads are independent. The blob-service clients are thread-local and pooled, so the reads are safe to run concurrently — they were simply serialised.
User-facing change¶
- BLAST result export (CSV/JSON, and raw XML/text) and the aggregate analytics fallback complete faster on multi-shard jobs. Output bytes, ordering, error codes, and degraded states are unchanged.
- No feature added or removed. This is a latency-only change.
API / IaC diff summary¶
api/services/blast/result_analytics.py- New helper
read_result_blob_texts_parallel(...): reads a list of result blobs concurrently on a boundedThreadPoolExecutor(_RESULT_READ_MAX_WORKERS = 8), preserving input order viaexecutor.map. Returns(blob_path, content, error)per blob so callers keep their existing per-blob failure accounting. Single-blob and empty lists short-circuit without spawning threads. api/services/blast/result_artifacts.pybuild_result_aggregate_payloadnow reads via the helper. Read and parse failures are still counted intoread_failuresexactly as before (the parse stays inside the per-blobtry).api/routes/blast/results.pyblast_job_results_exportand_export_raw_result_textnow read via the helper. The503 all_reads_failedguard, the409 format_not_captured/409 multiple_xml_reportsguards, the XML/text content filtering, and the ordered concatenation of the raw export are all preserved (order kept byexecutor.map). Removed two now-unused localfrom api.services.storage.data import read_result_blob_textimports._read_hitsinresult_artifacts.pyis intentionally left serial: itsmax_hitsearly-break is order-dependent, so parallelising it could change which hits land in a truncated result set. No behaviour change is acceptable there.
Validation evidence¶
uv run ruff check api/services/blast/result_analytics.py api/services/blast/result_artifacts.py api/routes/blast/results.py— all checks passed.uv run pytest -q api/tests/test_blast_results_routes.py api/tests/test_blast_result_manifest.py api/tests/test_blast_result_analytics_organism.py api/tests/test_blast_results_parser.py api/tests/test_blast_tasks.py— 196 passed.uv run pytest -q api/tests/test_job_artifacts.py api/tests/test_blast_ncbi_report.py api/tests/test_storage_data.py— 51 passed (these monkeypatchread_result_blob_texton the sharedstorage_datamodule, which the new helper still resolves at call time).uv run pytest -q api/tests— 2378 passed, 3 skipped (unchanged baseline).