concurrent foreach preparation, types, validation, refactors

This commit is contained in:
lda
2026-05-22 12:11:51 +07:00 Verified
parent afafe40109
commit d8d5770c9f
17 changed files with 712 additions and 15 deletions
+6 -5
View File
@@ -5,6 +5,7 @@ from wf_core.errors import WorkflowExecutionError
from wf_core.models.steps import ForeachNode
from wf_core.models.workflow import Workflow
from wf_core.run_state import ExecutionFrame, FrameStatus, RunState, StepExecutionResult
from wf_core.runtime.foreach_state import ForeachBarrierState
from wf_core.runtime.ops.flow import advance_frame, append_step_result_trace
from wf_core.runtime.ops.frames import frame_context_values
from wf_core.runtime.ops.index import WorkflowIndex
@@ -23,12 +24,11 @@ def step_foreach(
) -> RunState:
if step.mode != "serial":
raise WorkflowExecutionError(
"parallel foreach execution is not implemented yet"
"concurrent foreach execution is not implemented yet"
)
frame = run.current_frame()
progress_map = frame.metadata.setdefault("foreach_progress", {})
progress = progress_map.setdefault(step.id, {"index": 0})
barrier = ForeachBarrierState.from_frame(frame, step.id) or ForeachBarrierState()
iterable = safe_resolve_path(
str(step.over),
@@ -41,7 +41,7 @@ def step_foreach(
f"foreach source {str(step.over)!r} must resolve to a list"
)
loop_index = progress["index"]
loop_index = barrier.next_index
if loop_index >= len(iterable):
outcome = "done"
next_node_id = index.next_node_id(frame.node_id, outcome)
@@ -64,7 +64,8 @@ def step_foreach(
loop_start = index.next_node_id(frame.node_id, "loop")
item = iterable[loop_index]
progress["index"] = loop_index + 1
barrier.next_index = loop_index + 1
barrier.save_to_frame(frame, step.id)
child_id = f"{frame.id}:{step.id}:{loop_index}"
child_metadata = ForeachIterationMetadata(
foreach_node_id=step.id,
+51 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from collections.abc import Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass, field as dataclass_field
from typing import Any
from wf_core.errors import WorkflowExecutionError
@@ -23,6 +24,24 @@ from wf_core.runtime.ops.schemas import validate_payload_against_schema
_MISSING = object()
@dataclass(slots=True)
class StatePatch:
"""Validated state writes produced by one step before commit.
`changes` is the public trace-facing view: the incoming values keyed by
state path. `_prepared_writes` and `_staged_state` are the executor internals
needed to commit reducer-aware values atomically without recomputing the
patch.
"""
changes: dict[str, Any] = dataclass_field(default_factory=dict)
_prepared_writes: dict[StatePath, tuple[list[str], Any]] = dataclass_field(
default_factory=dict,
repr=False,
)
_staged_state: dict[str, Any] = dataclass_field(default_factory=dict, repr=False)
def apply_output_map(
workflow: Workflow,
node: NodeUse,
@@ -58,6 +77,27 @@ def apply_output_bindings(
missing_field_message: str = "node output did not include required field {field}",
) -> dict[str, Any]:
"""Prepare and commit one atomic state patch from canonical output bindings."""
patch = build_output_patch(
workflow,
bindings,
node_output,
state,
reducers=reducers,
missing_field_message=missing_field_message,
)
return commit_state_patch(state, patch)
def build_output_patch(
workflow: Workflow,
bindings: Sequence[OutputBinding],
node_output: Mapping[str, Any],
state: dict[str, Any],
*,
reducers: Mapping[str, ReducerDefinition] | None = None,
missing_field_message: str = "node output did not include required field {field}",
) -> StatePatch:
"""Build and validate one reducer-aware state patch without mutating state."""
if has_overlapping_paths(str(binding.target) for binding in bindings):
raise WorkflowExecutionError(
"mapped state patch has overlapping destination paths"
@@ -91,9 +131,18 @@ def apply_output_bindings(
for _destination_path, (key_path, merged_value) in prepared_patch.items():
safe_set_nested_value(staged_state, key_path, merged_value)
validate_staged_state_patch(staged_state, prepared_patch, state_fields)
return StatePatch(
changes={str(path): value for path, value in resolved_patch.items()},
_prepared_writes=prepared_patch,
_staged_state=staged_state,
)
def commit_state_patch(state: dict[str, Any], patch: StatePatch) -> dict[str, Any]:
"""Commit a prevalidated patch to state and return trace-facing changes."""
state.clear()
state.update(staged_state)
return {str(path): value for path, value in resolved_patch.items()}
state.update(patch._staged_state)
return dict(patch.changes)
def apply_mapped_state(