Skip to content

fix: make workflow fanout admission atomic - #535

Merged
chubes4 merged 2 commits into
mainfrom
fix/531-atomic-fanout-admission
Aug 26, 2026
Merged

fix: make workflow fanout admission atomic#535
chubes4 merged 2 commits into
mainfrom
fix/531-atomic-fanout-admission

Conversation

@chubes4

@chubes4 chubes4 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a per-dispatch admission generation that branch callbacks must validate before beginning effects
  • reject the generation before compensating a partial enqueue, then cancel exact-payload Action Scheduler actions before branch-store cleanup
  • preserve execution for pre-upgrade payloads that do not contain an admission token
  • defer callbacks claimed while admission is pending, with a post-enqueue rejection check that closes the compensation-scan race

Fixes #531.

Root cause

The Action Scheduler branch executor persisted and enqueued siblings one at a time, but its failure path only forgot branch-store rows. Actions inserted before a later enqueue failure remained pending or could already be claimed, allowing branch effects to begin even though the parent dispatch returned an admission failure.

Behavior change

Each new fanout starts with a pending admission generation stored at one deterministic per-run option. Branch callbacks carrying a token execute only after that exact generation is admitted. A partial failure first rejects the token, fencing already-claimed workers, then unschedules all exact-payload actions and cancels their known action IDs before releasing branch-store rows.

A callback claimed while admission is pending schedules one delayed copy and re-checks admission after enqueue. If compensation won the race, it removes matching delayed copies. Runtimes without delayed scheduling fail closed rather than immediately self-requeueing and amplifying the queue. Legacy actions durably queued before this field existed contain no admission_token key and continue through the original execution path.

Admission cleanup no longer depends on the mutable branch index: the generation lives at a deterministic per-run option ref and forget_run() deletes it directly, including when a consumer owns branch payload cleanup.

Tests

  • php tests/workflow-async-branch-payload-smoke.php
  • php tests/workflow-as-branch-smoke.php
  • composer test
  • composer phpstan
  • git diff --check

Focused regressions cover:

  • enqueue 2 failing while action 1 races compensation, with no branch side effects or runnable delayed copy
  • a pre-upgrade payload without admission_token executing normally
  • rejection after delayed enqueue removing the raced copy
  • unavailable delayed scheduling adding no immediate retry action
  • deterministic admission cleanup independent of branch indexing

AI assistance

Implemented and tested with OpenAI OpenCode assistance. The agent inspected the issue, current implementation, Action Scheduler cancellation APIs, existing workflow smoke harness, and review feedback; authored the code and regression tests; and ran the test/static-analysis commands listed above. Human review is required before merge.

@chubes4
chubes4 merged commit ab23152 into main Aug 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: make parallel branch admission all-or-nothing

1 participant