feat: promote a generic scoped Action Scheduler drain into the workflow substrate#406
Merged
Conversation
…ow substrate Add WP_Agent_Workflow_Scoped_Drain: a generic, product-free primitive that synchronously drains a caller-scoped set of Action Scheduler actions in-process until the scope is empty, a budget is exhausted, or a terminal-status callback signals the run is done. This is the foreground pump that completes a parallel-branch fan-out on ANY runtime, whether or not WP-Cron fires. The branch executor dispatches each branch as a claimed AS async action and relies on an EXTERNAL pump (WP-Cron, a cron heartbeat) to claim and run them. On a managed host that sets DISABLE_WP_CRON, or a core/CLI context with no heartbeat, the branches enqueue and strand SUSPENDED forever even though they are runnable. This service is that pump: a CLI, a wp-cron.php request, or an await=true foreground poll calls drain() to complete its own fan-out without depending on WP-Cron. Design: - Scope defaults to the branch executor's own BRANCH_HOOK + RESUME_HOOK and GROUP (read from the executor constants, never hardcoded), and a caller may narrow/widen hooks + group. - Budgets: batch_size, wall-clock time_limit_ms, stop_before_timeout_ms, and a memory soft-limit tunable via the generic agents_workflow_scoped_drain_memory _soft_limit_ratio filter. - terminal_status_callback stops the loop the instant the run is no longer suspended, so it never wastes batches after completion. - Pure AS mechanics: ActionScheduler_Store::instance()->stake_claim() + ActionScheduler::runner()->process_action(), scoped to the hooks + group. - Self-deadlock guard: refuses to run from a claimed-action or re-entrant context (doing_action(scoped hook) or an in-flight drain), so a branch worker can never drain its own siblings and re-introduce the self-deadlock that dispatch-and-return fixed. No-ops cleanly when AS is unavailable. Carries no product/domain coupling — generic hook+group scope, agents_-prefixed filters, caller-supplied terminal condition. Adds PHPStan class stubs for the optional Action Scheduler surface the drain touches (store, runner, claim, queue cleaner) plus as_get_scheduled_actions / as_get_datetime_object function stubs, keeping level-max clean without ignores. Adds tests/workflow-scoped-drain-smoke.php: drives the REAL drain against a controllable in-memory AS store — seeds N pending branch actions with nothing pumping the queue, drains them all to complete, asserts stats + terminal-callback early stop + the self-deadlock refusal + scope isolation. This promotes a proven scoped-drain pattern up into the substrate so every branch-executor caller can share one generic mechanism.
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.
What
Adds
WP_Agent_Workflow_Scoped_Drain— a generic, product-free primitive that synchronously drains a caller-scoped set of Action Scheduler actions in-process until the scope is empty, a budget is exhausted, or a terminal-status callback signals the run is done.Why
The Action Scheduler branch executor dispatches each parallel branch as a claimed AS async action and relies on something external — WP-Cron or a runtime cron heartbeat — to claim and run them. On a managed host that sets
DISABLE_WP_CRON(verified live on a WP Cloud runtime), or a core/CLI context with no heartbeat, the branches enqueue and strandpending/SUSPENDED forever even though they are perfectly runnable — a manualwp action-scheduler rundrains them, proving the branches can run; they just need something to actively pump the queue.This service is that pump, in-process. A foreground orchestrator (a CLI, a
wp-cron.phprequest, or anawait=trueforeground poll) callsdrain()to complete its own fan-out without depending on WP-Cron.Design
BRANCH_HOOK+RESUME_HOOKandGROUP(read from the executor constants, never hardcoded); a caller may narrow/widen hooks + group.batch_size, wall-clocktime_limit_ms,stop_before_timeout_ms, and a memory soft-limit tunable via the genericagents_workflow_scoped_drain_memory_soft_limit_ratiofilter.terminal_status_callbackstops the loop the instant the run is no longer suspended.\ActionScheduler_Store::instance()->stake_claim()+\ActionScheduler::runner()->process_action(), scoped to hooks + group.doing_action(scoped hook)or an in-flight drain), so a branch worker can never drain its own siblings. No-ops cleanly when AS is unavailable.Carries no product/domain coupling. Promotes a proven scoped-drain pattern up into the substrate so every branch-executor caller can share one generic mechanism.
Proof
composer smokegreen; PHPStan level-max clean (adds proper AS class/function stubs — no ignores).tests/workflow-scoped-drain-smoke.phpdrives the real drain against a controllable in-memory AS store.DISABLE_WP_CRONWP Cloud runtime via a downstreamawait=truepath (separate downstream PR).Follow-up
A prior implementation proved this scoped-drain pattern in a downstream plugin; that plugin could later adopt this promoted primitive and delete its own copy (not in this PR).
AI assistance