Repository navigation
Conversation
A launcher (orchestrator) execution is itself a workflow run, and every child workflow it submits now points back at it. This is what lets launcher progress be derived from persisted state rather than from a status endpoint on the launcher container. The index is composite (parent_run_id, created_at) for the same reason ix_workflow_runs_project_created_at is: child listings are "one parent, newest first", and a launcher fans out to hundreds of children. It also satisfies InnoDB's requirement that a foreign key column be indexed. ON DELETE SET NULL, not CASCADE: deleting a launcher run must not delete the child workflows it submitted, which are independent executions whose outputs outlive the orchestration that started them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…allback - parent_run_id on RunSummary and RunLog, so a caller can see which launcher submitted a run without a second request. - RunProgress: a run's own state plus a rollup of the runs it submitted. The two are separate fields on purpose -- a launcher whose Batch job died while its children run must be able to report SYSTEM_ERROR with 40 children still RUNNING, which is the orphan condition we want visible. - ExecutorStateChangeCallback: the executor-agnostic successor to the HealthOmics-specific callback payload, carrying the executor name so the service can pick the right status vocabulary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dation as 400
Children arrive through the ordinary GA4GH POST /runs carrying a reserved
ParentRunId tag, which create_run promotes into the indexed parent_run_id
column -- the same tag-promotion pattern already used for ProjectId and
TaskName. Listing by parent then needs no server work at all, because the
generic ListRuns filter resolves any WorkflowRun attribute by name. That
listing is also how a restarted launcher rediscovers work it already
submitted.
An unknown parent is a client error, so map create_run's ValueError to 400
rather than letting it surface as a 500. Every validation failure in create_run
-- missing ProjectId, unsupported workflow_type, and now an unknown parent --
was being reported as a server fault; the route already declared a 400.
GET /runs/{run_id}/progress is one GROUP BY over the direct children, modelled
on get_system_state_counts. Direct children only: a grandchild is reached by
asking for its own parent's progress.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Omics Every POST /runs pinged LambdaWorkflowSubmissionService, so a launcher run created by NGS360 APIServer would have been pushed at HealthOmics. Runs whose engine is awsbatch now go to ExternalDispatchSubmissionService, which makes no AWS call, notes "awaiting external dispatch" in system_logs, and leaves the run QUEUED for whoever submits the Batch job. Any other engine, including None, still goes to the Lambda service exactly as before. The factory is also the seam for the deferred decision about who submits: if this service should submit Batch jobs itself, add a BatchWorkflowSubmissionService and register it here, and nothing else in the design moves. Also parameterise the hardcoded "AWSHealthOmics (us-east)" deployment lookup by the requested engine, so the same NGS360 registry lookup can serve a Batch deployment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dentials Generalised rather than replaced: handle_omics_state_change is now a thin adapter over handle_state_change with the HealthOmics status map, so the existing route, schema, and tests keep working untouched. AWS Batch gets its own map (SUBMITTED/PENDING/RUNNABLE -> QUEUED, STARTING -> INITIALIZING, SUCCEEDED -> COMPLETE, FAILED -> EXECUTOR_ERROR, SUBMIT_FAILED -> SYSTEM_ERROR); the existing transition table already permits that progression, so it is verified by test rather than edited. POST /internal/callbacks/executor-state-change accepts either X-Internal-API-Key (the relay Lambda, fed by EventBridge) or X-Internal-Service-Key (APIServer, which is what knows the jobId it just got back from SubmitJob, and which reports pre-start failures). Two callers, one endpoint, two independently rotatable keys. A reported log stream becomes a CloudWatch console deep link in stdout, with the raw stream name kept in outputs so APIServer's log viewer can still use it. batch_log_group and aws_console_region configure that link; no Batch credentials are needed, because this service does not submit Batch jobs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Client: parent_run_id on list_runs and iter_runs, plus two new operations -- get_run_progress, and report_executor_state so the Python submitter (APIServer) has a typed way to bind a job id and report state instead of hand-rolling the POST. CLI: `wes runs list --parent`, `wes runs progress <id>` for a launcher's own state plus a rollup by child state, and `wes runs tree <id>` for one line per child, colouring terminal-but-not-COMPLETE children red because those are the samples that will not finish on their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Service and API: ParentRunId promotion, an unknown parent rejected as 400, the parent filter selecting only that launcher's children, and a progress rollup that counts direct children while deliberately excluding a grandchild. Callbacks: the full Batch progression through to SUCCEEDED, a duplicate event_id as a no-op, FAILED recording the container exit code, a post-terminal event ignored, an unknown executor rejected, and HealthOmics still working through the generic route after the refactor. Route-level tests confirm either internal key is accepted and a wrong one is 403 -- these patch get_settings where callback_auth looks it up, because that module reads settings directly rather than through FastAPI's DI, so the app fixture's override does not reach it. Integration: a launcher plus three children end to end, with the submission factory left real so the launcher genuinely takes the external-dispatch path and only the children's HealthOmics call stubbed. A failed child leaves the launcher COMPLETE, which is the separation the design asks for. Contract: the two new client operations are added to OPERATIONS, so every server route again has a client operation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ARCHITECTURE.md gains a Launcher Orchestration section: why a launcher execution is an ordinary WorkflowRun rather than a new table, how ParentRunId lineage works, why parent state and child progress are kept separate, why APIServer submits the Batch job while this service owns the record, the per-executor status maps, the launcher container env contract, and what is explicitly not done yet (cancel cascade, relaunch). The README's numbered flow described the launcher as the submitter of workflows to this API with no mention of the launcher's own record; it now covers the launcher being started as a Batch job and its children being tagged back to it. scripts/launcher_example.sh walks the whole flow against a local service: submit a launcher, report its Batch job RUNNING, fan out two children, then read progress and the tree back. It sends RUNNING before a terminal status because QUEUED -> COMPLETE is not a legal transition, and skips the callback steps when no key is set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The engine now selects the submission backend, so it has to be a name the service actually supports. It was neither validated nor advertised: service-info listed only cwltool, which this service cannot run, while get_submission_service sent anything it did not recognise to the HealthOmics Lambda. A launcher submitted as "aws-batch" was accepted, dispatched to HealthOmics, and failed there on a deployment it never had, reporting a HealthOmics error for a typo. service-info now advertises awsbatch and awshealthomics, and create_run validates against that same map -- the spec makes it the client's only way to discover legal values, so the check and the advertisement cannot disagree. The submission factory dispatches through a SUBMISSION_STRATEGIES registry keyed by those names and raises on an unregistered engine instead of defaulting. Omitting the engine still means this instance's default backend, so callers that predate launchers are unaffected. A submission naming an engine that is neither advertised nor omitted is now a 400 listing the accepted names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rejection of unadvertised engines at both the service and the route, acceptance of an advertised name in any case or padding, and the spec's rule that an engine is required when an engine version is given. Two tests exist to stop the advertisement and the dispatch registry from drifting apart, which is what made the original bug invisible: test_registry_matches_advertised_engines compares the two sets directly, and a route test submits every engine service-info advertises and expects 200 -- the spec's discovery contract as an assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RunSummary and RunLog mirrored the promoted parent_run_id column as a
top-level response field. That duplicated information already present,
unmodified, in the tags field: create_run never strips ParentRunId out
of tags_dict before storing it, so tags["ParentRunId"] carries the same
value on every response. The spec's RunSummary requires only run_id,
state, start_time, end_time, and tags, so the extra field was this
service's own addition beyond the minimum GA4GH shape, unlike the
parent_run_id column itself -- an internal MySQL indexing detail the
spec has no opinion on either way, kept as-is for the same reason
ProjectId/TaskName are promoted into project/task_name.
Filtering (?filters={"parent_run_id":...}) and the progress rollup are
unaffected: both query the internal column directly and never touched
the response schema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… key The launcher env contract handed launchers the internal service key plus an asserted X-On-Behalf-Of user. That key covers only this service: the launcher also calls the NGS360 API directly (project lookup, results upload), which does not accept it. And anything holding it may assert any user, so a launcher container would carry this service's full trust rather than its submitter's. Launchers now get NGS360_AUTH_TOKEN, an ngs360_ API key minted for the submitting user for one job, plus NGS360_AUTH_KEY_ID so the launcher can retire it on exit. One user credential then covers every call, each authorized against that user. The service key keeps its role for trusted sibling services. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed b24a0d0, a docs-only change: the launcher env contract in Why the service key is the wrong credential for launchers:
The service key itself is unchanged and keeps its role for trusted sibling services. Counterparts: NGS360/APIServer#484 mints the key; NGS360/PAML#132 retires it. Still outstanding on this branch: it predates |
|
Superseded by #233: the same launcher-orchestration work ported directly onto |
Why
Bioinformatics launchers (e.g. the RNA-Seq launcher) orchestrate work: read a samplesheet, submit one child workflow per sample, wait, then gather. Historically they ran wrapped in CWL so Seven Bridges or Arvados could execute them as a platform task. In AWS we decided not to add a CWL runner layer — the launcher image will run directly as an AWS Batch job, with CloudWatch as the log interface — and not to give the launcher a web server or status endpoint.
That removes the platform "task" record the launcher used to get for free. Nothing recorded that a launcher execution existed, and nothing linked it to the child runs it created. This PR adds that record, so launcher status and progress are derived from workflow rows this service already keeps. It is also the prerequisite for launcher restart/recovery.
Everything here is additive: no existing endpoint, schema, or callback changes shape.
What changed
A launcher execution is a
WorkflowRun, not a new table —workflow_engine=awsbatch,workflow_run_id= the BatchjobId. It reuses the state machine, listing, filtering, callbacks, the client, the CLI, and the frontend's run views.Lineage rides in as a reserved
ParentRunIdtag, promoted bycreate_runinto an indexedparent_run_idcolumn — the pattern already used forProjectIdandTaskName.POST /runsstays GA4GH-shaped, and?filters={"parent_run_id":"…"}works through the existing generic filter with no server change. That listing is also how a restarted launcher rediscovers work it already submitted.Parent state and child progress are deliberately separate.
GET /runs/{run_id}/progressreturns the launcher's own state plus aGROUP BYrollup of its direct children. They are not merged because the interesting failure is when they disagree: if the Batch job dies while 40 children run, the parent must be able to saySYSTEM_ERRORwith those children stillRUNNING— the orphan condition we want visible. Equally, a failed child does not fail the launcher.Submission now dispatches by engine. Every
POST /runsused to ping the HealthOmics Lambda, so a launcher run would have been pushed at HealthOmics.awsbatchruns go toExternalDispatchSubmissionService: no AWS call, onesystem_logsline, leftQUEUEDfor its submitter. Every other engine behaves exactly as before.The Omics callback is now executor-agnostic.
handle_omics_state_changebecame a thin adapter overhandle_state_changewith a per-executor status map, so the existing route, schema, and tests are untouched.POST /internal/callbacks/executor-state-changeaccepts eitherX-Internal-API-Key(the EventBridge relay Lambda) orX-Internal-Service-Key(APIServer, which is what knows thejobIdfromSubmitJoband reports pre-start failures) — two callers, one endpoint, two independently rotatable keys. A reported log stream becomes a CloudWatch console deep link, with the raw stream name kept inoutputsfor APIServer's log viewer.Client and CLI:
parent_run_idonlist_runs/iter_runs, newget_run_progressandreport_executor_stateoperations, pluswes runs list --parent,wes runs progress <id>, andwes runs tree <id>.Who submits the Batch job
NGS360 APIServer submits; this service owns the record. APIServer already has
submit_batch_job, aBatchJobtable, an EventBridge-driven status/log-stream path, a paginated CloudWatch log viewer, and the IAM — and a launcher job submitted by WES would be missing from the jobs UI operators already use. If we later want WES to submit, the seam isget_submission_service: add aBatchWorkflowSubmissionServiceand register it. Nothing else in the design moves.Review notes
upgrade headfrom empty,alembic checkreports "No new upgrade operations detected." — i.e. it matches the models exactly — thendowngrade -1and re-upgrade to prove reversibility.ON DELETE SET NULL, notCASCADE: deleting a launcher must not delete the child workflows, whose outputs outlive the orchestration.(parent_run_id, created_at)— child listings are "one parent, newest first", and it satisfies InnoDB's FK indexing requirement, so no separate index.create_run's other validation failures (missingProjectId, unsupportedworkflow_type) were surfacing as 500s; the route already declared a 400.make lintclean,make test360 passed.refactor/wes-workspace-packages, notmain.Not in this PR
../PAML(ngs360_platform.py), without which the launcher cannot participate. Written and unit-tested on afeature/launcher-orchestrationbranch there, to be released and pinned separately:get_current_task()returning a task built fromWES_RUN_ID;ParentRunIdinsubmit_task's tags; aWES_SERVICE_KEYfallback credential for headless runs (a Batch job has no user session to source a token from), with the bearer token still winning when both are set; and redacting credential-shaped values from the environment dumpPlatformFactory.detect_platformwrites to the log when detection fails.get_tasks_by_namedoes not paginate, so thetask_name=Nonecall — "all runs in this project" — is capped at the 10 newest runs, as is any sample retried more than 10 times; it fails by returning fewer rows than exist, with no way for a caller to tell truncation from a short list (NGS360Platform.get_tasks_by_name silently returns at most 10 runs (no pagination) PAML#130). And the launcher passesinputs_to_comparepositionally intoget_tasks_by_name'sworkflowparameter, so PAML receivesNoneand skips the parameter comparison entirely, while the misplaced dict becomes aworkflow_urlfilter that this service's filter handling silently drops — reuse therefore matches any same-named run regardless of parameters. Filed against the launcher repo; a one-word keyword-argument fix.../APIServer: the submit path — resolve the AWSBatch deployment, create the parent run, build argv fromworkflow_params, submit asjobName=wes-<run_id>, report the binding to the callback.WES_SERVICE_KEYsecret.CancelRunfor a launcher does not terminate the Batch job or cascade — as today it only setsCANCELINGfor every engine, so this is not a regression, but cancelling a launcher without cascading leaves children burning compute.Testing
Highest remaining risk is PAML: the integration test drives child submission over HTTP, not through the launcher's actual platform abstraction, and PAML's own tests mock
requests, so nothing yet exercises the two sides against each other. Running the RNA-Seq launcher image against a local service with a two-sample samplesheet is the check that would catch a mismatch.scripts/launcher_example.shwalks the flow in the meantime.🤖 Generated with Claude Code