refactor(scheduling): remove HashGatedRoutineBackend; rely on idempotent agents-api bridge - #3498
Merged
Merged
Conversation
…ent agents-api bridge The Agents API Action Scheduler bridge's register() (wordpress/agents-api v0.11.1) is now idempotent itself: it compares a pending action's recurrence (interval seconds / cron expression) against the routine and no-ops when nothing changed. HashGatedRoutineBackend duplicated that idempotency one layer up with a separate persisted option (datamachine_routine_schedule_hashes) that had its own failure mode — a poisoned notoptions cache entry for that key defeated the gate during the 2026-09-12 outage and every request rescheduled all 717 routines. - Delete HashGatedRoutineBackend.php and the wp_agent_routine_backend filter that installed it (FlowRoutines::install_backend()). - Remove every persist()/set_verification_mode() call from FlowRoutines::sync()/unschedule()/boot()/reconcile(). boot()'s try/finally and reconcile()'s inner try/finally existed solely to guarantee persist() ran; both are gone now that there is nothing to persist. Per-routine try/catch in register_routine_logged() is unchanged. - One-shot cleanup: datamachine_run_deferred_site_setup() (runs on every DATAMACHINE_VERSION bump via datamachine_maybe_ensure_current_schema()) now deletes the dead datamachine_routine_schedule_hashes option. - Rewrite tests/Unit/Engine/Scheduling/FlowRoutinesTest.php and tests/flow-routines-reconcile-lock-before-boot-smoke.php against the plain bridge: the smoke's persist() call-count spy is gone, and the throw-tolerance case is now "boot() continues past a throwing register() and the remaining routines are still registered" rather than "persist() survives a throw". Reconcile-lock-before-boot coverage is unchanged. - Update docs/core-system/routines-scheduling.md to describe substrate-owned idempotency instead of the removed decorator. Verification: both smoke tests pass (php tests/flow-routines-reconcile-lock- before-boot-smoke.php, php tests/flow-schedule-reconciliation-worker-smoke.php), php -l on every touched file, grep -rn HashGated inc tests docs returns nothing, and homeboy review audit --placement local data-machine --changed-since <merge-base> reports passed=true, drift_increased=false, 0 introduced_findings. Closes #3497
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.
Closes #3497
Summary
HashGatedRoutineBackendwas a consumer-side idempotency decorator around the Agents API routine registry backend. As ofwordpress/agents-apiv0.11.1 (already pinned incomposer.lockvia #3496) the Action Scheduler bridge'sregister()is idempotent itself — it compares a pending action's recurrence (interval seconds / cron expression) against the routine and no-ops when nothing changed. The decorator duplicated that check one layer up with its own persisted option (datamachine_routine_schedule_hashes) and its own failure mode: a poisonednotoptionscache entry for that key defeated the gate during the 2026-09-12 outage and every request rescheduled all 717 routines.This PR deletes
HashGatedRoutineBackendand everything that only existed to serve it, per the six numbered steps in #3497.Before / after
FlowRoutines::boot()wp_agent_routine_backendfilter wrapping the resolved backend inHashGatedRoutineBackend, ran the registration loops inside atry { ... } finally { HashGatedRoutineBackend::persist(); }whose sole purpose was guaranteeing the fingerprint cache got flushed even if a routine threw.register_routine_logged(), unchanged) so one bad routine can't stop the rest of the set from registering — but there's nothing left to persist, so the wrapper around the whole loop is gone.FlowRoutines::reconcile()boot()→HashGatedRoutineBackend::set_verification_mode(true)→Registry::reconcile()inside a nestedtry/finallythat turned verification mode back off and calledpersist().boot()→Registry::reconcile()directly. The DM reconcile lock (FlowScheduleReconciliationLock, untouched) still wraps the whole thing.FlowRoutines::sync()/unschedule()HashGatedRoutineBackend::persist()call in both; nothing else changed.One-shot cleanup
datamachine_run_deferred_site_setup()(already runs on everyDATAMACHINE_VERSIONbump viadatamachine_maybe_ensure_current_schema()) now alsodelete_option('datamachine_routine_schedule_hashes'), so existing installs don't carry the dead ~40 KB option row forward.delete_option()on an absent row is a no-op, so this is safe to run unconditionally on every version bump rather than needing its own marker.Tests
tests/Unit/Engine/Scheduling/FlowRoutinesTest.php: dropped theHashGatedRoutineBackendimport,reset_state()call, and the staledatamachine_routine_schedule_hashesteardowndelete_option(). The idempotence test (test_sync_is_idempotent_and_does_not_reset_timers) is unchanged — it now exercises the substrate's own idempotentregister()instead of the decorator's fingerprint cache, and still passes.tests/flow-routines-reconcile-lock-before-boot-smoke.php: removed the fakeHashGatedRoutineBackendnamespace stub and everypersist()/set_verification_mode()/$persist_callsreference. The lock-before-boot coverage (part a) is unchanged. The throw-tolerance case (part c) is reframed from "persist() survives a throw" to "boot() continues past a throwing register() and the remaining routines are still registered" — the assertion that was already there (array('flow-1','flow-2') === $register_calls) is now the whole point of that case instead of a side effect.Docs
docs/core-system/routines-scheduling.mdupdated to describe substrate-owned idempotency instead of the removed decorator, and to reference agents-api v0.11.1.Verification
php tests/flow-routines-reconcile-lock-before-boot-smoke.php— all assertions passphp tests/flow-schedule-reconciliation-worker-smoke.php(unaffected, noHashGatedreferences) — all assertions passphp -lon every touched PHP file — no syntax errorsgrep -rn HashGated inc tests docs— no matcheshomeboy review audit --placement local data-machine --path <worktree> --changed-since $(git merge-base origin/main HEAD)—passed: true,baseline_comparison.drift_increased: false,changed_since.introduced_findings: 0Note: this repo has no
phpcs.xml/phpcs.xml.distchecked in — the WPCS ruleset is supplied by homeboy's managed environment (seehomeboy.json'swordpressextension), not present in a bare git checkout.php vendor/bin/phpcsagainst a bare local install falls back to the PEAR default standard and produces noise on files I didn't touch too, so I relied on the homeboy audit run above (which uses the managed ruleset) plusphp -lfor local lint/syntax verification instead of a barecomposer lintinvocation. The PHPUnit path forFlowRoutinesTest.php(WP_UnitTestCase) needs a bootstrapped WordPress test environment that this bare worktree doesn't have locally — CI'sHomeboy Testworkflow runs the full MySQL suite against this PR.Do-not constraints respected
inc/Api/Flows/FlowScheduleReconciliationLock.phpand the deferred-reconcile worker (fix(scheduling): lock flow schedule reconcile before boot(), not after #3494) untouched.