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
8 changes: 4 additions & 4 deletions .github/workflows/build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ jobs:
- name: Archive test report
uses: actions/upload-artifact@v4
with:
name: Integration test report
path: client/build/reports/tests/endToEndTest
name: E2E test report
path: endtoendtests/build/reports/tests/endToEndTest

functions-sample-tests:

Expand Down Expand Up @@ -195,5 +195,5 @@ jobs:
- name: Archive test report
uses: actions/upload-artifact@v4
with:
name: Integration test report
path: client/build/reports/tests/endToEndTest
name: Functions Sample test report
path: samples-azure-functions/build/reports/tests/sampleTest
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Unreleased

* Adding rewind client API ([#253](https://github.com/microsoft/durabletask-java/pull/253)). Note: orchestration processing for rewind is supported with Azure Functions but not with the standalone `GrpcDurableTaskWorker`.

## v1.7.0
* Add descriptive error when orchestration type is not registered ([#261](https://github.com/microsoft/durabletask-java/pull/261))
* Update all dependencies to latest versions ([#260](https://github.com/microsoft/durabletask-java/pull/260))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class HttpManagementPayload {
private final String id;
private final String purgeHistoryDeleteUri;
private final String restartPostUri;
private final String rewindPostUri;
private final String sendEventPostUri;
private final String statusQueryGetUri;
private final String terminatePostUri;
Expand All @@ -33,6 +34,7 @@ public HttpManagementPayload(
this.id = instanceId;
this.purgeHistoryDeleteUri = instanceStatusURL + "?" + requiredQueryStringParameters;
this.restartPostUri = instanceStatusURL + "/restart?" + requiredQueryStringParameters;
this.rewindPostUri = instanceStatusURL + "/rewind?reason={text}&" + requiredQueryStringParameters;
this.sendEventPostUri = instanceStatusURL + "/raiseEvent/{eventName}?" + requiredQueryStringParameters;
this.statusQueryGetUri = instanceStatusURL + "?" + requiredQueryStringParameters;
this.terminatePostUri = instanceStatusURL + "/terminate?reason={text}&" + requiredQueryStringParameters;
Expand Down Expand Up @@ -94,4 +96,13 @@ public String getRestartPostUri() {
return restartPostUri;
}

/**
* Gets the HTTP POST instance rewind endpoint.
*
* @return The HTTP URL for posting instance rewind commands.
*/
public String getRewindPostUri() {
return rewindPostUri;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,41 @@ public abstract OrchestrationMetadata waitForInstanceCompletion(
*/
public abstract String restartInstance(String instanceId, boolean restartWithNewInstanceId);

/**
* Rewinds a failed orchestration instance to the last known good state and replays from there.
* <p>
* This method can only be used on orchestration instances that are in a <code>Failed</code> state.
* When rewound, the orchestration instance will restart from the point of failure as if the failure
* never occurred. It rewinds the orchestration by replaying any
* Failed Activities and Failed suborchestrations that themselves have Failed Activities
* <p>
* <b>Note:</b> Rewind requires a backend that supports it. When using Azure Functions with the
* Durable Task extension, rewind is fully supported. The standalone {@code GrpcDurableTaskWorker}
* does not currently support orchestration processing for rewind.
Comment on lines +303 to +305
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, what happens if someone calls this with DTS instead of a supported backend? Is it a defined behavior that will lead to the orchestration failing? Or would it lead to a non-deterministic state/stuck orchestration?

If it fails the orchestration, I'm fine just having a note here. If it will lead to stuck orchestrations, I wonder if we can find a way to reject/fail it in the worker?

Copy link
Contributor

@sophiatev sophiatev Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So all backends support rewind (including DTS). This is only an issue if someone is trying to use the GrpcDurableTaskClient as a standalone SDK, so not with Functions, in which case it will attempt to call into the sidecar gRPC implementation in DTS which will throw a not supported exception.

Hence this part of the method comment:

The standalone {@code GrpcDurableTaskWorker} does not currently support orchestration processing for rewind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fails with the following error: io.grpc.StatusRuntimeException: UNIMPLEMENTED: The rewind operation is not yet supported.

I was seeing this error in the CI tests when I added durabletask rewind tests which is how I found out that it wasn't supported. Here's a test run where this was happening: https://github.com/microsoft/durabletask-java/actions/runs/21721638553/job/62652715658

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as it's consistent, I'm OK with the note. The main thing I wanted to avoid was someone accidentally rewinding a valid, in-progress orchestration on an unsupported backend and having it get in a weird stuck state. If that's not the case, I'm good with this :)

*
* @param instanceId the ID of the orchestration instance to rewind
*/
public void rewindInstance(String instanceId) {
this.rewindInstance(instanceId, null);
}

/**
* Rewinds a failed orchestration instance to the last known good state and replays from there.
* <p>
* This method can only be used on orchestration instances that are in a <code>Failed</code> state.
* When rewound, the orchestration instance will restart from the point of failure as if the failure
* never occurred. It rewinds the orchestration by replaying any
* Failed Activities and Failed suborchestrations that themselves have Failed Activities
* <p>
* <b>Note:</b> Rewind requires a backend that supports it. When using Azure Functions with the
* Durable Task extension, rewind is fully supported. The standalone {@code GrpcDurableTaskWorker}
* does not currently support orchestration processing for rewind.
*
* @param instanceId the ID of the orchestration instance to rewind
* @param reason the reason for rewinding the orchestration instance
*/
public abstract void rewindInstance(String instanceId, @Nullable String reason);

/**
* Suspends a running orchestration instance.
* @param instanceId the ID of the orchestration instance to suspend
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,29 @@ public void resumeInstance(String instanceId, @Nullable String reason) {
this.sidecarClient.resumeInstance(resumeRequestBuilder.build());
}

@Override
public void rewindInstance(String instanceId, @Nullable String reason) {
Helpers.throwIfArgumentNull(instanceId, "instanceId");
RewindInstanceRequest.Builder rewindRequestBuilder = RewindInstanceRequest.newBuilder();
rewindRequestBuilder.setInstanceId(instanceId);
if (reason != null) {
rewindRequestBuilder.setReason(StringValue.of(reason));
}
try {
this.sidecarClient.rewindInstance(rewindRequestBuilder.build());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.NOT_FOUND) {
throw new IllegalArgumentException(
"No orchestration instance with ID '" + instanceId + "' was found.", e);
}
if (e.getStatus().getCode() == Status.Code.FAILED_PRECONDITION) {
throw new IllegalStateException(
"Orchestration instance '" + instanceId + "' is not in a failed state and cannot be rewound.", e);
}
throw e;
}
}

@Override
public String restartInstance(String instanceId, boolean restartWithNewInstanceId) {
OrchestrationMetadata metadata = this.getInstanceMetadata(instanceId, true);
Expand Down
Loading
Loading