audit: fix concurrent subgraph loss, fail-closed ownership, drop barrier compat

This commit is contained in:
lda
2026-09-04 10:58:13 +07:00 Verified
parent 79ce0d3eff
commit f155e6651a
11 changed files with 424 additions and 478 deletions
+100
View File
@@ -642,6 +642,106 @@ def test_subgraph_end_returns_to_subgraph_node_then_foreach_owner() -> None:
assert run.state["seen"] == ["a", "b"]
def test_concurrent_subgraph_item_returns_through_owner() -> None:
from wf_core import PreparedSubgraph
child = Workflow(
name="child",
input_schema=SchemaRef(type="object", properties={"value": {}}),
state_schema=StateSchema.from_field_map({"seen": StateField(type="string")}),
output_schema=SchemaRef(type="object", properties={"seen": {}}),
node_defs=[
NodeDef(
name="inner_record",
input_schema=SchemaRef(
type="object", properties={"value": {}}, required=["value"]
),
output_schema=SchemaRef(
type="object", properties={"seen": {}}, required=["seen"]
),
outcomes=["ok"],
)
],
start="inner_record",
nodes=[
NodeUse.model_validate(
{
"id": "inner_record",
"type": "node",
"node": "inner_record",
"input": [{"target": "value", "path": "input.value"}],
"output": [{"source": "seen", "target": "state.seen"}],
}
)
],
edges=[
Edge.model_validate({"from": "inner_record", "outcome": "ok", "to": END})
],
)
foreach = ForeachNode.model_validate(
{
"id": "each",
"type": "foreach",
"over": "state.items",
"as": "item",
"mode": "concurrent",
"concurrent": {"max_active": 2, "max_outstanding": 2},
}
)
workflow = Workflow(
name="foreach_concurrent_subgraph",
input_schema=SchemaRef(type="object", properties={"items": {"type": "array"}}),
state_schema=StateSchema.from_field_map(
{
"items": StateField(type="array"),
"seen": StateField(
type="array", reducer=ReducerRef(name="wf.std.append")
),
}
),
output_schema=SchemaRef(type="object", properties={"seen": {"type": "array"}}),
node_defs=[],
start="each",
nodes=[
foreach,
SubgraphNode.model_validate(
{
"id": "child",
"type": "subgraph",
"workflow": "child.workflow",
"input_schema": {"type": "object", "properties": {"value": {}}},
"output_schema": {"type": "object", "properties": {"seen": {}}},
"input": [{"target": "value", "path": "context.item"}],
"output": [{"source": "seen", "target": "state.seen"}],
"outcomes": ["ok"],
}
),
],
edges=[
Edge.model_validate({"from": "each", "outcome": "loop", "to": "child"}),
Edge.model_validate({"from": "child", "outcome": "ok", "to": "each"}),
Edge.model_validate({"from": "each", "outcome": "done", "to": END}),
],
)
run = execute_workflow(
workflow,
{"items": ["a", "b"]},
{},
subgraphs={
"child.workflow": PreparedSubgraph(
workflow=child,
registry={
"inner_record": lambda payload, _ctx: {"seen": payload["value"]}
},
)
},
)
assert run.status == RunStatus.COMPLETED
assert sorted(run.state["seen"]) == ["a", "b"]
def test_nonlocal_runtime_return_fails_closed_when_validation_is_bypassed() -> None:
run = RunState(
workflow_name="nonlocal",
+116 -172
View File
@@ -5,107 +5,77 @@ import pytest
from wf_core.errors import WorkflowExecutionError
from wf_core.models.reducers import ReducerRef
from wf_core.paths import StatePath
from wf_core.run_state import ExecutionFrame, RunState, RunStatus, StateWrite
from wf_core.run_state import (
ExecutionFrame,
LineageState,
RunState,
RunStatus,
RuntimeScope,
StateWrite,
)
from wf_core.runtime.foreach_state import (
ForeachBarrierState,
ForeachItemOwner,
ItemErrorRecord,
PendingItemResult,
_state_write_from_metadata,
item_frame_owner,
load_foreach_activation,
load_or_begin_foreach_activation,
save_foreach_activation,
)
from wf_core.runtime.lineage import LineageStateView, lineage_writes_for_frame
from wf_core.runtime.ops.state import StatePatch
from wf_core.runtime.lineage import (
LineageStateView,
add_lineage,
append_lineage_writes,
lineage_writes_for_frame,
)
def test_foreach_barrier_state_round_trips_through_frame_metadata() -> None:
def test_foreach_barrier_state_round_trips_through_activation_metadata() -> None:
frame = ExecutionFrame(id="root", kind="root", node_id="each")
barrier = ForeachBarrierState(
next_index=2,
active_frame_ids=("child-1",),
outstanding_frame_ids=("child-1", "child-2"),
pending_results={
1: PendingItemResult(
index=1,
frame_id="child-1",
status="failed",
patch=StatePatch(changes={"state.count": 1}),
error=ItemErrorRecord(
index=1,
frame_id="child-1",
node_id="work",
error_type="ValueError",
message="bad item",
item={"id": "a"},
),
)
},
)
barrier.save_to_frame(frame, "each")
loaded = ForeachBarrierState.from_frame(frame, "each")
assert loaded is not None
assert loaded.next_index == 2
assert loaded.active_frame_ids == ("child-1",)
assert loaded.outstanding_frame_ids == ("child-1", "child-2")
assert loaded.pending_results[1].patch.changes["state.count"] == 1
assert loaded.pending_results[1].error is not None
assert loaded.pending_results[1].error.message == "bad item"
def test_foreach_barrier_state_round_trips_reducer_write_records() -> None:
frame = ExecutionFrame(id="root", kind="root", node_id="each")
barrier = ForeachBarrierState(
next_index=1,
mode="concurrent",
pending_results={
0: PendingItemResult(
index=0,
frame_id="child-0",
status="succeeded",
lineage_id="root/each[0]",
patch=StatePatch(
writes=[
StateWrite(
path=StatePath(("count",)),
incoming_value=3,
visible_value=5,
reducer=ReducerRef(name="wf.std.add"),
)
]
),
)
},
)
barrier.save_to_frame(frame, "each")
loaded = ForeachBarrierState.from_frame(frame, "each")
assert loaded is not None
write = loaded.pending_results[0].patch.writes[0]
assert loaded.pending_results[0].lineage_id == "root/each[0]"
assert write.path == StatePath(("count",))
assert write.incoming_value == 3
assert write.visible_value == 5
assert write.reducer.name == "wf.std.add"
def test_pending_write_metadata_preserves_invalid_reducer_detail() -> None:
with pytest.raises(WorkflowExecutionError, match="mutually exclusive"):
_state_write_from_metadata(
{
"path": {"root": "state", "parts": ["count"]},
"incoming_value": 1,
"visible_value": 1,
"reducer": {
"name": "wf.std.add",
"ref": {"source": "wf.std", "capability_key": "add"},
},
}
activation = load_or_begin_foreach_activation(frame, "each", mode="serial")
activation.barrier.next_index = 2
activation.barrier.start_child("child-1")
activation.barrier.start_child("child-2")
activation.barrier.finish_child("child-2")
activation.barrier.add_failure(
error=ItemErrorRecord(
index=1,
frame_id="child-1",
node_id="work",
error_type="ValueError",
message="bad item",
item={"id": "a"},
)
)
save_foreach_activation(frame, activation)
loaded = load_foreach_activation(frame, "each", activation.id)
assert loaded is not None
assert loaded.barrier.next_index == 2
assert loaded.barrier.active_frame_ids == ("child-1",)
assert loaded.barrier.outstanding_frame_ids == ("child-1",)
assert loaded.barrier.pending_results[1].status == "failed"
assert loaded.barrier.pending_results[1].error is not None
assert loaded.barrier.pending_results[1].error.message == "bad item"
def test_concurrent_success_round_trips_lineage_identity() -> None:
frame = ExecutionFrame(id="root", kind="root", node_id="each")
activation = load_or_begin_foreach_activation(frame, "each", mode="concurrent")
activation.barrier.add_success_patch(
index=0,
frame_id="child-0",
lineage_id="root:each#0[0]",
)
save_foreach_activation(frame, activation)
loaded = load_foreach_activation(frame, "each", activation.id)
assert loaded is not None
assert loaded.barrier.pending_results[0].lineage_id == "root:each#0[0]"
assert loaded.barrier.pending_results[0].status == "succeeded"
def test_lineage_state_view_materializes_visible_values_without_mutating_base() -> None:
@@ -136,7 +106,7 @@ def test_lineage_state_view_materializes_visible_values_without_mutating_base()
assert base_state["nested"]["value"] == "old"
def test_lineage_writes_for_frame_reads_current_foreach_pending_result() -> None:
def test_lineage_writes_for_frame_reads_item_lineage_store() -> None:
parent = ExecutionFrame(id="root", kind="workflow", node_id="each")
activation = load_or_begin_foreach_activation(parent, "each", mode="concurrent")
child_lineage_id = f"{activation.id}[0]"
@@ -160,23 +130,6 @@ def test_lineage_writes_for_frame_reads_current_foreach_pending_result() -> None
assert isinstance(owner, ForeachItemOwner)
assert owner.activation_id == activation.id
assert owner.item_index == 0
patch = StatePatch(
writes=[
StateWrite(
path=StatePath(("count",)),
incoming_value=3,
visible_value=5,
reducer=ReducerRef(name="wf.std.add"),
)
]
)
activation.barrier.pending_results[0] = PendingItemResult(
index=0,
frame_id=child.id,
status="succeeded",
lineage_id=child.lineage_id,
patch=patch,
)
save_foreach_activation(parent, activation)
run = RunState(
workflow_name="lineage",
@@ -185,6 +138,27 @@ def test_lineage_writes_for_frame_reads_current_foreach_pending_result() -> None
state={"count": 2},
frames={parent.id: parent, child.id: child},
)
run.scopes["root"] = RuntimeScope(
id="root",
workflow_name="lineage",
workflow_input={},
committed_state=run.state,
)
run.lineages["root"] = LineageState(id="root", scope_id="root")
add_lineage(run, scope_id="root", lineage_id=child_lineage_id, parent_id="root")
append_lineage_writes(
run,
scope_id="root",
lineage_id=child_lineage_id,
writes=[
StateWrite(
path=StatePath(("count",)),
incoming_value=3,
visible_value=5,
reducer=ReducerRef(name="wf.std.add"),
)
],
)
writes = lineage_writes_for_frame(run, child)
@@ -193,48 +167,16 @@ def test_lineage_writes_for_frame_reads_current_foreach_pending_result() -> None
assert writes[0].visible_value == 5
def test_lineage_writes_for_frame_rejects_missing_compatibility_parent_frame() -> None:
child = ExecutionFrame(
id="missing:each#0:0",
kind="foreach_iteration",
node_id="work",
parent_frame_id="missing",
metadata={
"foreach_node_id": "each",
"activation_id": "missing:each#0",
"loop_index": 0,
"loop_item": "a",
"loop_alias": "item",
},
)
run = RunState(
workflow_name="lineage",
status=RunStatus.PENDING,
workflow_input={},
state={},
frames={child.id: child},
)
def test_load_activation_returns_none_when_missing() -> None:
from wf_core.runtime.foreach_state import close_foreach_activation
with pytest.raises(WorkflowExecutionError, match="missing parent frame"):
lineage_writes_for_frame(run, child)
def test_foreach_barrier_state_returns_none_when_missing() -> None:
frame = ExecutionFrame(id="root", kind="root", node_id="each")
activation = load_or_begin_foreach_activation(frame, "each", mode="serial")
save_foreach_activation(frame, activation)
close_foreach_activation(frame, activation)
assert ForeachBarrierState.from_frame(frame, "each") is None
def test_foreach_barrier_state_rejects_malformed_metadata() -> None:
frame = ExecutionFrame(
id="root",
kind="root",
node_id="each",
metadata={"foreach_barriers": {"each": {"next_index": "bad"}}},
)
with pytest.raises(WorkflowExecutionError, match="next_index"):
ForeachBarrierState.from_frame(frame, "each")
assert load_foreach_activation(frame, "each", activation.id) is None
assert load_foreach_activation(frame, "each", "root:each#99") is None
def test_foreach_barrier_tracks_active_and_outstanding_children() -> None:
@@ -269,39 +211,44 @@ def test_foreach_barrier_rejects_finishing_unknown_child() -> None:
barrier.finish_child("child-0")
def test_foreach_barrier_accumulates_multiple_patches_for_one_item() -> None:
def test_foreach_barrier_success_registration_is_idempotent_for_same_lineage() -> None:
barrier = ForeachBarrierState(mode="concurrent")
patch = StatePatch(changes={"state.count": 1})
second_patch = StatePatch(changes={"state.name": "a"})
barrier.add_success_patch(
index=0,
frame_id="child-0",
patch=patch,
lineage_id="root/each[0]",
lineage_id="root:each#0[0]",
)
barrier.add_success_patch(
index=0,
frame_id="child-0",
patch=second_patch,
lineage_id="root/each[0]",
lineage_id="root:each#0[0]",
)
result = barrier.pending_results[0]
assert result.lineage_id == "root/each[0]"
assert result.patch.changes["state.count"] == 1
assert result.patch.changes["state.name"] == "a"
assert len(result.patch.writes) == 2
assert result.status == "succeeded"
assert result.lineage_id == "root:each#0[0]"
def test_foreach_barrier_rejects_success_lineage_mismatch() -> None:
barrier = ForeachBarrierState(mode="concurrent")
barrier.add_success_patch(index=0, frame_id="child-0", lineage_id="root:each#0[0]")
with pytest.raises(WorkflowExecutionError, match="belongs to lineage"):
barrier.add_success_patch(
index=0, frame_id="child-0", lineage_id="root:each#0[1]"
)
def test_foreach_barrier_rejects_item_result_frame_mismatch() -> None:
barrier = ForeachBarrierState(mode="concurrent")
patch = StatePatch(changes={"state.count": 1})
barrier.add_success_patch(index=0, frame_id="child-0", patch=patch)
barrier.add_success_patch(index=0, frame_id="child-0", lineage_id="root:each#0[0]")
with pytest.raises(WorkflowExecutionError, match="belongs to frame"):
barrier.add_success_patch(index=0, frame_id="child-1", patch=patch)
barrier.add_success_patch(
index=0, frame_id="child-1", lineage_id="root:each#0[0]"
)
def test_item_error_record_rejects_negative_index() -> None:
@@ -333,16 +280,13 @@ def test_pending_item_result_rejects_negative_index() -> None:
)
def test_save_to_frame_rejects_corrupt_table_without_mutating() -> None:
frame = ExecutionFrame(
id="root",
kind="root",
node_id="each",
metadata={"foreach_barriers": "corrupt"},
)
barrier = ForeachBarrierState(next_index=1)
with pytest.raises(WorkflowExecutionError, match="barrier table"):
barrier.save_to_frame(frame, "each")
assert frame.metadata["foreach_barriers"] == "corrupt"
def test_pending_item_result_requires_lineage_for_success() -> None:
with pytest.raises(WorkflowExecutionError, match="lineage"):
PendingItemResult.from_metadata(
{
"index": 0,
"frame_id": "child",
"status": "succeeded",
"lineage_id": None,
}
)