Code review · P3 Task 6, nf-spectio writes the spool¶
review-code on autopilot/P3-t06, base a758cf3, 2026-10-30. The step is P1 Task 6. It adds:
engine-bridges/nf-spectio/src/spectio/SpectioPlugin.groovy: aTraceObserverV2plugin, ported from the spike T2 prototype. The hooks enqueue allow-listed data; one writer thread appends envelopes to the spool and rotates;engine-bridges/nf-spectio/build.sh(builds the unpacked plugin without Gradle) andtests/run_local.sh(runs the synthetic pipeline with it);packages/spectio/tests/test_bridge_nextflow.py, 14 tests markedengines, skipped by the defaultpytestrun (pyproject.toml);- in CI: a step in
pipelines-synthetic.ymlthat runs them on the Nextflow legs, and the bridge paths inci.yml's PR filter.
The reviewers were code-reviewer and security-reviewer, independent and in parallel. Format: README.md. What the bridge writes is owned by api §14.1; how it fails, by architecture §13.3. The rules decided here are in inbox #82.
| Round | Head or range | code-reviewer | security-reviewer |
|---|---|---|---|
| 1 | 511976c (the step) |
changes (0 blockers, 1 major, 7 minors) | approve (0 blockers, 0 majors, 5 minors) |
| 2 | 694a42c (the round-1 fix) |
approve (0 blockers, 0 majors, 5 minors) | approve (0 blockers, 0 majors, 5 minors) |
| 3 | 2074d6d (fixes of three round-2 minors) |
approve (0 blockers, 0 majors, 2 minors) | approve (0 blockers, 0 majors, 1 minor) |
Minors include the simplify: finding, as in the earlier records.
Rounds. One fix round for the major, then both approved in round 2. The worker then fixed three round-2 minors (2074d6d), and round 3 reviewed that fix.
IDs repeat across rounds. Each round numbered its findings again, so each ID below carries its round: CR-P3T06-02 (r1) is not CR-P3T06-02 (r2). The commit messages use the bare IDs.
Test-first. The worker's record: each fix began with a failing engine test. Test command, passing at 17281b7 with Nextflow 26.10.4 (13 passed, 1 skipped as root):
uv run pytest -m engines packages/spectio/tests/test_bridge_nextflow.py -v
CODE step=P3-t06 review=approve b/M/m=0/1/14 security=approve b/M/m=0/0/11 rounds=3 simplify=changed -1/+1 tests=pass
The counts are per reviewer over the three rounds, each finding counted in the round that raised it; a problem raised again counts again. code-simplifier ran after round 3 and made one change, 17281b7 (CR-P3T06-08 (r1)). Its baseline and its after-change run of the default gate were the same: pytest 214 passed, 14 deselected; ruff, mypy, lint-imports and uv lock --check clean. It ran the engine tests after the change only.
Findings¶
The finding texts are summarised from the round summaries and the fix commits' messages. A pair such as CR-P3T06-04 (r1) / SEC-P3T06-05 (r1) is one problem found by both reviewers.
| Id | Severity | Finding | Status |
|---|---|---|---|
| CR-P3T06-01 (r1) | major | A write that keeps failing (block quota, ENOSPC, EFBIG) created a new spool file for each event. After 1024 files (the reader's MAX_SPOOL_FILES) every later file of the launch counts too_many_files, onFlowComplete included; at index 9999 the bridge stops writing for good. |
fixed in 694a42c: a failed write does not rotate; the writer reopens only when the file is gone, and reuses an empty current file (test_writer_does_not_rotate_on_a_failed_write, under ulimit -f 16) |
| CR-P3T06-02 (r1) | minor | The close-time line "N events dropped: the queue was full" goes through the 100-line cap, so after 100 errors the drop count is lost, and nothing says lines were suppressed. | fixed in 2074d6d, as CR-P3T06-04 (r2) |
| CR-P3T06-03 (r1) | minor | Code comments cite Nextflow internals (Session.groovy:919-936, WorkflowMetadata.setErrorAttributes) with no tag or commit. |
partly fixed: api §14.1 and architecture §13.3 now cite them at tag v26.10.4 (807ef3a), read 2026-10-30. Open: the code comments, follow-up FU-P3-10 |
| CR-P3T06-04 (r1) / SEC-P3T06-05 (r1) | minor | The PR path filter runs the bridge test when the bridge or its test changes, not when the contract it replays through changes (spectio_sdk/events.py, spectio/runner/spool.py, the projector). A break shows first on main. |
open, follow-up FU-P3-09 |
| CR-P3T06-05 (r1) | minor | Plugin-Requires: >=25.04.0 declares a floor that CI never runs (the matrix is 26.10.4 and 25.10.9); architecture §13.4 says the range equals the versions tested in CI. The plan asks for the 25.04 floor; 25.04.8 was checked by hand only. |
open, assumed: inbox #82 rule 4; the 25.04.8 CI leg is follow-up FU-P3-11; architecture §13.4 says so |
| CR-P3T06-06 (r1) | minor | With no framework jar, build.sh falls back to the launcher file as the classpath. That works only for a -dist launcher; otherwise java fails with an unrelated "could not find or load main class". |
open, follow-up FU-P3-10 |
| CR-P3T06-07 (r1) | minor | On a run with no failed task, errorMessage is Nextflow's own error (session.error.message), capped at 4096 characters, not filtered. A script or config error can quote source or paths. |
open, assumed: inbox #82 rule 1; api §14.1 says so |
| CR-P3T06-08 (r1) | minor | simplify: the "no exit status yet" rule is written twice, in exit() and as a string comparison in trace(). |
applied by code-simplifier in 17281b7: trace() calls exit() |
| SEC-P3T06-01 (r1) | minor | The redaction reads WorkflowMetadata.errorMessage, whose content depends on the Nextflow version, and blanks it on one known condition only (session.fault.task != null). It fails open if a later Nextflow puts task output there under another condition; Plugin-Requires has no upper bound. |
open, assumed: inbox #82 rules 1 and 4 |
| SEC-P3T06-02 (r1) | minor | SEC-P3T1-09 / FU-P3-08 is closed in code but not in the spec: api §14.1 still says errorMessage is kept, and api §7 says the failure message is that errorMessage. Nextflow's own error text is still sent, up to 4096 characters. |
spec fixed by this step's docs (api §14.1, api §7 runs.failure). Open: the residual, inbox #82 rule 1 |
| SEC-P3T06-03 (r1) | minor | The writer does not apply the reader's confinement: createDirectories follows a symlinked .spectio; no owner or mode check of .spectio or events; files reopened by path after createFile; bridge.err appended whatever its owner or mode, and placed in <SPECTIO_SPOOL_DIR>/.. without checking that path. Matters in group-writable launch directories (terminal runs, architecture §13.8), not in the 0700 run directories of driven runs. |
partly fixed in 2074d6d: every open passes NOFOLLOW_LINKS, and bridge.err must be a regular file. Open: P1 Task 25, the fd-relative writes of dev-env §15.3 req. 12 |
| SEC-P3T06-04 (r1) | minor | The in-JVM queue is bounded by count (100 000 items), not by bytes: hundreds of MB of head heap while the writer stalls. The shutdown hook calls the synchronized writer.error / close after join(10 s), so a writer blocked in write() keeps the JVM from exiting. |
hang fixed in 2074d6d, as SEC-P3T06-02 (r2). Open: the byte bound, inbox #82 rule 3 |
| CR-P3T06-02 (r2) | minor | After the round-1 fix, a torn write followed by a rotation leaves an unended line that the reader never counts; the tracked size misses the partial bytes. | fixed in 2074d6d: the file is truncated back to size after a failed write (test_writer_truncates_a_failed_write) |
| CR-P3T06-03 (r2) | minor | Two new branches of the round-1 fix are untested (ending a torn line, reopening an empty file), and the file-count assertion (1 <= len(files) <= 2) is looser than needed. |
partly fixed: the torn-line branch is gone in 2074d6d. Open: a test of the empty-file reopen, and the tighter assertion, follow-up FU-P3-10 |
| CR-P3T06-04 (r2) | minor | After 100 earlier errors (easy under ENOSPC, one per event), the cap also suppresses the close-time drop summary. | fixed in 2074d6d: SpoolWriter.finish() writes "N further errors not logged" and the drop summary past the cap (test_error_cap_keeps_the_summary) |
| CR-P3T06-05 (r2) | minor | The fix changes data-loss behaviour and nobody decided it: under a per-file limit (EFBIG: ulimit -f, a file system's maximum file size), events after the limit are lost for the rest of the run. Before, they spilled into new files. |
open, assumed: inbox #82 rule 2; architecture §13.3 says so |
| CR-P3T06-06 (r2) | minor | The docs still describe the old errorMessage behaviour, and the Nextflow citations in code comments name no version. |
docs fixed by this step (api §14.1, §7); code comments open, as CR-P3T06-03 (r1) |
| SEC-P3T06-01 (r2) | minor | run_local.sh lets Nextflow download nf-spectio@0.0.1 from the public registry when the local unpacked copy is not found. A release published under that ID would run in CI as the plugin under test. |
open, follow-up FU-P3-10. The registry ID is now held by spectio-org (inbox #66, answer 2026-10-29), which narrows the risk to a release of our own |
| SEC-P3T06-02 (r2) | minor | The shutdown hook has no time bound: after 5 s + 10 s it calls the synchronized writer.close() and writer.error(), so a writer blocked holding the monitor (a stalled shared file system, a FIFO as bridge.err or a spool file) keeps the head alive. Reproduced: with .spectio/bridge.err a FIFO, the JVM was alive after timeout's SIGTERM at 60 s; only SIGKILL ended it. Also, after a timed-out join, the writer could create a new spool file after close. |
fixed in 2074d6d: when the writer thread is alive after the join, Spool.close() prints one stderr line and returns, without finish() or close(); bridge.err is written only when it is a regular file (test_stuck_writer_does_not_hang_the_head, test_bridge_err_fifo_does_not_hang_the_head). Residual: SEC-P3T06-03 (r3) |
| SEC-P3T06-03 (r2) | minor | req. 12's bridge rules for the launch directory are not applied (symlinks followed by createDirectories, no owner or mode check, opens by path). Not exploitable in driven runs (0700, req. 5). |
open: P1 Task 25, as SEC-P3T06-03 (r1) |
| SEC-P3T06-04 (r2) | minor | The queue holds up to 100 000 items of about 12 keys plus a trace of up to 41 strings: about 0.5 GB of head heap (estimate, not measured). Hooks never block, but heap pressure can still fail the run. | open, assumed: inbox #82 rule 3 |
| SEC-P3T06-05 (r2) | minor | SEC-P3T1-09 and FU-P3-08 are handled correctly: errorMessage is null whenever session.fault.task is set, the case where Nextflow fills it with the task's stderr or stdout (WorkflowMetadata.groovy:386-395, v26.10.4). The kept text is session.error.message, which can quote values: Nextflow's own errors, a workflow's error() text, compile errors, validation messages [to be validated]. |
open, assumed: inbox #82 rule 1 |
| CR-P3T06-05 (r3) | minor | run_writer now runs every writer script with capture_output=True and check=True; a failing Groovy script's stack trace is no longer in pytest's failure report. |
open, follow-up FU-P3-10 |
| CR-P3T06-06 (r3) | minor | When the join times out, Spool.close() prints the queue-full drop count only, not the events still in the queue, which are lost when the JVM halts. A slow writer (a large backlog on slow NFS) loses the end of the stream and nothing counts it. |
open, follow-up FU-P3-10 |
| SEC-P3T06-03 (r3) | minor | The FIFO guard on bridge.err is a check, then a separate open by path. finish() now runs on the shutdown-hook thread, which nothing bounds: a FIFO swapped in between isRegularFile and Files.write hangs the hook, and the head never exits. Only a process of the same user can make the swap (directories 0700); the spool files' check-then-open runs on the writer thread, which the join timeout bounds. |
open: P1 Task 25 (fd-relative opens), or a bound on the hook first; inbox #82 rule 3 |
Closed from earlier reviews:
| Id | Outcome |
|---|---|
| SEC-P3T1-09 (record) | closed for the task's stderr: onFlowComplete.errorMessage is sent as null when a task failed (test_task_stderr_kept_out_of_the_spool). Nextflow's own error text is kept: inbox #82 rule 1 |
| FU-P3-07 (spike first-look) | closed at the source: exitStatus and trace.exit of 2147483647 (Integer.MAX_VALUE, "no exit status yet") are sent as null (test_no_exit_yet_is_never_an_exit_code) |
| FU-P3-08 (spike first-look) | closed with SEC-P3T1-09: runs.failure.message no longer carries the tool's stderr |