Skip to content

Commit d2e2bd5

Browse files
author
Alex Wang
committed
test(insight): cover real fan-in bursts
1 parent d31505f commit d2e2bd5

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
"""End-to-end test for on-change export bursts during parallel fan-in."""
5+
6+
from __future__ import annotations
7+
8+
import threading
9+
import time
10+
from typing import Any
11+
12+
from aws_durable_execution_sdk_python.context import DurableContext
13+
from aws_durable_execution_sdk_python.execution import (
14+
InvocationStatus,
15+
durable_execution,
16+
)
17+
18+
from aws_durable_execution_sdk_python_insight import (
19+
WorkflowInsightConfig,
20+
workflow_insight,
21+
)
22+
from aws_durable_execution_sdk_python_testing.runner import (
23+
DurableFunctionTestResult,
24+
DurableFunctionTestRunner,
25+
)
26+
27+
28+
_BRANCH_COUNT = 6
29+
30+
31+
class _BlockingCaptureExporter:
32+
"""Blocks the first export so the real hook burst queues deterministically."""
33+
34+
def __init__(self) -> None:
35+
self.max_record_size_bytes: int | None = None
36+
self.started = threading.Event()
37+
self.release = threading.Event()
38+
self.records: list[dict[str, Any]] = []
39+
self._lock = threading.Lock()
40+
41+
def render(self, record: dict[str, Any]) -> dict[str, Any]:
42+
return record
43+
44+
def export(self, record: dict[str, Any]) -> None:
45+
if not self.started.is_set():
46+
self.started.set()
47+
self.release.wait(10.0)
48+
with self._lock:
49+
self.records.append(record)
50+
51+
def flush(self) -> None:
52+
return None
53+
54+
def snapshots(self) -> list[dict[str, Any]]:
55+
with self._lock:
56+
return list(self.records)
57+
58+
59+
def _branch(index: int):
60+
def run(context: DurableContext) -> int:
61+
return context.step(lambda _step_context: index, name=f"step-{index}")
62+
63+
return run
64+
65+
66+
def _parallel_handler(event: Any, context: DurableContext) -> list[int]: # noqa: ARG001
67+
return context.parallel(
68+
[_branch(index) for index in range(_BRANCH_COUNT)],
69+
name="fan-in",
70+
).get_results()
71+
72+
73+
def _wait_until(predicate, timeout: float = 10.0) -> bool:
74+
deadline = time.monotonic() + timeout
75+
while time.monotonic() < deadline:
76+
if predicate():
77+
return True
78+
time.sleep(0.005)
79+
return predicate()
80+
81+
82+
def test_parallel_fan_in_preserves_every_on_change_snapshot() -> None:
83+
capture = _BlockingCaptureExporter()
84+
plugin = workflow_insight(
85+
WorkflowInsightConfig(
86+
exporters=[capture],
87+
emit_mode="on-change",
88+
operation_detail="full-tree",
89+
)
90+
)
91+
handler = durable_execution(_parallel_handler, plugins=[plugin])
92+
results: list[DurableFunctionTestResult] = []
93+
94+
with DurableFunctionTestRunner(handler=handler, execution_timeout=15) as runner:
95+
run_thread = threading.Thread(
96+
target=lambda: results.append(runner.run(input="{}")),
97+
daemon=True,
98+
)
99+
run_thread.start()
100+
try:
101+
assert capture.started.wait(5.0)
102+
lane = plugin._scheduler._lanes[0]
103+
# Invocation start is in flight. Three real PluginExecutor changes
104+
# and the terminal record must queue behind it before release.
105+
assert _wait_until(lambda: lane._pending_record_count() == 4)
106+
finally:
107+
capture.release.set()
108+
run_thread.join(10.0)
109+
110+
assert not run_thread.is_alive()
111+
assert len(results) == 1
112+
assert results[0].status is InvocationStatus.SUCCEEDED
113+
114+
records = capture.snapshots()
115+
assert [record["status"] for record in records] == [
116+
"RUNNING",
117+
"RUNNING",
118+
"RUNNING",
119+
"RUNNING",
120+
"SUCCEEDED",
121+
]
122+
final_names = {operation["name"] for operation in records[-1]["operations"]}
123+
assert "fan-in" in final_names
124+
assert {f"step-{index}" for index in range(_BRANCH_COUNT)} <= final_names

0 commit comments

Comments
 (0)