Skip to content

Commit d88192c

Browse files
DeanChensjcopybara-github
authored andcommitted
fix(workflow): Prevent replay divergence hang in sequence barrier
The wait method in ReplaySequenceBarrier silently blocked forever when the workflow replayed with divergent history. Added a 15-second timeout to loudly raise a RuntimeError instead. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 930765155
1 parent a5a3f2e commit d88192c

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

src/google/adk/workflow/utils/_replay_sequence_barrier.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
class ReplaySequenceBarrier:
2323
"""Unified chronological sequence barrier to ensure deterministic replay ordering."""
2424

25-
def __init__(self, sequence: list[str]) -> None:
25+
def __init__(self, sequence: list[str], timeout_sec: float = 15.0) -> None:
2626
self.sequence = sequence
27+
self.timeout_sec = timeout_sec
2728
self.current_index = 0
2829
self.events = {key: asyncio.Event() for key in sequence}
2930
if sequence:
@@ -37,7 +38,15 @@ async def wait(self, key: str) -> None:
3738
output are not in the sequence barrier, so they fast-forward immediately.
3839
"""
3940
if key in self.events:
40-
await self.events[key].wait()
41+
try:
42+
await asyncio.wait_for(
43+
self.events[key].wait(), timeout=self.timeout_sec
44+
)
45+
except asyncio.TimeoutError:
46+
raise RuntimeError(
47+
"Replay divergence detected: Timed out waiting for sequence key"
48+
f" '{key}' to be unblocked."
49+
)
4150

4251
def check_and_advance(self, key: str) -> None:
4352
"""Advance the sequence if the key matches the current expected execution."""

tests/unittests/workflow/utils/test_replay_sequence_barrier.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,15 @@ async def test_barrier_wait_non_existent_key():
9494

9595
# No blocks, successfully completes!
9696
assert True
97+
98+
99+
@pytest.mark.asyncio
100+
async def test_barrier_wait_timeout_on_divergence():
101+
"""Verifies that waiting on a blocked key raises RuntimeError due to timeout."""
102+
# We use a short sequence where NodeB is never unblocked
103+
sequence = ['NodeA@1', 'NodeB@1']
104+
# Use a fast timeout to keep the test rapid without mocking standard library functions
105+
barrier = ReplaySequenceBarrier(sequence, timeout_sec=0.01)
106+
107+
with pytest.raises(RuntimeError, match='Replay divergence detected'):
108+
await barrier.wait('NodeB@1')

0 commit comments

Comments
 (0)