audit: walk serial owners in write routing, harden result identity
This commit is contained in:
@@ -138,16 +138,21 @@ class PendingItemResult:
|
||||
raise WorkflowExecutionError(
|
||||
"malformed pending foreach result lineage id"
|
||||
)
|
||||
error = (
|
||||
ItemErrorRecord.from_metadata(raw_error) if raw_error is not None else None
|
||||
)
|
||||
if error is not None and (error.index != index or error.frame_id != frame_id):
|
||||
raise WorkflowExecutionError(
|
||||
"malformed pending foreach result: error identity "
|
||||
f"(index {error.index!r}, frame {error.frame_id!r}) does not "
|
||||
f"match enclosing result (index {index!r}, frame {frame_id!r})"
|
||||
)
|
||||
return cls(
|
||||
index=index,
|
||||
frame_id=frame_id,
|
||||
status=status,
|
||||
lineage_id=lineage_id,
|
||||
error=(
|
||||
ItemErrorRecord.from_metadata(raw_error)
|
||||
if raw_error is not None
|
||||
else None
|
||||
),
|
||||
error=error,
|
||||
)
|
||||
|
||||
def to_metadata(self) -> dict[str, Any]:
|
||||
@@ -443,10 +448,16 @@ def item_frame_owner(frame: ExecutionFrame) -> ForeachItemOwner | None:
|
||||
"""Return the named foreach ownership record for item frames.
|
||||
|
||||
Malformed item metadata fails closed via ``ForeachIterationMetadata``;
|
||||
only non-item frames return ``None``.
|
||||
only genuinely non-item frames return ``None``. An item frame without
|
||||
a parent is corrupt state and raises rather than masquerading as an
|
||||
ordinary frame.
|
||||
"""
|
||||
if frame.kind != "foreach_iteration" or frame.parent_frame_id is None:
|
||||
if frame.kind != "foreach_iteration":
|
||||
return None
|
||||
if frame.parent_frame_id is None:
|
||||
raise WorkflowExecutionError(
|
||||
f"foreach item frame {frame.id!r} is missing its parent frame"
|
||||
)
|
||||
metadata = ForeachIterationMetadata.from_frame(frame)
|
||||
if metadata is None:
|
||||
return None
|
||||
|
||||
@@ -94,38 +94,47 @@ def commit_foreach_aware_patch(
|
||||
) -> dict[str, Any]:
|
||||
"""Commit one write patch with foreach-aware routing.
|
||||
|
||||
Ordinary frames commit (or buffer) through their own lineage. Serial
|
||||
item writes commit through the parent scope so they land in root state;
|
||||
concurrent item writes stay buffered in the item lineage for the barrier
|
||||
to merge. Malformed ownership, missing parents, and closed or
|
||||
superseded activations fail closed.
|
||||
Ordinary frames commit (or buffer) through their own lineage. The walk
|
||||
climbs through every serial item owner until it reaches either the
|
||||
workflow/subgraph scope root, where it commits, or a concurrent item
|
||||
boundary, where it buffers in that item lineage for the barrier to
|
||||
merge. Malformed ownership, missing parents, parent cycles, and closed
|
||||
or superseded activations fail closed.
|
||||
"""
|
||||
from wf_core.runtime.foreach_state import (
|
||||
item_frame_owner,
|
||||
require_foreach_activation,
|
||||
)
|
||||
|
||||
owner = item_frame_owner(frame)
|
||||
if owner is None:
|
||||
return commit_patch_for_frame(run, frame, patch)
|
||||
parent_frame = run.frames.get(owner.parent_frame_id)
|
||||
if parent_frame is None:
|
||||
raise WorkflowExecutionError(
|
||||
"foreach item state references missing parent frame "
|
||||
f"{owner.parent_frame_id!r} for child frame {frame.id!r}"
|
||||
current = frame
|
||||
seen: set[str] = set()
|
||||
while True:
|
||||
owner = item_frame_owner(current)
|
||||
if owner is None:
|
||||
return commit_patch_for_frame(run, current, patch)
|
||||
if current.id in seen:
|
||||
raise WorkflowExecutionError(
|
||||
f"cycle detected in foreach parent chain at frame {current.id!r}"
|
||||
)
|
||||
seen.add(current.id)
|
||||
parent_frame = run.frames.get(owner.parent_frame_id)
|
||||
if parent_frame is None:
|
||||
raise WorkflowExecutionError(
|
||||
"foreach item state references missing parent frame "
|
||||
f"{owner.parent_frame_id!r} for child frame {current.id!r}"
|
||||
)
|
||||
activation = require_foreach_activation(
|
||||
parent_frame, owner.foreach_node_id, owner.activation_id
|
||||
)
|
||||
activation = require_foreach_activation(
|
||||
parent_frame, owner.foreach_node_id, owner.activation_id
|
||||
)
|
||||
if activation.barrier.mode == "concurrent":
|
||||
append_lineage_writes(
|
||||
run,
|
||||
scope_id=frame.scope_id,
|
||||
lineage_id=frame.lineage_id,
|
||||
writes=patch.writes,
|
||||
)
|
||||
return {}
|
||||
return commit_patch_for_frame(run, parent_frame, patch)
|
||||
if activation.barrier.mode == "concurrent":
|
||||
append_lineage_writes(
|
||||
run,
|
||||
scope_id=current.scope_id,
|
||||
lineage_id=current.lineage_id,
|
||||
writes=patch.writes,
|
||||
)
|
||||
return {}
|
||||
current = parent_frame
|
||||
|
||||
|
||||
def scope_state_for_frame(run: RunState, frame: ExecutionFrame) -> dict[str, Any]:
|
||||
|
||||
@@ -18,7 +18,7 @@ from wf_core.runtime.foreach_state import (
|
||||
)
|
||||
from wf_core.runtime.lineage import (
|
||||
add_lineage,
|
||||
commit_patch_for_frame,
|
||||
commit_foreach_aware_patch,
|
||||
lineage_patch,
|
||||
scope_input_for_frame,
|
||||
)
|
||||
@@ -379,7 +379,7 @@ def _finish_concurrent_foreach(
|
||||
state_view_for_frame(run, frame),
|
||||
reducers=reducers,
|
||||
)
|
||||
state_changes = commit_patch_for_frame(run, frame, combined)
|
||||
state_changes = commit_foreach_aware_patch(run, frame, combined)
|
||||
append_step_result_trace(
|
||||
run,
|
||||
frame_id=frame.id,
|
||||
|
||||
Reference in New Issue
Block a user