Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## placeholder
* Fix exception type issue when using `RetriableTask` in fan in/out pattern ([#174](https://github.com/microsoft/durabletask-java/pull/174))
* Add implementation to generate name-based deterministic UUID ([#175](https://github.com/microsoft/durabletask-java/pull/175))


## v1.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

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

/**
* Create a new UUID that is safe for replay within an orchestration or operation.
* <p>
* The default implementation of this method creates a name-based UUID
* using the algorithm from RFC 4122 §4.3. The name input used to generate
* this value is a combination of the orchestration instance ID and an
* internally managed sequence number.
*</p>
* @return a deterministic UUID
*/
default UUID newUUID() {
throw new RuntimeException("No implementation found.");
}

/**
* Restarts the orchestration with a new input and clears its history.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.microsoft.durabletask.interruption.OrchestratorBlockedException;
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.*;
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.ScheduleTaskAction.Builder;
import com.microsoft.durabletask.util.UUIDGenerator;

import javax.annotation.Nullable;
import java.time.Duration;
Expand Down Expand Up @@ -80,6 +81,7 @@ private class ContextImplTask implements TaskOrchestrationContext {
private boolean isComplete;
private boolean isSuspended;
private boolean isReplaying = true;
private int newUUIDCounter;

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

@Override
public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {
Helpers.throwIfOrchestratorComplete(this.isComplete);

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

@Override
public UUID newUUID() {
final int version = 5;
final String hashV5 = "SHA-1";
final String dnsNameSpace = "9e952958-5e33-4daf-827f-2fa12937b875";
final String name = new StringBuilder(this.instanceId)
.append("-")
.append(this.currentInstant)
.append("-")
.append(this.newUUIDCounter).toString();
this.newUUIDCounter++;
return UUIDGenerator.generate(version, hashV5, UUID.fromString(dnsNameSpace), name);
}

@Override
public void sendEvent(String instanceId, String eventName, Object eventData) {
Helpers.throwIfOrchestratorComplete(this.isComplete);
Expand Down Expand Up @@ -358,11 +375,8 @@ public <V> Task<V> callSubOrchestrator(
createSubOrchestrationActionBuilder.setInput(StringValue.of(serializedInput));
}

// TODO:replace this with a deterministic GUID generation so that it's safe for replay,
// please find potential bug here https://github.com/microsoft/durabletask-dotnet/issues/9

if (instanceId == null) {
instanceId = UUID.randomUUID().toString();
instanceId = this.newUUID().toString();
}
createSubOrchestrationActionBuilder.setInstanceId(instanceId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.microsoft.durabletask.util;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;

public class UUIDGenerator {
public static UUID generate(int version, String algorithm, UUID namespace, String name) {

MessageDigest hasher = hasher(algorithm);

if (namespace != null) {
ByteBuffer ns = ByteBuffer.allocate(16);
ns.putLong(namespace.getMostSignificantBits());
ns.putLong(namespace.getLeastSignificantBits());
hasher.update(ns.array());
}

hasher.update(name.getBytes(StandardCharsets.UTF_8));
ByteBuffer hash = ByteBuffer.wrap(hasher.digest());

final long msb = (hash.getLong() & 0xffffffffffff0fffL) | (version & 0x0f) << 12;
final long lsb = (hash.getLong() & 0x3fffffffffffffffL) | 0x8000000000000000L;

return new UUID(msb, lsb);
}

private static MessageDigest hasher(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(String.format("%s not supported.", algorithm));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1484,4 +1484,50 @@ void activityAnyOf() throws IOException, TimeoutException {
assertTrue(Integer.parseInt(output) >= 0 && Integer.parseInt(output) < activityCount);
}
}

@Test
public void newUUIDTest() {
String orchestratorName = "test-new-uuid";
String echoActivityName = "Echo";
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
.addOrchestrator(orchestratorName, ctx -> {
// Test 1: Ensure two consequiteively created GUIDs are not unique
UUID currentUUID0 = ctx.newUUID();
UUID currentUUID1 = ctx.newUUID();
if (currentUUID0.equals(currentUUID1)) {
ctx.complete(false);
}

// Test 2: Ensure that the same GUID values are created on each replay
UUID originalUUID1 = ctx.callActivity(echoActivityName, currentUUID1, UUID.class).await();
if (!currentUUID1.equals(originalUUID1)) {
ctx.complete(false);
}

// Test 3: Ensure that the same UUID values are created on each replay even after an await
UUID currentUUID2 = ctx.newUUID();
UUID originalUUID2 = ctx.callActivity(echoActivityName, currentUUID2, UUID.class).await();
if (!currentUUID2.equals(originalUUID2)) {
ctx.complete(false);
}

// Test 4: Finish confirming that every generated UUID is unique
if (currentUUID1.equals(currentUUID2)) ctx.complete(false);
else ctx.complete(true);
})
.addActivity(echoActivityName, ctx -> ctx.getInput(UUID.class))
.buildAndStart();
DurableTaskClient client = new DurableTaskGrpcClientBuilder().build();

try(worker; client) {
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName);
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
assertNotNull(instance);
assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus());
assertTrue(instance.readOutputAs(boolean.class));
} catch (TimeoutException e) {
throw new RuntimeException(e);
}

}
}