A scripted step's output pumps are cancelled before they drain: the transcript loses the tail, and so can an effect marker #123
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happens
ScriptedExecutor.start(src/osf/engine/executors.py) cancels both output pumps (_pump, one per stream) the momentproc.wait()returns. A pump can still be holding output at that point, and the cancel drops it: it never becomes ascript.outputevent, and an::osf:effect::marker in it is never scanned, so noosf.effect.recordedrow is written. The step then recordsscript.exitand carries on as if nothing were missing.It surfaced as a flaky test,
tests/unit/test_scheduler.py::test_a_scripted_step_streams_its_output_and_its_exit_code(assert 'script.output' in [...], line 1210): one failure in a-n autorun, 2 of 15 isolated runs with one busy loop per core, 0 of 15 on an idle machine. The test is right and the executor is wrong. The flake is the smallest case of a bigger loss.Three ways the output is lost
Each reproduced on demand, with no CPU load, by holding one thing back:
proc.wait()is first awaited,wait()returns without suspending and the pumps are cancelled before their first step. Reproduced by delaying theosf.step.deadlinewrite by 0.5 s: no output, no marker.StreamReaderstill holds up to about 100 KB that the pump never read. None of it is chunked, and none of it is scanned for markers.await emit(...). The chunk has already left the buffer, andDatabase.writeawaits aconcurrent.futures.Futurefor the writer thread: cancelling the awaiting task cancels that future, and the writer skips a job it has not started. Reproduced by parking the writer thread on a gate job: both thescript.outputevent and the marker are missing.Measured on current main, no extra load
seq 1 500(1.9 KB)seq 1 2000(8.9 KB)seq 1 10000(49 KB)seq 1 40000(229 KB)seq 1 100000(589 KB)sleep 0.3; echo '::osf:effect::{...}'seq 1 40000; echo '::osf:effect::{...}'Why it matters beyond the test
npm ci, a test run's summary, a refused push), and it is the part the console loses.pr-existsandpr-mergedread theexternal_effectrows that markers write, and the scripts inforgejo.py,plan.pyandgitflow.pyprint their marker as they finish. A marker lost this way fails a step whose pull request was in fact opened, and a redo cannot show the effect.Expected
Once the process is gone, its pumps read to EOF and flush before they are cancelled. The cancel stays for the stuck case, a pipe a grandchild still holds open, and for the stop path, and it is bounded so a step cannot hang on a drain. A deterministic regression test holds the write back, or has the child exit first, instead of relying on load.
Shipped in
7dfd2e4(deploy run #90, production restarted on it at 17:02 UTC) and archived in53abd8d. The change isopenspec/changes/archive/2026-10-02-script-output-drain, and its spec is nowopenspec/specs/script-output/spec.md.What was wrong.
ScriptedExecutorcancelled both output pumps the momentproc.wait()returned. A pump can still be holding output then, in one of three states, and each lost the output and any::osf:effect::marker in it:wait()returned at once and the pumps were cancelled before they ran. This was the flaky test: 11 of 11 failures in 160 separate pytest processes under load, with no write cancelled in any of them.A marker with no newline after it was also never scanned (0 of 10 recorded).
What changed. Once the process is gone, its pumps are waited for. A stream is waited for while it makes progress, and given up on after 5 s of silence, or 60 s after the process ended, with a log warning and a transcript line saying which. A step cancelled by the engine is unchanged.
Verified.
mainfor the reason it names. They passed 20 runs under the same load.@liveskipped.pr-exists, and its transcript was byte-identical.quick-fixrun on soundcheck#71 recorded complete transcripts forgit.clone,script.brief,script.agents-mdandscript.deps, with no notices and a clean journal. The run itself is still going.Left alone, not filed.