@@ -52,14 +52,45 @@ def deterministic_random(instance_id: str, orchestration_time: datetime) -> rand
5252
5353
5454def deterministic_uuid4 (rnd : random .Random ) -> uuid .UUID :
55- """Generate a deterministic UUID4 using the provided random generator."""
55+ """
56+ Generate a deterministic UUID4 using the provided random generator.
57+
58+ Note: This is deprecated in favor of deterministic_uuid_v5 which matches
59+ the .NET implementation for cross-language compatibility.
60+ """
5661 bytes_ = bytes (rnd .randrange (0 , 256 ) for _ in range (16 ))
5762 bytes_list = list (bytes_ )
5863 bytes_list [6 ] = (bytes_list [6 ] & 0x0F ) | 0x40 # Version 4
5964 bytes_list [8 ] = (bytes_list [8 ] & 0x3F ) | 0x80 # Variant bits
6065 return uuid .UUID (bytes = bytes (bytes_list ))
6166
6267
68+ def deterministic_uuid_v5 (instance_id : str , current_datetime : datetime , counter : int ) -> uuid .UUID :
69+ """
70+ Generate a deterministic UUID v5 matching the .NET implementation.
71+
72+ This implementation matches the durabletask-dotnet NewGuid() method:
73+ https://github.com/microsoft/durabletask-dotnet/blob/main/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
74+
75+ Args:
76+ instance_id: The orchestration instance ID.
77+ current_datetime: The current orchestration datetime (frozen during replay).
78+ counter: The per-call counter (starts at 0 on each replay).
79+
80+ Returns:
81+ A deterministic UUID v5 that will be the same across replays.
82+ """
83+ # DNS namespace UUID - same as .NET DnsNamespaceValue
84+ namespace = uuid .UUID ("9e952958-5e33-4daf-827f-2fa12937b875" )
85+
86+ # Build name matching .NET format: instanceId_datetime_counter
87+ # Using isoformat() which produces ISO 8601 format similar to .NET's ToString("o")
88+ name = f"{ instance_id } _{ current_datetime .isoformat ()} _{ counter } "
89+
90+ # Generate UUID v5 (SHA-1 based, matching .NET)
91+ return uuid .uuid5 (namespace , name )
92+
93+
6394@runtime_checkable
6495class DeterministicContextProtocol (Protocol ):
6596 """Protocol for contexts that provide deterministic operations."""
@@ -76,8 +107,18 @@ class DeterministicContextMixin:
76107 Mixin providing deterministic helpers for workflow contexts.
77108
78109 Assumes the inheriting class exposes `instance_id` and `current_utc_datetime` attributes.
110+
111+ This implementation matches the .NET durabletask SDK approach with an explicit
112+ counter for UUID generation that resets on each replay.
79113 """
80114
115+ def __init__ (self , * args , ** kwargs ):
116+ """Initialize the mixin with a UUID counter."""
117+ super ().__init__ (* args , ** kwargs )
118+ # Counter for deterministic UUID generation (matches .NET newGuidCounter)
119+ # This counter resets to 0 on each replay, ensuring determinism
120+ self ._uuid_counter : int = 0
121+
81122 def now (self ) -> datetime :
82123 """Return orchestration time (deterministic UTC)."""
83124 value = self .current_utc_datetime # type: ignore[attr-defined]
@@ -98,9 +139,29 @@ def random(self) -> random.Random:
98139 return rnd
99140
100141 def uuid4 (self ) -> uuid .UUID :
101- """Return a deterministically generated UUID using the deterministic PRNG."""
102- rnd = self .random ()
103- return deterministic_uuid4 (rnd )
142+ """
143+ Return a deterministically generated UUID v5 with explicit counter.
144+ https://www.sohamkamani.com/uuid-versions-explained/#v5-non-random-uuids
145+
146+ This matches the .NET implementation's NewGuid() method which uses:
147+ - Instance ID
148+ - Current UTC datetime (frozen during replay)
149+ - Per-call counter (resets to 0 on each replay)
150+
151+ The counter ensures multiple calls produce different UUIDs while maintaining
152+ determinism across replays.
153+ """
154+ # Lazily initialize counter if not set by __init__ (for compatibility)
155+ if not hasattr (self , "_uuid_counter" ):
156+ self ._uuid_counter = 0
157+
158+ result = deterministic_uuid_v5 (
159+ self .instance_id , # type: ignore[attr-defined]
160+ self .current_utc_datetime , # type: ignore[attr-defined]
161+ self ._uuid_counter ,
162+ )
163+ self ._uuid_counter += 1
164+ return result
104165
105166 def new_guid (self ) -> uuid .UUID :
106167 """Alias for uuid4 for API parity with other SDKs."""
0 commit comments