@@ -44,32 +44,55 @@ def __init__(self) -> None:
4444 self ._cond = asyncio .Condition ()
4545 self ._quiescence_event = asyncio .Event ()
4646
47- # Work tracking (prevents premature quiescence before any work)
48- self ._total_committed : int = 0
49- self ._has_started : bool = False
50-
5147 # Force stop flag (for explicit workflow stop)
5248 self ._force_stopped : bool = False
5349
5450 def reset (self ) -> None :
55- """Reset for a new workflow run."""
51+ """
52+ Reset for a new workflow run.
53+
54+ Note: This is a sync reset that replaces primitives. It should only be
55+ called when no coroutines are waiting on the old primitives (e.g., at
56+ the start of a new workflow invocation before any tasks are spawned).
57+ """
5658 self ._active .clear ()
5759 self ._processing_count .clear ()
5860 self ._uncommitted_messages = 0
5961 self ._cond = asyncio .Condition ()
6062 self ._quiescence_event = asyncio .Event ()
61- self ._total_committed = 0
62- self ._has_started = False
6363 self ._force_stopped = False
6464
65+ async def reset_async (self ) -> None :
66+ """
67+ Reset for a new workflow run (async version).
68+
69+ This version properly wakes any waiting coroutines before resetting,
70+ preventing deadlocks if called while the workflow is still running.
71+ """
72+ async with self ._cond :
73+ # Wake all waiters so they can exit gracefully
74+ self ._force_stopped = True
75+ self ._quiescence_event .set ()
76+ self ._cond .notify_all ()
77+
78+ # Give waiters a chance to wake up and exit
79+ await asyncio .sleep (0 )
80+
81+ # Now safe to reset state
82+ async with self ._cond :
83+ self ._active .clear ()
84+ self ._processing_count .clear ()
85+ self ._uncommitted_messages = 0
86+ self ._force_stopped = False
87+ self ._quiescence_event .clear ()
88+
6589 # ─────────────────────────────────────────────────────────────────────────
6690 # Node Lifecycle (called from _invoke_node)
6791 # ─────────────────────────────────────────────────────────────────────────
6892
6993 async def enter (self , node_name : str ) -> None :
7094 """Called when a node begins processing."""
7195 async with self ._cond :
72- self ._has_started = True
7396 self ._quiescence_event .clear ()
7497 self ._active .add (node_name )
7598 self ._processing_count [node_name ] += 1
@@ -94,7 +117,6 @@ async def on_messages_published(self, count: int = 1, source: str = "") -> None:
94117 if count <= 0 :
95118 return
96119 async with self ._cond :
97- self ._has_started = True
98120 self ._quiescence_event .clear ()
99121 self ._uncommitted_messages += count
100122
@@ -112,13 +134,9 @@ async def on_messages_committed(self, count: int = 1, source: str = "") -> None:
112134 return
113135 async with self ._cond :
114136 self ._uncommitted_messages = max (0 , self ._uncommitted_messages - count )
115- self ._total_committed += count
116137 self ._check_quiescence_unlocked ()
117138
118- logger .debug (
119- f"Tracker: { count } messages committed from { source } "
120- f"(uncommitted={ self ._uncommitted_messages } , total={ self ._total_committed } )"
121- )
139+ logger .debug (f"Tracker: { count } messages committed from { source } " )
122140 self ._cond .notify_all ()
123141
124142 # Aliases for clarity
@@ -144,14 +162,9 @@ def _check_quiescence_unlocked(self) -> None:
144162 logger .debug (
145163 f"Tracker: checking quiescence - active={ list (self ._active )} , "
146164 f"uncommitted={ self ._uncommitted_messages } , "
147- f"has_started={ self ._has_started } , "
148- f"total_committed={ self ._total_committed } , "
149165 f"is_quiescent={ is_quiescent } "
150166 )
151167 if is_quiescent :
152- logger .info (
153- f"Tracker: quiescence detected (committed={ self ._total_committed } )"
154- )
155168 self ._quiescence_event .set ()
156169
157170 def _is_quiescent_unlocked (self ) -> bool :
@@ -165,12 +178,12 @@ def _is_quiescent_unlocked(self) -> bool:
165178 - No messages waiting to be committed
166179 - At least some work was done
167180 """
168- return (
169- not self ._active
170- and self ._uncommitted_messages == 0
171- and self ._has_started
172- and self ._total_committed > 0
181+ is_quiescent = not self ._active and self ._uncommitted_messages == 0
182+ logger .debug (
183+ f"Tracker: _is_quiescent_unlocked check - active={ list (self ._active )} , "
184+ f"uncommitted={ self ._uncommitted_messages } , is_quiescent={ is_quiescent } "
173185 )
186+ return is_quiescent
174187
175188 async def is_quiescent (self ) -> bool :
176189 """
@@ -263,6 +276,5 @@ async def get_metrics(self) -> Dict:
263276 return {
264277 "active_nodes" : list (self ._active ),
265278 "uncommitted_messages" : self ._uncommitted_messages ,
266- "total_committed" : self ._total_committed ,
267279 "is_quiescent" : self ._is_quiescent_unlocked (),
268280 }
0 commit comments