Repository navigation
Conversation
Once two matching chains existed (e.g. a registration race where two concurrent requests both saw no pending action and both scheduled), register()'s idempotent path returned at the first match and never cancelled the others: every duplicate chain kept firing once per tick, forever (observed in production as paired jobs created in the same second for system-* routines). When several pending chains now match the routine's trigger, register() collapses them: it keeps the highest-id chain and cancels the rest, then returns true as before. The highest id is the only keeper guaranteed to sit at or above the routine's current watermark (Action Scheduler ids are a global increasing sequence and the watermark is always some chain's own first id), so the kept chain can never be killed by the generation fence. A collapse is not a schedule change: no new action is scheduled and no generation/watermark is minted, keeping the unchanged register() path read-only apart from the duplicate cancels. One-shot run_now enqueues are non-recurring and are never collapsed away. Existing duplicate chains self-heal on the next registration (every plugin boot re-registers), so no concurrent double-schedule lock is added around check-then-schedule - the collapse already bounds the race's damage to a single interval, without reintroducing the stuck-lock failure mode fixed for the reconcile lock in Automattic#580. Fixes Automattic#582.
Contributor
Author
|
Superseded by #588 (merged), which collapses duplicate matching chains and also makes the check-then-schedule step single-flight and skips Action Scheduler I/O for unchanged declarations. AI disclosure: Claude (via the Pi coding agent in a Roadie session) closed this at Chris's direction during the #582 / Extra-Chill/data-machine#3495 work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #582.
Problem
A registration race (two concurrent requests both see no pending action and both schedule) leaves two matching recurring chains for a routine.
register()'s idempotent path returns at the first match and never cancels the others, so every duplicate chain fires once per tick forever. In Extra Chill production this showed up as pairedwp_agent_routine_run_scheduledjobs created in the same second (e.g. jobs 1031852/1031853) forsystem-*routines, with two separate 300s chains a day apart (first_timestamp1789233749 / 1789365037).The fix
In
register(), when one or more pending chains already match the routine's trigger, all matching chains are collapsed before returningtrue:run_nowenqueues survive: they are non-recurring, never match, and are never collapsed away.Design calls
add_optionlock around check-then-schedule. The collapse bounds the race's damage to a single scheduling interval, because routines re-register on every boot. A short lock would reintroduce the stuck-lock / cache-drift failure mode that fix: retry transient InnoDB deadlocks during option-store lock acquisition #580 just fixed for the reconcile lock (and which forced that lock down to raw$wpdb). Not worth it for a self-correcting condition.run_nowenqueue. Not worth it.run_nowwould silently drop a manual wake.Tests
tests/routines-durability-smoke.php(all against the fake in-memory AS store):before_execute(id below watermark), does not fire, spawns no successor; the keeper fires normally;run_nowenqueue survives the collapse; no generation option is minted when none existed (pre-fence legacy chains).Verified the new assertions fail on
main(6 failures) and pass with the fix; fullcomposer smokeandcomposer phpstanare green.