@@ -678,6 +678,30 @@ def to_server_command(
678678 return cmd
679679
680680
681+ def _condition_predicate_fingerprint (predicate : Callable [[], bool ]) -> str :
682+ h = hashlib .sha256 ()
683+ h .update (b"durable-workflow-python.wait-condition.v1\0 " )
684+ h .update (f"{ getattr (predicate , '__module__' , '' )} \0 " .encode ())
685+ h .update (f"{ getattr (predicate , '__qualname__' , '' )} \0 " .encode ())
686+
687+ code = getattr (predicate , "__code__" , None )
688+ if code is None :
689+ h .update (repr (predicate ).encode ())
690+ else :
691+ h .update (repr ((
692+ code .co_argcount ,
693+ code .co_posonlyargcount ,
694+ code .co_kwonlyargcount ,
695+ code .co_code ,
696+ code .co_consts ,
697+ code .co_names ,
698+ code .co_varnames ,
699+ code .co_freevars ,
700+ )).encode ())
701+
702+ return f"sha256:{ h .hexdigest ()} "
703+
704+
681705Command = (
682706 ScheduleActivity | StartTimer | CompleteWorkflow | FailWorkflow
683707 | CompleteUpdate | FailUpdate | ContinueAsNew | RecordSideEffect | StartChildWorkflow
@@ -960,6 +984,7 @@ def wait_condition(
960984 return WaitCondition (
961985 predicate = predicate ,
962986 condition_key = key ,
987+ condition_definition_fingerprint = _condition_predicate_fingerprint (predicate ),
963988 timeout_seconds = timeout_seconds ,
964989 )
965990
@@ -1551,10 +1576,10 @@ def _state(commands: list[Command]) -> _ReplayState:
15511576 # external receivers apply before the generator consumes the resolved_result
15521577 # at the stored index, preserving history interleaving with activities.
15531578 pending_receivers : list [tuple [int , str , str , list [Any ]]] = []
1554- # Ordered list of condition_wait_id strings from ConditionWaitOpened events,
1555- # used by ``WaitCondition`` yields to match against their corresponding
1556- # opened wait in history (Nth yield ↔ Nth opened).
1557- wait_opened_ids : list [str ] = []
1579+ # Ordered ``ConditionWaitOpened`` payloads, used by ``WaitCondition`` yields
1580+ # to match against their corresponding opened wait in history
1581+ # (Nth yield ↔ Nth opened).
1582+ wait_opened : list [dict [ str , Any ] ] = []
15581583 # Map condition_wait_id → resolution: 'satisfied' (from ConditionWaitSatisfied
15591584 # in history, future server-recorded) or 'timed_out' (from a matching
15601585 # condition_timeout TimerFired event).
@@ -1577,7 +1602,7 @@ def _state(commands: list[Command]) -> _ReplayState:
15771602 elif etype == "ConditionWaitOpened" :
15781603 wait_id = payload .get ("condition_wait_id" )
15791604 if isinstance (wait_id , str ) and wait_id :
1580- wait_opened_ids .append (wait_id )
1605+ wait_opened .append (dict ( payload ) )
15811606 elif etype == "ConditionWaitSatisfied" :
15821607 wait_id = payload .get ("condition_wait_id" )
15831608 if isinstance (wait_id , str ) and wait_id :
@@ -1734,8 +1759,35 @@ def _apply_due_receivers() -> None:
17341759 continue
17351760 if isinstance (cmd , WaitCondition ):
17361761 resolution : str | None = None
1737- if wait_yield_count < len (wait_opened_ids ):
1738- resolution = wait_resolutions .get (wait_opened_ids [wait_yield_count ])
1762+ opened : dict [str , Any ] | None = None
1763+ if wait_yield_count < len (wait_opened ):
1764+ opened = wait_opened [wait_yield_count ]
1765+ opened_id = opened .get ("condition_wait_id" )
1766+ if isinstance (opened_id , str ):
1767+ resolution = wait_resolutions .get (opened_id )
1768+ opened_key = opened .get ("condition_key" )
1769+ if isinstance (opened_key , str ) and opened_key != (cmd .condition_key or "" ):
1770+ return _state ([FailWorkflow (
1771+ message = (
1772+ "wait_condition key changed during replay: "
1773+ f"history has { opened_key !r} , workflow yielded "
1774+ f"{ cmd .condition_key !r} "
1775+ ),
1776+ exception_type = "NonDeterministicWaitCondition" ,
1777+ )])
1778+ opened_fingerprint = opened .get ("condition_definition_fingerprint" )
1779+ if (
1780+ isinstance (opened_fingerprint , str )
1781+ and cmd .condition_definition_fingerprint != opened_fingerprint
1782+ ):
1783+ return _state ([FailWorkflow (
1784+ message = (
1785+ "wait_condition predicate fingerprint changed during replay: "
1786+ f"history has { opened_fingerprint !r} , workflow yielded "
1787+ f"{ cmd .condition_definition_fingerprint !r} "
1788+ ),
1789+ exception_type = "NonDeterministicWaitCondition" ,
1790+ )])
17391791 if resolution == "timed_out" :
17401792 next_value = False
17411793 wait_yield_count += 1
0 commit comments