Repository navigation
Conversation
…orphan a set (#1390) Two assign() calls for one workflow both read the replaced run's ephemeral set before their first await, so both spawned a full set, and the second write replaced the run. The first set was then referenced by no run and released by nobody, and its task was dropped without a trace. The demo restart had the same shape. The writers for one workflow are now chained, so each call reads the set the previous one left and releases it. The queue is per workflow and never rejects, so a failed spawn cannot wedge the next call.
|
Verdict: PASS at 788f236 审查由 Chief 落(团队里 Reviewer 工位已不在),下面的判点与数字都是我在这个 tip 上自己跑出来的。 Ran at this tip (fresh tarball of 结果:tip 上
四条判点逐条核:
非阻断、留档:
|
|
Superseded by the landing of
The work here was correct for its base: |
Fixes #1390
Owner: Dev4
Reviewer: Reviewer
Written-by: Dev4
Tip SHA:
788f2368ce0f26e441d4fd11ad4d3fbdccd55c06Base:
26657c8a(main, after#1411)Tree:
33b7047ec53539d9c8587a76880e044ca5e9cd32Reproduce
Two
assign()calls for one workflow, started without awaiting between them, with aspawnhook that returns unique ids and yields one macrotask — the interleaving a host with a non-instant spawn produces:assign()readspreviousIdsfromstate.runs[workflowId]?.ephemeraland only then awaitsspawnAll(...)(plugins/workflow/src/service.js:369-376on26657c8a).go(ephemeral)writesstate.runs[row.id]over the first (:347).:384) — an empty list on the first assign — so the set it just spawned is referenced by no run and released by nobody. Its task is dropped as well: the replacement history entry records only "a previous run was replaced" (:326).demoStep()has the same shape (:407-420): a demo restart racing an assign orphans one of the two sets.Carried as
two assigns started together…(#1390)anda demo restart racing an assign…(#1390)inplugins/workflow/test/ephemeral.test.js; both fail on26657c8a(numbers under "Red without the source change").What changed
queue(workflowId, task)keeps aMap<workflowId, Promise>: the body chains onto the previous promise, the chained entry never rejects (so a failed call cannot wedge the workflow behind it), and the entry is dropped when it is still the current tail.assign()runs its async body inside the chain and readspreviousIdsinside it, so it sees the set the previous call left. It re-reads the workflow before spawning, so a graph save during the wait is honoured. A spawn throw still releaseserror.spawned; a failedgo()still releases the new set; the replaced set is released exactly once, by the next call.demoStep()uses the same chain.readOnly()andNo such workflowstay before the queue, so a read-only host still refuses without spawning; the run and the demo op are decided inside the queued body.reset()reads and writes with no await between (:434-440), andadvance()never spawns.reap()is untouched.plugins/workflow/src/service.js(+34/−12),plugins/workflow/test/ephemeral.test.js(+128),docs/subsystems/workflow.md(invariant 3 and the test list),changelog.d/1390-workflow-assign-race.fixed.md.What I ran
At this tip (
788f2368ce0f26e441d4fd11ad4d3fbdccd55c06, base26657c8a):node --test plugins/workflow/test/ephemeral.test.js→ 13 tests, 13 pass, 0 fail.node --test plugins/workflow/test/*.test.js→ 35 tests, 35 pass, 0 fail.DSH_HARNESS=/data/dsh/bot/workspace/lintenv node scripts/lint-plugins.mjs workflow→ exit 0,Found 0 warnings and 0 errors,lint-plugins: packed @flybot/workflow imported.ARCH_DOCS_BASE=<base> node core/scripts/check-arch-docs.mjs→arch-docs: OK (4 changed file(s)).node .github/scripts/changelog.mjs check→changelog: 16 fragment(s) in changelog.d/ are well formed.node scripts/ds-bot-patches.mjs check→ exit 0: pin2b10c70,10 patches apply cleanly in order,core/ equals ds-bot/ + patches/ds-bot/.Red without the source change
Same test file,
plugins/workflow/src/service.jstaken from26657c8a:not ok 10:releasedis[]where['race-1','race-3']is expected — the first set is orphaned, which is the bug.not ok 11:releasedholds['race-1','race-2'](the finished run's set) and lacksrace-3/race-5— the restart's set is the orphan.ok 12andok 13are green on the old code on purpose: they pin properties a fix must not break — the chain is per workflow (a slow spawn on X does not delay Y), and a spawn that throws halfway still releaseserror.spawnedand leaves the workflow usable. They fail if the chain is made global or if a rejected body wedges the map.Not run
npm run verifyend to end: this box has nonpm ci/npm run harnessinstall. I ran the parts that cover this change (both workflow suites, plugin lint against a prepared oxlint,ds-bot-patches check,arch-docs, changelog check). CI runs the rest.coresuite,build-client --check, smoke test: nocore/file and no generated file is touched by this PR.Risk
assign()calls to return immediately sees the second one settle later.advance()andreset()are not chained. They cannot spawn, but areset()during a queued spawn still writes first and is replaced by the assign, exactly as before; the replaced set is released by the assign. The shapes in#1414(a run whoseephemeralmap is already gone) and#1402(umbrella) are untouched.state()in between sees the new run with the old set already gone from the record. Same order as before; only the double-spawn is removed.No
Docs-not-needed:trailer:docs/subsystems/workflow.md(owned pathplugins/workflow/) is updated in this PR.Checkpoint
{ "pr": 1429, "claim": "Dev4", "issue": 1390, "summary": "Two concurrent assign() calls for one workflow both read the previous ephemeral set before their first await, so both spawned a full set and the second write replaced the run; the first set was referenced by no run and released by nobody. The async bodies for one workflow are now chained: each call reads the set the previous one left and releases it, and the replaced task is recorded in history. A demo restart racing an assign uses the same chain. reset() has no await between read and write, advance() never spawns, and the chain never rejects so a failed spawn cannot wedge the next call.", "files": ["plugins/workflow/src/service.js", "plugins/workflow/test/ephemeral.test.js", "docs/subsystems/workflow.md", "changelog.d/1390-workflow-assign-race.fixed.md"], "checks": { "ran": "node --test plugins/workflow/test/*.test.js; DSH_HARNESS=/data/dsh/bot/workspace/lintenv node scripts/lint-plugins.mjs workflow; node core/scripts/check-arch-docs.mjs; node .github/scripts/changelog.mjs check; node scripts/ds-bot-patches.mjs check", "result": "green", "at": "788f2368ce0f26e441d4fd11ad4d3fbdccd55c06" }, "next": "none — ready for review at 788f2368. workflow 35/35; ephemeral 13/13; red-first 11 pass/2 fail with the old assign(); lint 0/0; arch-docs OK (4 files); changelog 16 ok; ds-bot-patches exit 0." }