Self-learning per-job ETA for the elb-openapi status API¶
Motivation¶
Users submit ~20 separate OpenAPI BLAST requests at once and poll each job's status as XML. They asked for (a) remaining time for a running job, (b) queue position / jobs-ahead for a queued job, and © a precise per-job estimate that self-learns from each completed run rather than using a fixed constant.
The result-equivalence guarantee is unchanged: this feature is observability
only. It never touches num_nodes, searchsp, sharding, or the BLAST command,
so results still equal NCBI web BLAST.
User-facing change¶
The /status payload gains an optional eta object, present only when
ELB_OPENAPI_ETA_ENABLED is truthy:
- Running job:
{ remaining_seconds, estimated_finish_seconds, estimated_finish_at, confidence, basis } - Queued job:
{ jobs_ahead, estimated_start_seconds, estimated_finish_seconds, estimated_start_at, estimated_finish_at, confidence, basis }
confidence is high (≥10 samples for the key), medium (≥_MIN_SAMPLES), or
low (fallback estimate). basis names the partition key / fallback used.
The estimate is keyed on input features — database, query size bucket
(log-bucketed sequence count + residue count), and cold/warm cluster state — so
each (db, bucket, cold/warm) combination learns its own EWMA mean/variance from
observed run times. The queued estimate runs a C-server (MAX_ACTIVE_SUBMISSIONS)
min-heap simulation over the priority-sorted queue.
When the feature is OFF the payload is unchanged except that two harmless
feature fields (query_seqs, query_bases, both 0) are captured at submit.
API / IaC diff summary¶
- New
scripts/dev/openapi-overlays/eta.py— import-safe, opt-in overlay copied into the siblingdocker-openapibuild context asapp/eta.py. Pure Python core; Azure Table backing is optional and lazy. - New
scripts/dev/openapi-overlays/test_eta.py— 16 unit tests (feature parsing, learning convergence, cold/warm separation, fallback chain, C=2 queue stagger, cross-replica ETag-merge convergence). - Modified
scripts/dev/patch-openapi-build-context.py— copies the overlay and injects_eta.enabled()-gated hooks: import, submit feature capture, a completion sample record at the single state-write choke point_update_job(with an atomiceta_recordedclaim under_jobs_lock), and the statusetaprojection on both status surfaces —_external_job_payload(/api/v1/elastic-blast/jobs/{id}) and the canonicalget_job_status(/v1/jobs/{id}/status). - No infra/Bicep change. No change to the shared production image (
4.18); a separateelb-openapi:eta-testtag is used for validation.
Hardening applied after design critique¶
- Atomic completion record — the
eta_recordedflag is claimed inside_jobs_lockbefore recording, so concurrent status polls of the same completed job record the sample exactly once. - Cross-replica convergence —
_Store.updateuses an ETag optimistic-merge (re-read row → re-apply EWMA →update_entity/create_entitywith boundedELB_OPENAPI_ETA_UPDATE_RETRIESretries) instead of last-writer-wins upsert, and_Store.getre-reads the Table when the cached row is older thanELB_OPENAPI_ETA_CACHE_TTL_SECONDS, so two openapi replicas accumulate samples instead of clobbering each other. - Observability —
_Store._tablelogs once (warn) when ETA is enabled but no Table backing is configured or client init fails, and once (info) on successful init, so an "ETA never gets more confident" state is diagnosable.
Endpoint-coverage fixes (found in live validation)¶
Live validation surfaced that the openapi service exposes two status surfaces and the original hooks only covered one:
/v1/jobs/{id}/status(get_job_status) builds its own inline dict and does not route through_external_job_payload(which serves only the external facade/api/v1/elastic-blast/jobs/{id}). The first burst returnedeta=nullon every job because the harness/status_urlpolls the canonical endpoint. Fix: projectetaon theget_job_statuspayload too.- The completion sample recorder had the same single-surface problem, so
basis.samplesstayed0(no learning) when jobs were observed via the canonical endpoint. Fix: move recording into the endpoint-independent choke point_update_job, which also fires from the background watchdog with zero polling, keeping exactly-once semantics via the persistedeta_recordedflag.
Known limitations (documented, not blocking)¶
- Cold detection (
_job_was_cold) is an O(n) scan, makingrecord_sampleeffectively O(n²) over the job set — acceptable at the 20-burst scale; jobs are not garbage-collected (pre-existing). - Queued jobs have
started_at=None, so the very first cold job's ETA cannot account for cluster spin-up until at least one cold sample is recorded.
Tunables (all default-safe, opt-in)¶
ELB_OPENAPI_ETA_ENABLED, ELB_OPENAPI_ETA_BIAS_Z, ELB_OPENAPI_ETA_MIN_SAMPLES,
ELB_OPENAPI_ETA_EWMA_WINDOW, ELB_OPENAPI_ETA_COLD_GAP_SECONDS,
ELB_OPENAPI_ETA_DEFAULT_RUN_SECONDS, ELB_OPENAPI_ETA_TABLE,
ELB_OPENAPI_ETA_TABLE_CONN, ELB_OPENAPI_ETA_CACHE_TTL_SECONDS,
ELB_OPENAPI_ETA_UPDATE_RETRIES.
Validation evidence¶
uv run ruff check scripts/dev/openapi-overlays/ scripts/dev/patch-openapi-build-context.py→ clean.uv run python -m pytest scripts/dev/openapi-overlays/test_eta.py -q→ 16 passed.- Fresh-copy patch apply (
cp -a ~/dev/elastic-blast-azure/docker-openapi …→patch-openapi-build-context.py) →py_compileofapp/main.py+app/eta.pyOK, all four hook sites grep-confirmed. - Test image
acrelbdashboard3abp67bppe.azurecr.io/elb-openapi:eta-testrebuilt with the hardened overlay (ACR runde46). - Live validation on
elb-cluster-02(singleeta-testreplica, ETA enabled, shared image left at4.18and restored afterwards): - 5-job burst →
/v1/jobs/{id}/statusreturnedetafor every non-terminal job: running jobs carriedremaining_seconds(38.5 s, 41.0 s); queued jobs carriedjobs_ahead⅔/4 with monotoneestimated_start_seconds28.8 → 35.5 → 138.5 s, confirming the C=2 queue simulation staggers starts correctly. - Two consecutive 3-job bursts → after the first burst's three completions,
the second burst's jobs reported
basis.samples=3,confidence=medium(up fromlow), and learnedremaining_seconds(82.0 s, 71.1 s) instead of the 110 s default — proving online EWMA learning fires end-to-end. - The
eta-testbase image lacksazure-data-tables/azure-identity, so the Table store degrades to the in-memory fallback (warn-once logged as designed); learning was validated against that fallback. Cross-replica Table persistence is covered by the unit tests. - NCBI parity unchanged: ETA never touches the submit/
searchsp/sharding path; 11/11 jobs across the validation bursts that reached submit ran the standard pipeline.