Spike T1 — fixture workflows, golden fixtures and resume identity (2026-10-27)¶
Question.
- Does every failure mode of the synthetic fixture workflows (
dev-environment-and-testing.md§3, §10.1) show up on the local executor, with the same outcome on every run, on each supported engine version? - What does each engine do with the run's identity on a resume (inbox #32)?
Answer.
- Scenarios. All 11 PR scenarios pass on Nextflow 26.10.4, 25.10.9 and 25.04.8 and on Snakemake 9.27.1. Every suite ran twice, and the normalised task outcomes were identical both times. They are now golden fixtures, which CI compares on every run.
- Fixes. The runs found and fixed three faults in the fixtures: the Snakemake align gate, the
dockerprofile, and a wrong claim about tool-error retries. - Resume identity. Nextflow
-resumegets a new run name and keeps the same session ID, and it only works from the same launch directory. A Snakemake rerun gets a fresh randomworkflow_id, and nothing links it to the previous invocation. - nf-core gate check. The nf-core/rnaseq and nf-core/sarek gate stop-and-resume check was not run: Docker works here, but the measured cost is over this step's budget (see below).
Setup¶
-
Machine:
- Claude Code cloud session: 4 CPUs, 15 GB RAM, 26 GB free disk;
- Linux 6.18, OpenJDK 21.0.12;
- Docker 29.9.2 started by hand (
dockerd --registry-mirror=https://mirror.gcr.io, cgroup v1).
-
Engines. Installed by
dev/bootstrap.shat the pins ofdev/versions.env: Nextflow 26.10.4, 25.10.9 and 25.04.8, and Snakemake 9.27.1. -
Commands (from
tests/pipelines/synthetic/):python3 run_scenarios.py --engine nextflow --nextflow nextflow --workdir $W/nf26 --golden record python3 run_scenarios.py --engine nextflow --nextflow nextflow-25.10.9 --workdir $W/nf25 --golden record python3 run_scenarios.py --engine nextflow --nextflow nextflow-25.04.8 --workdir $W/nf2504 --golden record python3 run_scenarios.py --engine snakemake --workdir $W/smk --golden record # then the same four with --golden check in a fresh workdir: all exit 0 python3 -m unittest tests/pipelines/synthetic/test_golden.py # 8 tests, OK (5 at e2064ff; 3 added by the review fixes, 7202d9f)
Golden fixtures¶
- Where.
tests/pipelines/synthetic/golden/<engine>-<version>/<scenario>.tsv. There is one file per PR scenario: 11 per engine version, 44 in all, about 500 lines. -
What each file holds:
- the exit code of each run;
- the task outcomes, from the Nextflow trace (
execution_trace.txt, read after each run) or from the Snakemake log; - the columns
run, process, status, exit, attempt, tag, count.
-
Normalisation. Timestamps, hashes, durations, task IDs and paths are dropped.
- First-attempt
COMPLETEDandCACHEDtasks are counted under the tag*. - Every other outcome keeps its tag (sample or wildcards).
- First-attempt
-
Target filter. When a scenario has a run that must fail, only the injected task is kept (process plus sample).
- Why: after a fatal failure, which other tasks finished, were aborted or were cached depends on timing.
- Example: in
tool_error_then_fixed_resumethe resume run had 62 cached tasks in one execution and 63 in the next, on each of the three Nextflow versions.
-
Checks:
run_scenarios.py --golden checkfails a scenario whose outcomes differ from its fixture, and writesgolden.actual.tsvnext to the run. Three cases fail the run: a missing fixture directory for the engine version, a version that does not read asX.Y.Z, and a missing file for a PR scenario. Only nightly scenarios may have no fixture.- A deliberately altered fixture (
happy, ALIGN 24 → 23) failed the check, and the restored one passed. test_golden.pyunit-tests the normaliser on sample trace and log text, and checks that every engine version has exactly one fixture per PR scenario.
-
CI.
pipelines-synthetic.ymlnow runs the unit test and--golden check. Its Nextflow 26.10 pin moves from 26.10.3 to 26.10.4, so that it matchesNEXTFLOW_VERSIONindev/versions.envand has fixtures. Nextflow 25.04.8 has fixtures but no CI leg (open item 4).
Failure modes proven (local executor)¶
All rows are from the golden fixtures and are identical on both executions.
| Mode | Scenario | NF 26.10.4 | NF 25.10.9 | NF 25.04.8 | Snakemake 9.27.1 |
|---|---|---|---|---|---|
| flaky (fails on attempt 1) | flaky_retry |
ALIGN S07, 2 chunks: FAILED exit 1 at attempt 1, COMPLETED at attempt 2; run exit 0 | same | same | same |
| OOM once (exit 137) | oom_once_retry |
QC_STATS S05: FAILED 137 at attempt 1, COMPLETED at attempt 2; run exit 0 | same | same | same |
| OOM, fatal | oom_fatal |
QC_STATS S05: FAILED 137 at attempts 1, 2, 3; run exit 1 | same | same | same |
| time limit, fatal | timeout_fatal |
FAILED 143 at attempts 1–3; run exit 1 | FAILED with no exit code (-) at attempts 1–3 |
same as 25.10.9 | FAILED 124 (timeout in the shell) at attempts 1–3 |
| non-zero exit, then fix and resume | tool_error_then_fixed_resume |
COMPARE treatA FAILED exit 1, 1 attempt; run exit 1; resume: COMPLETED, exit 0 | same | same | FAILED exit 1 at attempts 1, 2, 3; rerun: COMPLETED, exit 0 |
| checkpoint / dynamic fan-out | dynamic_fanout |
45 ALIGN tasks | 45 | 45 | 45 (the split checkpoint) |
| fan-out, PR size | fanout_medium |
180 ALIGN tasks | 180 | 180 | 180 |
| gate stop, then resume | gate_qc_then_resume |
resume: 25 cached, 40 run | same | same | rerun: 42 jobs, nothing upstream of the gate |
gate_align_then_resume |
resume: 61 cached, 4 run | same | same | rerun: 5 jobs (after fix 1) | |
exclude_sample_and_resume |
resume: 22 cached, 38 run | same | same | rerun: 40 jobs | |
| real OOM (allocate beyond the limit) | oom_real, by hand (not a scenario) |
QC_STATS S05: FAILED 137 at attempts 1–3 under docker run --memory 512m / 1024m / 1536m (2 GB allocated); run exit 1 |
not run | not run | not run |
The happy scenario runs 65 tasks on Nextflow and 67 jobs on Snakemake (the extra two are the all and align_done target rules). The Snakemake engine fault listed in scenarios.yaml (lost-retry-at-checkpoint-update) did not occur: oom_once_retry ran 4 times without it.
The oom_real run used this command, after fix 3:
nextflow -q run tests/pipelines/synthetic/nextflow/main.nf -profile test,local,docker --outdir results \
--fail_mode oom_real --fail_process QC_STATS --fail_sample S05
The --memory 512m flag is visible in the task's .command.run.
Findings and fixes¶
-
The Snakemake align gate skipped the qc stage.
- Cause.
--until align_done"only runs jobs that are dependencies of the specified rule or files, does not run sibling DAGs" (cli.pyL892–L902 at v9.27.1). - Effect.
qc_statsandqc_reportnever ran before the gate. On the rerun, thetemp()reads had already been deleted, so 12make_readsand 2splitjobs ran again. - Fix. The harness now stops at that gate with
--until qc_report align_done. The rerun then runs only 5 jobs (all,merge, twocompareandfinal_report), and the first run must now producemultiqc/general_stats.tsv(a newscenarios.yamlexpectation). - For Spectio. A Snakemake gate after stage N must name the last target of every stage up to N, not only stage N's.
- Spec. §10.1 now records the harness fact. The generation rule belongs to
checking-scenarios.md§12.3, which needs a change: inbox #48.
- Cause.
-
Snakemake retries tool errors; Nextflow does not.
--retriesis the "Number of times to restart failing jobs" (cli.pyL1508–L1513 at v9.27.1), whatever the exit code. Sotool_error_then_fixed_resumeruns 3 attempts on Snakemake.- On Nextflow, the
errorStrategyclosure innextflow.config(line 17) retries exit codes 137, 140, 143 andInteger.MAX_VALUE, and any failure whenparams.fail_mode == 'flaky'. That is whyflaky_retry(exit 1) is retried on Nextflow while the exit-1 tool error is not. - Spec. §10.1 said "a non-retried tool error"; that row is now corrected, and so is the
scenarios.yamltitle. - For Spectio. On Snakemake, failures are retried by count, not by class, unless the workflow's
retriesor resources logic says otherwise [to be validated: per-rule retry conditions in Snakemake 9].
-
The
dockerprofile of the Nextflow twin could not run any task. Two causes:-
psmissing.python:3.12-slimhas nops, and Nextflow stops every task with "Command 'ps' required by nextflow to collect task metrics cannot be found" (observed on 26.10.4).- Fix: the
dockerandk8sprofiles now usepython:3.12, pinned by digest (sha256:98d8fe4e…). It ships procps-ng 4.0.4 (docker run --rm python:3.12 ps --version), but it is 1.62 GB against 182 MB for-slim.
- Fix: the
-
Dangling link. Nextflow mounts
nextflow/bininto the container, butnextflow/bin/synth.pyis a link to../../bin, which was not mounted, so the task saw "synth.py: command not found".- Fix:
docker.runOptionsmounts the sharedbin/read-only. - The
k8sprofile has the same layout problem; T4 must solve it with its own storage.
- Fix:
-
After both fixes,
oom_realgives exit 137 under Docker's memory limit (table above). Spec §3 listsoom_realamong the failure modes, and this is its first proof.
-
-
No exit code on a Nextflow 25.x time limit. Nextflow 25.04.8 behaves like 25.10.9 here: no exit code, so the trace shows
-(the 2026-10-26 spike saw it on 25.10.9 only). Only 26.10.4 reports 143.- Spec. The §10.1 row named 143 for every version; it now gives the code per version.
Resume identity (inbox #32)¶
Nextflow¶
Observed on 26.10.4 and 25.10.9 (same results):
- The commands:
gate_qc_then_resume(a stop withconf/stop_after_qc.config, then-resume), thennextflow logand.nextflow/history.
| What | Observation | Source |
|---|---|---|
| Run name | New on every launch, resume included: sharp_meninsky then dreamy_mclean (26.10.4); festering_picasso then infallible_elion (25.10.9). Reusing one with -resume -name dreamy_mclean fails: "Run name `dreamy_mclean` has been already used -- Specify a different one", exit 1. |
checkRunName(), CmdRun.groovy L551–L566 at v25.10.9. At v26.10.4 the file is not at that path; the 26.10.4 behaviour is observed only [to be validated: source location in 26.10] |
| Session ID | Reused. Both rows of nextflow log carry the same session ID (2816c414-… on 26.10.4, c2170367-… on 25.10.9). .nextflow/cache/ holds one directory, named after it. |
-resume with no ID, or last, takes the last session ID in the history (ConfigBuilder.groovy L536–L550 at v25.10.9). The session then takes that ID as its uniqueId; a launch without resume gets NXF_UUID or a random UUID (Session.groovy L374–L379 at v25.10.9; L431–L435 at v26.10.4). Docs: "Nextflow resumes from the previous run by default" (cache and resume, "Resuming from a specific run") |
| History | Each launch appends one line to .nextflow/history: the run name, the session ID, the revision and the command line. |
ScriptRunner.groovy L281–L287 at v25.10.9 (L281–L285 at v26.10.4) |
| Launch directory | The same one is required. From another directory, -resume <session-id> -w <old work dir> fails with "Can't find a run with the specified id: … -- Execution can't be resumed", exit 1. |
The resume check looks the ID up in the history (ScriptRunner.groovy L282–L284 at v25.10.9). The history and the cache sit under NXF_CACHE_DIR, by default .nextflow in the launch directory (Const.groovy L70; HistoryFile.groovy L38; DefaultCacheStore.groovy L67). Docs: "The default cache store uses the .nextflow/cache directory, relative to the launch directory" |
Consequence for #32 (option B).
- A Nextflow resume is a new run (new run name, new history line) of the same session (same session ID, same task cache), in the same launch directory.
-
A child run per resume therefore matches Nextflow's own model, provided Spectio records both IDs:
- the run name, unique per launch;
- the session ID, shared by the whole lineage.
-
Spectio could also set the session ID of a first launch itself, through
NXF_UUID(Session.groovyL379 above).
Snakemake¶
Observed on 9.27.1, with gate_qc_then_resume (--until qc_report, then a plain rerun in the same directory):
| What | Observation | Source |
|---|---|---|
| Run identity | Each invocation has its own random workflow_id: 486ceb8b-… for the first run, then 8e78c667-… for the rerun. It is printed in the log header ("Workflow ID") and sent to logger plugins in the WORKFLOW_STARTED event. Nothing derives it from, or links it to, the previous invocation. |
"workflow_id": uuid.uuid4() in info_header (workflow.py L303 at v9.27.1), emitted with LogEvent.WORKFLOW_STARTED (L1292–L1299); printed by logging.py L168 |
| Per-invocation state | One log file per invocation, named by its start time (.snakemake/log/2026-10-27T215502.610861.snakemake.log, then …T215506…). The per-output records in .snakemake/metadata/ have no run or workflow ID. Their keys are code, conda_env, container_img_url, endtime, external_jobid, incomplete, input, input_checksums, job_hash, log, params, record_format_version, rule, shellcmd, software_stack_hash and starttime. |
observed |
| Job identity | Stable across invocations: a job's UUID is a UUID5 of its rule name and wildcards. | jobs.py L1202–L1207, common/__init__.py L268–L269 at v9.27.1 |
| What links the runs | Only the working directory: its outputs and their metadata decide what reruns. | observed (gate_* rows above) |
Consequence for #32.
- A Snakemake rerun is a new invocation with a new
workflow_id, and nothing in the engine records a lineage. - With option B, a child run per resume maps one-to-one onto Snakemake invocations, and Spectio has to record the parent link and the shared directory itself.
- Option A (the same run continues) would have to hide two
workflow_ids behind one Spectio run on Snakemake, and two run names on Nextflow.
nf-core/rnaseq and nf-core/sarek: not run¶
- Not the blocker: Docker.
dockerdstarts in this session (Docker 29.9.2, cgroup v1). -
The blocker: the cost, measured on CI runners in corpus-ci-2026-10-27:
- nf-core/rnaseq 3.27.0: 18–23 min per full run. The lowest free disk was 23.0 GB on a runner that starts with about 46 GB free, so the run used about 23 GB. This session has 26 GB free.
- nf-core/sarek 3.10.0: 9 min. The lowest free disk was 27.3 GB on the same kind of runner.
- Missing tooling.
tests/pipelines/real/run_corpus.pycannot stop at a gate or resume yet: it runs-profile test,dockeronce.
-
Total. A stop run, a resume run and the gate configs for both pipelines would take well over the 20 minutes set for this step. So this step did not run them.
-
Where they should run instead.
pipelines-real.ymlalready runs rnaseq and sarek nightly. Two changes would turn those entries into the gate check, at about the cost of one full run each:- give
run_corpus.pya per-entrygatekey: a generatedext.when = falseconfig for the processes after alignment, then-resumewithout it, recording the cached-task count and the exit code of each run; - use it in those two nightly entries.
No new job is needed. This is open item 1.
- give
Open items¶
- Gate stop-and-resume on rnaseq and sarek. Add
gatesupport torun_corpus.pyand use it in their nightly entries (above). This closes CH-016 (inbox #28). Tracked as inbox #47. - Job arrays in the
slurmprofile (array = 20on ALIGN): not provable with the local executor. To be proven in T3 on sind. fanout_stress(about 12 000 ALIGN tasks, nightly): not run here. It has no golden fixture, which--golden checkallows for nightly scenarios.- Nextflow 25.04.8, the supported floor (§1), passes and has fixtures, but has no CI leg in
pipelines-synthetic.yml. Adding one costs about 5 min per run. oom_realon Snakemake, andoom_realas a scenario: needs a memory limit that is actually enforced (Docker, SLURM or Kubernetes). To be proven in T3 (OUT_OF_MEMORY) and T4 (OOMKilled).- The
k8sprofile needs the sharedbin/inside the pod (finding 3). T4 must solve it. - Inbox #32. The observations above support option B. The decision stays with Thomas.