refactor(scheduling): converge flow and system-task scheduling on Agents API Routines - #3475
Merged
Merged
Conversation
…nts API Routines Implements #3458. Deletes the bespoke RecurringScheduler stack and registers every scheduled flow (flow-<id>) and built-in system schedule (system-<schedule_id>) as an Agents API Routine whose wake executes datamachine/run-flow or datamachine/dispatch-system-task directly. - FlowRoutines adapter: sync/unschedule/pause/resume/next_run(s)/boot/ reconcile + one-shot legacy migration (datamachine_routines_migrated_v1, wp datamachine flows migrate-routines [--dry-run]) - HashGatedRoutineBackend decorator keeps per-request registry re-declaration cheap (interim for Automattic/agents-api#548) - datamachine_run_flow_now stays live for backpressure deferrals; one-time runs move to datamachine_run_flow_once - RecurringScheduleRegistry/RecurringRejectionTracker kept (TaskRegistry and health consumers); RecurringScheduler, ScheduleActionIdentity, GenerationFencedAction, FlowScheduling, FlowScheduleReconciler + lock deleted
- composer.lock: wordpress/agents-api dev-main 6c785b32 → 96806e2 (v0.11.0, routines ability targets + reconcile); CI test/analysis now sees the substrate the new adapter targets - Fix every PHPStan level 7 finding in the touched files (is_array/isset narrowing, optional offset access, dead branches, match arm order, wpdb/AS store argument types) - Drop the now-unneeded method_exists guard from FlowRoutines::available()
…fixables - FlowRoutines: direct typed reads of Registry::reconcile() report keys, drop proven-redundant is_array guards in the migration plan loop, rename the reserved-keyword closure parameter - SystemAbilities: replace the progressively-narrowing match with a message map lookup - DeletePipelineAbility: row-delete failures log-and-continue instead of tracking a provably-dead failure array
… tests - sync(): a bare cron-expression interval now persists as interval=cron + cron_expression, matching the desired-state shape the old scheduler wrote (SchedulingDelegationTest canonical forms) - FlowRoutinesTest: read legacy action status via ActionScheduler_Store (canceled action objects lack get_status()) - AgentBundlerImportTest: as_next_scheduled_action returns false when unscheduled, not null - Remove FlowScheduleReconciliationLockTest with its deleted subject
- FlowRoutines: constant-backed legacy AS group (GroupRegistrar::GROUP), extract register_routine_logged() to deduplicate boot registration - Engine: extract datamachine_dispatch_run_flow() shared by the run_flow_now/run_flow_once bridges and datamachine_log_routine_outcome() shared by the routine observability hooks - SystemAgentServiceProvider: extract schedule_tick()/record_tick_outcome()/ tick_result() from dispatchSchedule() - RunFlowAbility: constant-backed AS group for the backpressure deferral query and reschedule
…args[2]) so a straggler cannot double-fire beside its routine Positional [flow_id] / [flow_id, job_id] wakes from backpressure deferrals and stuck-job recovery still dispatch. Adds a regression test.
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 #3458
Converges Data Machine's recurring scheduling onto Agents API Routines (v0.11.0) and deletes the bespoke scheduler. This is a live migration on ~706 scheduled flows (verified against events.extrachill.com, below): every flow scheduled before the upgrade is scheduled after it, exactly once, with the same interval.
Status: all 18 CI checks green (homeboy review lint + full MySQL test suite across 4 shards + audit).
Identity contract
datamachine_run_flow_nowwp_agent_routine_run_scheduled[flow_id, null, {generation}]['routine_id' => 'flow-N']data-machineagents-apiGenerationFencedActiondatamachine_run_flow_nowdatamachine_run_flow_onceFlowScheduleReconciler+ reconciliation lockRegistry::reconcile()(verification-mode gated)RecurringScheduler::ensureScheduleperdatamachine_recurring_<schedule>hooksystem-<schedule_id>routines →datamachine/dispatch-system-taskcalculateStaggerOffset(crc32 of flow id)stagger(crc32 of routine id)hasLogicalCoveragescansas_next_scheduled_actionIdempotence decision (design item 1a-CHECK)
Verified by reading the substrate:
WP_Agent_Routine_Registry::register()fireswp_agent_routine_registeredon every call, and the bridge'sregister()unconditionally doesas_unschedule_all_actions+as_schedule_*+ mints a fresh generation. Naive per-request re-declaration of ~700 flows would therefore thrash Action Scheduler every request.Chosen fix: a backend decorator. DM installs
HashGatedRoutineBackendvia thewp_agent_routine_backendfilter. It implements the backend interface by delegation and no-opsregister()when the routine's schedule fingerprint (trigger + interval/expression + stagger window + ability + input — not label/meta, so flow renames never reset timers) matches a persisted per-routine hash in one option (datamachine_routine_schedule_hashes, autoload off).unregister()/pause()delete the hash entry so boot self-heals; reconciliation runs inside a verification mode that forces pass-through so registry repairs can never be swallowed by the hash gate. First boot after upgrade has no hashes → every register passes through → that is the migration moment.This is a consumer-side cache, not a substrate reimplementation. Upstream issue filed as the permanent fix: Automattic/agents-api#548 ("Registry needs a non-persisting adopt/declare path for consumers with many persisted routines"). Boot cost after first sync: one option read + ~700 in-memory registrations + zero AS writes per request.
Note:
composer.lockmovedwordpress/agents-apifrom6c785b32to96806e2(v0.11.0) so CI and runtime share the substrate version the adapter targets.Migration runbook
The migration also runs itself once on first boot (guarded by
datamachine_routines_migrated_v1), so installs that never run the CLI still converge. The legacydatamachine_run_flow_nowlistener stays live for one release (TODO(3458)): queue-backpressure deferrals and stuck-job recovery still schedule it with positional args, so it remains a real executor; migration only cancels pending actions carrying the generated[flow_id, null, {gen}]shape.Live dry-run verification (read-only, events.extrachill.com)
Exactly 706 pending actions map 1:1 to 706 flows with non-manual intervals; no flow maps to zero or >1 old action; zero orphans. (Matches the interval mix quoted in #3458.)
LOC
58 files changed, +2,701 / −6,352(net −3,651)Deleted:
RecurringScheduler.php(1,436),ScheduleActionIdentity.php,GenerationFencedAction.php,FlowScheduling.php,FlowScheduleReconciler.php(538),FlowScheduleReconciliationLock.php, and 9 test files targeting them.Kept despite the brief's deletion list (non-scheduling users verified):
RecurringScheduleRegistry(TaskRegistry trigger metadata + schedule definitions source),RecurringRejectionTracker(SystemAbilities health signal; now fed bydatamachine/dispatch-system-task).TaskScheduler/TaskRegistry(batch fan-out, #3428) untouched.Risks
time() + stagger_offset(routine_id); first runs after upgrade shift by up tomin(interval, 3600)s. No timestamps carried across (deliberate).datamachine_run_flow_nowremains an execution bridge for backpressure deferrals; a straggler generated action that slips past migration executes once rather than double-firing (the new chain is the sole recurring mechanism, and migration cancels all pending generated actions).FlowRoutines::reconcile()forces boot registration beforeRegistry::reconcile(); an empty registry would classify every pending routine action as an orphan. The deferred-repair marker hook moved fromaction_scheduler_inittoinitfor the same reason.datamachine/run-flowanddatamachine/dispatch-system-taskgate throughPermissionHelper::can_manage(), which already admits the Action Scheduler principal (doing_action('action_scheduler_run_queue')); REST surface unchanged (routine permission filter defaults deny and allow-lists only routines registered this request).Verification
php -lon all changed filesphpcbf --standard=WordPressclean on every changed PHP fileimport-export-portable-flow-settings-smoke(55 assertions),recurring-schedule-registry-contract-smoke,recurring-rejection-escalation-smoke(26 assertions) — all passtests/Unit/Engine/Scheduling/FlowRoutinesTest.phpcovers alias→seconds, sync idempotence (no timer reset), manual→unregister, permission allow/deny, migration dry-run plan shape, and apply-cancels-generated-chains-onlyphpstan-baseline.neonhad no entries for any deleted symbolflow-config-cli-round-trip-smokefails identically onmain(missingAbilityResultstub in the standalone stubs — same chore(lint): eslint.config.js does not extend @wordpress/eslint-plugin recommended, so local eslint predicts nothing CI enforces #3469 class of local-only gap); it passes under the managed runtime in CIAI-generated with Claude Code via Homeboy worktree