Skip to content

refactor(scheduling): remove HashGatedRoutineBackend; rely on idempotent agents-api bridge - #3498

Merged
chubes4 merged 1 commit into
mainfrom
refactor/remove-hash-gated-routine-backend
Sep 12, 2026
Merged

chubes4 merged 1 commit into
mainfrom
refactor/remove-hash-gated-routine-backend

Conversation

@chubes4

@chubes4 chubes4 commented Sep 12, 2026

Copy link
Copy Markdown
Member

Closes #3497

Summary

HashGatedRoutineBackend was a consumer-side idempotency decorator around the Agents API routine registry backend. As of wordpress/agents-api v0.11.1 (already pinned in composer.lock via #3496) the Action Scheduler bridge's register() 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 poisoned notoptions cache entry for that key defeated the gate during the 2026-09-12 outage and every request rescheduled all 717 routines.

This PR deletes HashGatedRoutineBackend and everything that only existed to serve it, per the six numbered steps in #3497.

Before / after

FlowRoutines::boot()

  • Before: installed the wp_agent_routine_backend filter wrapping the resolved backend in HashGatedRoutineBackend, ran the registration loops inside a try { ... } finally { HashGatedRoutineBackend::persist(); } whose sole purpose was guaranteeing the fingerprint cache got flushed even if a routine threw.
  • After: no backend filter install, no top-level try/finally. Per-routine registration is still wrapped in its own try/catch (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()

  • Before: boot()HashGatedRoutineBackend::set_verification_mode(true)Registry::reconcile() inside a nested try/finally that turned verification mode back off and called persist().
  • After: boot()Registry::reconcile() directly. The DM reconcile lock (FlowScheduleReconciliationLock, untouched) still wraps the whole thing.

FlowRoutines::sync() / unschedule()

  • Dropped the trailing HashGatedRoutineBackend::persist() call in both; nothing else changed.

One-shot cleanup

  • datamachine_run_deferred_site_setup() (already runs on every DATAMACHINE_VERSION bump via datamachine_maybe_ensure_current_schema()) now also delete_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 the HashGatedRoutineBackend import, reset_state() call, and the stale datamachine_routine_schedule_hashes teardown delete_option(). The idempotence test (test_sync_is_idempotent_and_does_not_reset_timers) is unchanged — it now exercises the substrate's own idempotent register() instead of the decorator's fingerprint cache, and still passes.
  • tests/flow-routines-reconcile-lock-before-boot-smoke.php: removed the fake HashGatedRoutineBackend namespace stub and every persist()/set_verification_mode()/$persist_calls reference. 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.md updated 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 pass
  • php tests/flow-schedule-reconciliation-worker-smoke.php (unaffected, no HashGated references) — all assertions pass
  • php -l on every touched PHP file — no syntax errors
  • grep -rn HashGated inc tests docs — no matches
  • homeboy 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: 0

Note: this repo has no phpcs.xml/phpcs.xml.dist checked in — the WPCS ruleset is supplied by homeboy's managed environment (see homeboy.json's wordpress extension), not present in a bare git checkout. php vendor/bin/phpcs against 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) plus php -l for local lint/syntax verification instead of a bare composer lint invocation. The PHPUnit path for FlowRoutinesTest.php (WP_UnitTestCase) needs a bootstrapped WordPress test environment that this bare worktree doesn't have locally — CI's Homeboy Test workflow runs the full MySQL suite against this PR.

Do-not constraints respected

…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
@chubes4
chubes4 merged commit 6c4f39d into main Sep 12, 2026
22 checks passed
@chubes4
chubes4 deleted the refactor/remove-hash-gated-routine-backend branch September 12, 2026 21:41
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.

Remove HashGatedRoutineBackend now that the agents-api routine bridge is idempotent

1 participant