Skip to content
Draft
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 @@
## Unreleased

* Add orchestration cloning via `restartInstance(instanceId, RestartInstanceOptions)` with a required target instance ID and optional version override, preserving the existing boolean overload. Uses the dedicated `CloneInstance` RPC without falling back to legacy restart or scheduling APIs on unsupported backends. `getSourceInstanceId()` exposes the immediate source only when the backend supplies it in orchestration tags; the cloning protocol keeps lineage internal.
* Write self-describing v2 blob payload tokens by default while retaining v1 and v2 reads from the configured storage endpoint and container. Require complete token formats for direct downloads and account for URL token length in worker action-size estimates. Upgrade readers before deploying v2 writers; changing storage locations requires explicit migration or a store configured for the original location.
* Add explicit primary-account-wide and additional-account read scopes for Azure Blob payloads while retaining configured-container access by default. Additional accounts require identity authentication and exact HTTPS account service endpoints. Preserve named and nested v1/v2 references and ignore legacy v2 URL query and fragment suffixes.

Expand Down
7 changes: 6 additions & 1 deletion client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ compileTestJava {
}

task downloadProtoFiles {
ext.branch = project.hasProperty('protoBranch') ? project.protoBranch : 'main'
// Includes the dedicated CloneInstance RPC from durabletask-protobuf PR #82.
ext.branch = project.hasProperty('protoBranch') ? project.protoBranch : '0a4f8075e383f6ac783acfd5aee9ecb791334e87'

doLast {
def protoDir = file("${rootProject.projectDir}/internal/durabletask-protobuf/protos")
Expand All @@ -89,6 +90,10 @@ task downloadProtoFiles {

// Get and save the commit hash
def commitHashFile = new File("${rootProject.projectDir}/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH")
if (ext.branch ==~ /[0-9a-fA-F]{40}/) {
commitHashFile.text = ext.branch
return
}
def commitApiUrl = new URL("https://api.github.com/repos/microsoft/durabletask-protobuf/commits?path=protos/orchestrator_service.proto&sha=${ext.branch}&per_page=1")
def connection = commitApiUrl.openConnection()
connection.setRequestProperty('Accept', 'application/vnd.github.v3+json')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,28 @@ public List<HistoryEvent> getOrchestrationHistory(String instanceId) {
*/
public abstract String restartInstance(String instanceId, boolean restartWithNewInstanceId);

/**
* Restarts an orchestration from the beginning with its original name and input and a required target ID.
* Execution history is not copied. The target may be the source instance ID; existing-instance conflicts
* are handled by the backend. Source lineage is stored internally by the backend and is not exposed by the
* cloning protocol. {@link TaskOrchestrationContext#getSourceInstanceId()} is available only if the backend
* also supplies the source reference in orchestration tags.
* Requires a backend implementing the {@code CloneInstance} RPC and the requested version behavior.
* Unsupported backends reject the request without creating an orchestration. This method does not fall
* back to the legacy restart or scheduling APIs.
*
* @param instanceId the ID of the source orchestration
* @param options the restart options, including a required nonblank new instance ID and an optional version;
* a {@code null} version preserves the backend's default restart behavior, and an empty
* version selects an unversioned orchestration
* @throws IllegalArgumentException if the source ID is blank, the options are {@code null},
* or the target ID is missing or blank
* @return the ID of the restarted orchestration
*/
public String restartInstance(String instanceId, RestartInstanceOptions options) {
throw new UnsupportedOperationException("Explicit restart targets are not supported by this client.");
}

/**
* Rewinds a failed orchestration instance to the last known good state and replays from there.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,23 @@ public String restartInstance(String instanceId, boolean restartWithNewInstanceI
}
}

@Override
public String restartInstance(String instanceId, RestartInstanceOptions options) {
Helpers.throwIfArgumentNullOrWhiteSpace(instanceId, "instanceId");
Helpers.throwIfArgumentNull(options, "options");
String newInstanceId = options.getNewInstanceId();
Helpers.throwIfArgumentNullOrWhiteSpace(newInstanceId, "newInstanceId");
String version = options.getVersion();

CloneInstanceRequest.Builder request = CloneInstanceRequest.newBuilder()
.setSourceInstanceId(instanceId)
.setNewInstanceId(newInstanceId);
if (version != null) {
request.setVersion(StringValue.of(version));
}
return this.sidecarClient.cloneInstance(request.build()).getInstanceId();
}

private PurgeResult toPurgeResult(PurgeInstancesResponse response){
return new PurgeResult(response.getDeletedInstanceCount());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.durabletask;

import javax.annotation.Nullable;

/**
* Options for restarting an orchestration with a required target instance ID and an optional version.
*/
public final class RestartInstanceOptions {
private String newInstanceId;
private String version;

/**
* Creates a new {@link RestartInstanceOptions}. A target instance ID must be set before restarting.
*/
public RestartInstanceOptions() {
}

/**
* Sets the required target instance ID.
*
* @param newInstanceId the nonblank ID of the restarted orchestration
* @return this {@link RestartInstanceOptions} object
*/
public RestartInstanceOptions setNewInstanceId(String newInstanceId) {
this.newInstanceId = newInstanceId;
return this;
}

/**
* Gets the target instance ID.
*
* @return the target instance ID, or {@code null} if none has been set
*/
@Nullable
public String getNewInstanceId() {
return this.newInstanceId;
}

/**
* Sets the version override. A {@code null} value preserves the backend's default restart behavior;
* an empty string explicitly selects an unversioned orchestration.
*
* @param version the version override
* @return this {@link RestartInstanceOptions} object
*/
public RestartInstanceOptions setVersion(@Nullable String version) {
this.version = version;
return this;
}

/**
* Gets the version override.
*
* @return the version override, or {@code null} to preserve the backend's default restart behavior
*/
@Nullable
public String getVersion() {
return this.version;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ default Logger createReplaySafeLogger(Logger logger) {
*/
String getVersion();

/**
* Gets the immediate source instance ID when this orchestration was created as a clone.
* The backend must supply this reference in orchestration tags; the cloning protocol does not expose it.
* When supplied, it is available during replay and is not inherited by sub-orchestrations.
*
* @return the source instance ID, or {@code null} if this is not a clone or the backend does not expose it in tags
*/
@Nullable
default String getSourceInstanceId() {
return null;
}

/**
* Returns a new {@code Task} that is completed when all tasks in {@code tasks} completes.
* See {@link #allOf(Task[])} for more detailed information.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ private class ContextImplTask implements TaskOrchestrationContext {
private String orchestratorName;
private String rawInput;
private String instanceId;
private String sourceInstanceId;
private Instant currentInstant;
private boolean isComplete;
private boolean isSuspended;
Expand Down Expand Up @@ -208,6 +209,12 @@ public String getInstanceId() {
return this.instanceId;
}

@Override
@Nullable
public String getSourceInstanceId() {
return this.sourceInstanceId;
}

@Override
@Nullable
public ParentOrchestrationInstance getParentInstance() {
Expand Down Expand Up @@ -1707,6 +1714,9 @@ private void processEvent(HistoryEvent e) {
this.setInput(input);
String version = startedEvent.getVersion().getValue();
this.setVersion(version);
String tagSourceInstanceId = startedEvent.getTagsMap().get("MS_DurableFunctions_SourceInstanceId");
this.sourceInstanceId = !startedEvent.hasParentInstance() && !Helpers.isNullOrEmpty(tagSourceInstanceId)
? tagSourceInstanceId : null;
if (startedEvent.hasParentTraceContext()) {
this.parentTraceContext = startedEvent.getParentTraceContext();
} else {
Expand Down
Loading
Loading