Skip to content

feat: promote a generic scoped Action Scheduler drain into the workflow substrate#406

Merged
chubes4 merged 1 commit into
mainfrom
feat/scoped-branch-drain
Jul 3, 2026
Merged

feat: promote a generic scoped Action Scheduler drain into the workflow substrate#406
chubes4 merged 1 commit into
mainfrom
feat/scoped-branch-drain

Conversation

@chubes4

@chubes4 chubes4 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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 strand pending/SUSPENDED forever even though they are perfectly runnable — a manual wp action-scheduler run drains 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.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); 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.
  • Pure AS mechanics: \ActionScheduler_Store::instance()->stake_claim() + \ActionScheduler::runner()->process_action(), scoped to 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. 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 smoke green; PHPStan level-max clean (adds proper AS class/function stubs — no ignores).
  • New tests/workflow-scoped-drain-smoke.php drives the real drain against a controllable in-memory AS store.
  • Verified end-to-end on a DISABLE_WP_CRON WP Cloud runtime via a downstream await=true path (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

  • AI assistance: Yes
  • Tool(s): Claude Opus 4.8 (Claude Code)
  • Used for: Extracting the generic scoped-drain core, writing the substrate class + AS PHPStan stubs + the drive-the-real-drain smoke test, and wiring/verification.

…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.
@chubes4 chubes4 merged commit cc236f8 into main Jul 3, 2026
2 checks passed
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.

1 participant