212 lines
8.5 KiB
Markdown
212 lines
8.5 KiB
Markdown
# Concurrent Foreach Phase 4 Roadmap
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Split concurrent foreach execution into safe, independently testable runtime slices.
|
|
|
|
**Architecture:** `foreach(mode="concurrent")` is the workflow mode. Sync runtime should interleave admitted item frames one node call at a time; async runtime may later run admitted async node handlers simultaneously. All item writes must flow through `StatePatch` and commit at a foreach barrier, not directly from child frames into shared state.
|
|
|
|
**Tech Stack:** Python 3.14, dataclasses, Pydantic v2, pytest, `wf_core.runtime.scheduler`, `wf_core.runtime.foreach_state`, `wf_core.runtime.ops.state`.
|
|
|
|
---
|
|
|
|
## Current State
|
|
|
|
Already implemented:
|
|
|
|
- `ForeachNode.mode` accepts canonical `"concurrent"`.
|
|
- `ForeachConcurrentPolicy` exists with `max_active`, `max_outstanding`, and `interrupt="quiesce"`.
|
|
- Legacy `mode="parallel"` / `parallel={...}` parse into canonical concurrent shape.
|
|
- `ForeachItemErrorPolicy` exists with `fail`, `skip`, and `collect`.
|
|
- `completed_with_errors` is derived for `skip` and `collect`.
|
|
- `collect_to` is validated as a declared array state field.
|
|
- `StatePatch`, `build_output_patch(...)`, and `commit_state_patch(...)` exist.
|
|
- `ForeachBarrierState`, `PendingItemResult`, and `ItemErrorRecord` exist.
|
|
- Serial foreach progress now uses `ForeachBarrierState`.
|
|
- Sync `foreach(mode="concurrent")` runs with fail-only item policy, bounded
|
|
admission, deterministic interleaving, item-local overlays, and barrier
|
|
commits.
|
|
- Multi-step concurrent item bodies are supported for fail-only item policy.
|
|
- Barrier write validation rejects ambiguous sibling writes: same-path sibling
|
|
writes require a `mergeable` reducer, and ancestor/descendant
|
|
sibling writes are rejected.
|
|
- Concurrent `item_error.action="skip"` and `"collect"` are supported.
|
|
- `collect` writes ordered item error records to `collect_to`, writes an empty
|
|
list on clean success, and emits `completed_with_errors` only when failures
|
|
were collected.
|
|
- Async runtime batches ready concurrent-foreach item node handlers so handler
|
|
awaits may overlap up to admitted `max_active` work. State finalization and
|
|
traces still happen sequentially after handler results return.
|
|
|
|
## Non-Goals For Phase 4
|
|
|
|
- Do not implement Fork/Gather graph nodes.
|
|
- Do not turn `JoinNode` into a real barrier.
|
|
- Do not run sync node handlers in threads or processes.
|
|
- Do not add OpenTelemetry.
|
|
- Do not add platform-level source/tool semaphores.
|
|
- Do not add persistent run storage.
|
|
|
|
## Slice 1: Sync Concurrent Foreach, Fail-Only
|
|
|
|
Implement first because it proves the scheduler and frame admission model without async task orchestration or handled item failures.
|
|
|
|
Scope:
|
|
|
|
- `foreach(mode="concurrent", item_error.action="fail")` runs in sync runtime.
|
|
- Parent foreach admits up to `concurrent.max_active` item frames.
|
|
- Scheduler interleaves item frames one step at a time.
|
|
- Child output writes are buffered as per-item `StatePatch` objects.
|
|
- Barrier commits all successful item patches only when every item succeeds.
|
|
- Any item runtime failure fails the whole run.
|
|
- `skip` and `collect` remain runtime-unsupported for concurrent mode.
|
|
|
|
Plan:
|
|
|
|
- See [`2026-05-22-concurrent-foreach-v1-sync-fail-only.md`](2026-05-22-concurrent-foreach-v1-sync-fail-only.md).
|
|
|
|
## Slice 2: Item-Local Overlays
|
|
|
|
Implemented after Slice 1 because fail-only concurrent foreach needed
|
|
lineage-local reads before multi-step item bodies could be supported. Overlays
|
|
let later nodes in one item read earlier buffered writes from the same item
|
|
without exposing those writes to siblings.
|
|
|
|
Scope:
|
|
|
|
- `state_view_for_frame(...)` returns committed parent state plus current item
|
|
overlay for concurrent foreach item frames.
|
|
- Parent `RunState.state` remains unchanged until the barrier commits.
|
|
- Item patches accumulate across multiple nodes in the same item lineage.
|
|
- Multi-step concurrent item bodies are supported.
|
|
- Sibling item overlays remain isolated.
|
|
- Sibling write conflict policy remains deferred to Slice 3.
|
|
|
|
Plan:
|
|
|
|
- See [`2026-05-22-concurrent-foreach-item-overlays.md`](2026-05-22-concurrent-foreach-item-overlays.md).
|
|
|
|
## Slice 3: Barrier Commit Conflict Semantics
|
|
|
|
Implement after Slice 2 so conflict checks operate on real item-local overlays
|
|
and multi-step item patches.
|
|
|
|
Scope:
|
|
|
|
- Detect sibling lineage writes to the same state path.
|
|
- If exactly one lineage writes a destination path, default replace is allowed.
|
|
- If multiple sibling lineages write the same destination path, a declared
|
|
`mergeable` reducer is required.
|
|
- Ancestor/descendant writes across sibling lineages are conflicts unless an explicit future merge strategy covers them.
|
|
- Commit order is item index order, never completion order.
|
|
|
|
Files likely touched:
|
|
|
|
- `src/wf_core/runtime/foreach_state.py`
|
|
- `src/wf_core/runtime/ops/state.py`
|
|
- `src/wf_core/runtime/ops/foreach.py`
|
|
- `tests/core/test_concurrent_foreach.py`
|
|
|
|
Key tests:
|
|
|
|
- `test_concurrent_foreach_rejects_sibling_writes_without_reducer`
|
|
- `test_concurrent_foreach_applies_reducer_in_item_index_order`
|
|
- `test_concurrent_foreach_rejects_ancestor_descendant_write_conflict`
|
|
|
|
Plan:
|
|
|
|
- See [`2026-05-22-concurrent-foreach-barrier-write-semantics.md`](2026-05-22-concurrent-foreach-barrier-write-semantics.md).
|
|
|
|
## Slice 4: Item Error Policies
|
|
|
|
Implemented after barrier success commits became correct.
|
|
|
|
Scope:
|
|
|
|
- `item_error.action="skip"` continues after item runtime failures.
|
|
- `item_error.action="collect"` continues and writes structured errors to `collect_to`.
|
|
- Both emit `completed_with_errors` if at least one item failed.
|
|
- `collect` writes an empty list and emits `done` if all items succeed.
|
|
- Failed item frames remain `FAILED`; parent foreach decides whether the failure is handled.
|
|
|
|
Files likely touched:
|
|
|
|
- `src/wf_core/runtime/foreach_state.py`
|
|
- `src/wf_core/runtime/ops/foreach.py`
|
|
- `tests/core/test_concurrent_foreach_errors.py`
|
|
|
|
Key tests:
|
|
|
|
- `test_concurrent_foreach_skip_emits_completed_with_errors`
|
|
- `test_concurrent_foreach_collect_writes_ordered_error_records`
|
|
- `test_concurrent_foreach_collect_writes_empty_list_on_clean_success`
|
|
|
|
## Slice 5: Async Concurrent Foreach
|
|
|
|
Implemented after sync semantics stabilized.
|
|
|
|
Scope:
|
|
|
|
- Async runtime may have multiple async node handler calls in flight.
|
|
- `concurrent.max_active` caps admitted/running item work.
|
|
- Sync handlers are still called normally; no thread/process executor.
|
|
- Trace remains append-only chronological execution history.
|
|
- Barrier commit order remains item index order.
|
|
|
|
Files likely touched:
|
|
|
|
- `src/wf_core/runtime/engine.py`
|
|
- `src/wf_core/runtime/step.py`
|
|
- `src/wf_core/runtime/ops/nodes.py`
|
|
- `src/wf_core/runtime/ops/foreach.py`
|
|
- `tests/core/test_concurrent_foreach_async.py`
|
|
|
|
Key tests:
|
|
|
|
- `test_async_concurrent_foreach_respects_max_active`
|
|
- `test_async_concurrent_foreach_commits_in_item_index_order`
|
|
|
|
## Slice 6: Interrupt Quiescence
|
|
|
|
Implemented after async execution exists.
|
|
|
|
Scope:
|
|
|
|
- If any concurrent item interrupts, the whole run pauses.
|
|
- No new item frames are admitted after the interrupt.
|
|
- Already-started async node calls drain to pending results.
|
|
- The caller gets control only at a quiescent point.
|
|
- Pending results do not commit until resume/commit policy allows it.
|
|
- Item frames that route into an `InterruptNode` are prioritized before the
|
|
parent foreach can refill capacity.
|
|
- Already-started async handler calls drain at the batch boundary; state
|
|
finalization remains sequential.
|
|
|
|
Files likely touched:
|
|
|
|
- `src/wf_core/runtime/engine.py`
|
|
- `src/wf_core/runtime/preparation.py`
|
|
- `src/wf_core/runtime/ops/interrupts.py`
|
|
- `src/wf_core/runtime/ops/foreach.py`
|
|
- `tests/core/test_concurrent_foreach_interrupts.py`
|
|
|
|
Key tests:
|
|
|
|
- `test_concurrent_foreach_interrupt_returns_before_refill`
|
|
- `test_resume_prioritizes_interrupted_item_frame_before_siblings`
|
|
|
|
## Execution Order
|
|
|
|
1. Sync concurrent foreach, fail-only.
|
|
2. Item-local overlays for multi-step item bodies.
|
|
3. Barrier conflict semantics.
|
|
4. `skip` / `collect` item error policies.
|
|
5. Async concurrent foreach.
|
|
6. Interrupt quiescence.
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: the roadmap covers scheduler admission, barrier commits, reducer conflicts, handled item failures, async handler execution, and interrupt quiescence.
|
|
- Placeholder scan: each slice has scope, likely files, and named tests; concrete code lives in the slice-specific plan.
|
|
- Type consistency: the roadmap uses canonical `concurrent`, `ForeachConcurrentPolicy`, `ForeachBarrierState`, `PendingItemResult`, and `StatePatch`.
|