Skip to content

Commit 6db9b80

Browse files
committed
add deterministic tests, use dotnet version for uuid
Signed-off-by: Filinto Duran <[email protected]>
1 parent de68786 commit 6db9b80

File tree

4 files changed

+525
-13
lines changed

4 files changed

+525
-13
lines changed

durabletask/deterministic.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,45 @@ def deterministic_random(instance_id: str, orchestration_time: datetime) -> rand
5252

5353

5454
def 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
6495
class 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."""

examples/components/statestore.yaml

Whitespace-only changes.

0 commit comments

Comments
 (0)