Skip to content

Commit 333511b

Browse files
authored
Add deterministic GUID generation (#176)
1 parent 6395485 commit 333511b

File tree

5 files changed

+117
-4
lines changed

5 files changed

+117
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## placeholder
22
* Fix exception type issue when using `RetriableTask` in fan in/out pattern ([#174](https://github.com/microsoft/durabletask-java/pull/174))
3+
* Add implementation to generate name-based deterministic UUID ([#175](https://github.com/microsoft/durabletask-java/pull/175))
34

45

56
## v1.4.0

client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.time.ZonedDateTime;
99
import java.util.Arrays;
1010
import java.util.List;
11+
import java.util.UUID;
1112

1213
/**
1314
* Used by orchestrators to perform actions such as scheduling tasks, durable timers, waiting for external events,
@@ -301,6 +302,20 @@ default void continueAsNew(Object input){
301302
this.continueAsNew(input, true);
302303
}
303304

305+
/**
306+
* Create a new UUID that is safe for replay within an orchestration or operation.
307+
* <p>
308+
* The default implementation of this method creates a name-based UUID
309+
* using the algorithm from RFC 4122 §4.3. The name input used to generate
310+
* this value is a combination of the orchestration instance ID and an
311+
* internally managed sequence number.
312+
*</p>
313+
* @return a deterministic UUID
314+
*/
315+
default UUID newUUID() {
316+
throw new RuntimeException("No implementation found.");
317+
}
318+
304319
/**
305320
* Restarts the orchestration with a new input and clears its history.
306321
* <p>

client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.microsoft.durabletask.interruption.OrchestratorBlockedException;
99
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.*;
1010
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.ScheduleTaskAction.Builder;
11+
import com.microsoft.durabletask.util.UUIDGenerator;
1112

1213
import javax.annotation.Nullable;
1314
import java.time.Duration;
@@ -80,6 +81,7 @@ private class ContextImplTask implements TaskOrchestrationContext {
8081
private boolean isComplete;
8182
private boolean isSuspended;
8283
private boolean isReplaying = true;
84+
private int newUUIDCounter;
8385

8486
// LinkedHashMap to maintain insertion order when returning the list of pending actions
8587
private final LinkedHashMap<Integer, OrchestratorAction> pendingActions = new LinkedHashMap<>();
@@ -294,6 +296,7 @@ public <V> Task<V> callActivity(
294296
return this.createAppropriateTask(taskFactory, options);
295297
}
296298

299+
@Override
297300
public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {
298301
Helpers.throwIfOrchestratorComplete(this.isComplete);
299302

@@ -309,6 +312,20 @@ public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {
309312
"The orchestrator invoked continueAsNew. This Throwable should never be caught by user code.");
310313
}
311314

315+
@Override
316+
public UUID newUUID() {
317+
final int version = 5;
318+
final String hashV5 = "SHA-1";
319+
final String dnsNameSpace = "9e952958-5e33-4daf-827f-2fa12937b875";
320+
final String name = new StringBuilder(this.instanceId)
321+
.append("-")
322+
.append(this.currentInstant)
323+
.append("-")
324+
.append(this.newUUIDCounter).toString();
325+
this.newUUIDCounter++;
326+
return UUIDGenerator.generate(version, hashV5, UUID.fromString(dnsNameSpace), name);
327+
}
328+
312329
@Override
313330
public void sendEvent(String instanceId, String eventName, Object eventData) {
314331
Helpers.throwIfOrchestratorComplete(this.isComplete);
@@ -358,11 +375,8 @@ public <V> Task<V> callSubOrchestrator(
358375
createSubOrchestrationActionBuilder.setInput(StringValue.of(serializedInput));
359376
}
360377

361-
// TODO:replace this with a deterministic GUID generation so that it's safe for replay,
362-
// please find potential bug here https://github.com/microsoft/durabletask-dotnet/issues/9
363-
364378
if (instanceId == null) {
365-
instanceId = UUID.randomUUID().toString();
379+
instanceId = this.newUUID().toString();
366380
}
367381
createSubOrchestrationActionBuilder.setInstanceId(instanceId);
368382

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.microsoft.durabletask.util;
2+
3+
import java.nio.ByteBuffer;
4+
import java.nio.charset.StandardCharsets;
5+
import java.security.MessageDigest;
6+
import java.security.NoSuchAlgorithmException;
7+
import java.util.UUID;
8+
9+
public class UUIDGenerator {
10+
public static UUID generate(int version, String algorithm, UUID namespace, String name) {
11+
12+
MessageDigest hasher = hasher(algorithm);
13+
14+
if (namespace != null) {
15+
ByteBuffer ns = ByteBuffer.allocate(16);
16+
ns.putLong(namespace.getMostSignificantBits());
17+
ns.putLong(namespace.getLeastSignificantBits());
18+
hasher.update(ns.array());
19+
}
20+
21+
hasher.update(name.getBytes(StandardCharsets.UTF_8));
22+
ByteBuffer hash = ByteBuffer.wrap(hasher.digest());
23+
24+
final long msb = (hash.getLong() & 0xffffffffffff0fffL) | (version & 0x0f) << 12;
25+
final long lsb = (hash.getLong() & 0x3fffffffffffffffL) | 0x8000000000000000L;
26+
27+
return new UUID(msb, lsb);
28+
}
29+
30+
private static MessageDigest hasher(String algorithm) {
31+
try {
32+
return MessageDigest.getInstance(algorithm);
33+
} catch (NoSuchAlgorithmException e) {
34+
throw new RuntimeException(String.format("%s not supported.", algorithm));
35+
}
36+
}
37+
}

client/src/test/java/com/microsoft/durabletask/IntegrationTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,4 +1484,50 @@ void activityAnyOf() throws IOException, TimeoutException {
14841484
assertTrue(Integer.parseInt(output) >= 0 && Integer.parseInt(output) < activityCount);
14851485
}
14861486
}
1487+
1488+
@Test
1489+
public void newUUIDTest() {
1490+
String orchestratorName = "test-new-uuid";
1491+
String echoActivityName = "Echo";
1492+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
1493+
.addOrchestrator(orchestratorName, ctx -> {
1494+
// Test 1: Ensure two consequiteively created GUIDs are not unique
1495+
UUID currentUUID0 = ctx.newUUID();
1496+
UUID currentUUID1 = ctx.newUUID();
1497+
if (currentUUID0.equals(currentUUID1)) {
1498+
ctx.complete(false);
1499+
}
1500+
1501+
// Test 2: Ensure that the same GUID values are created on each replay
1502+
UUID originalUUID1 = ctx.callActivity(echoActivityName, currentUUID1, UUID.class).await();
1503+
if (!currentUUID1.equals(originalUUID1)) {
1504+
ctx.complete(false);
1505+
}
1506+
1507+
// Test 3: Ensure that the same UUID values are created on each replay even after an await
1508+
UUID currentUUID2 = ctx.newUUID();
1509+
UUID originalUUID2 = ctx.callActivity(echoActivityName, currentUUID2, UUID.class).await();
1510+
if (!currentUUID2.equals(originalUUID2)) {
1511+
ctx.complete(false);
1512+
}
1513+
1514+
// Test 4: Finish confirming that every generated UUID is unique
1515+
if (currentUUID1.equals(currentUUID2)) ctx.complete(false);
1516+
else ctx.complete(true);
1517+
})
1518+
.addActivity(echoActivityName, ctx -> ctx.getInput(UUID.class))
1519+
.buildAndStart();
1520+
DurableTaskClient client = new DurableTaskGrpcClientBuilder().build();
1521+
1522+
try(worker; client) {
1523+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName);
1524+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
1525+
assertNotNull(instance);
1526+
assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus());
1527+
assertTrue(instance.readOutputAs(boolean.class));
1528+
} catch (TimeoutException e) {
1529+
throw new RuntimeException(e);
1530+
}
1531+
1532+
}
14871533
}

0 commit comments

Comments
 (0)