Skip to content

Commit 914a083

Browse files
committed
Merge remote-tracking branch 'origin/main' into andystaples/add-functions-support
# Conflicts: # CHANGELOG.md
2 parents 25f21df + e7b5f15 commit 914a083

14 files changed

Lines changed: 1860 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010
ADDED
1111

1212
- Added a `result` property to `Task` as a convenience alias for `get_result()`.
13-
- Added `OrchestrationContext.parent_instance_id`, which returns the instance
14-
ID of the parent orchestration for a sub-orchestration, or `None` for a
15-
top-level orchestration.
13+
- Added `TaskHubGrpcClient.rewind_orchestration()` to rewind a failed orchestration instance to its last known good state. Failed activity and sub-orchestration results are removed from the history and the orchestration replays from the last successful checkpoint, retrying only the failed work. The in-memory testing backend supports rewind as well.
1614
- Exported `bind_context` and `clear_context` from
1715
`durabletask.extensions.history_export` so hosts that register the export
1816
functions themselves (rather than via `ExportHistoryClient.register_worker`)
1917
can supply the activities' runtime dependencies.
2018

21-
CHANGED
22-
23-
- Changed the default large-payload externalization threshold (`LargePayloadStorageOptions.threshold_bytes`) from 900,000 bytes to 262,144 bytes (256 KiB), matching the .NET SDK default. Behavioral change (not source/binary breaking): payloads larger than 256 KiB are now externalized by default.
24-
2519
FIXED
2620

2721
- Fixed durabletask scheduled tasks (`durabletask.scheduled`) failing under
@@ -44,6 +38,25 @@ FIXED
4438
- `OrchestrationContext.create_timer` now accepts timezone-aware `datetime`
4539
values, normalizing them to UTC instead of raising when compared against the
4640
orchestration's internal clock.
41+
42+
## v1.7.2
43+
44+
FIXED
45+
46+
- Fixed history export failing for orchestrations that completed with an error. Events carrying failure information (orchestration completion, activity failure, sub-orchestration failure, and entity operation failure) were not JSON-serializable, causing the `durabletask.extensions.history_export` module to raise `TypeError: Object of type FailureDetails is not JSON serializable`. `FailureDetails` now serializes to a JSON object with `message`, `error_type`, and `stack_trace` fields.
47+
48+
## v1.7.1
49+
50+
ADDED
51+
52+
- Added `OrchestrationContext.parent_instance_id`, which returns the instance ID of the parent orchestration for a sub-orchestration, or `None` for a top-level orchestration.
53+
54+
CHANGED
55+
56+
- Changed the default large-payload externalization threshold (`LargePayloadStorageOptions.threshold_bytes`) from 900,000 bytes to 262,144 bytes (256 KiB), matching the .NET SDK default. Behavioral change (not source/binary breaking): payloads larger than 256 KiB are now externalized by default.
57+
58+
FIXED
59+
4760
- Fixed `OrchestrationContext.call_entity` not propagating entity operation failures when running under the legacy entity protocol (used by the Azure Functions Durable extension). A failed entity operation now raises `TaskFailedError` in the calling orchestration instead of silently completing, matching the behavior of the current entity protocol and the .NET SDK.
4861
- Fixed `OrchestrationContext.call_entity` returning a double-encoded result under the legacy entity protocol. The entity's return value was left as a raw serialized JSON string (for example, a returned string arrived with extra quotes and dicts/lists arrived as strings); it is now fully deserialized and coerced to the requested `return_type`, matching the current entity protocol.
4962
- Fixed unbounded growth of the internal entity request/lock tracking maps when using entities over the legacy entity protocol. Entries are now released as each response is handled, reducing memory use in long-running orchestrations that call or lock entities.

durabletask-azuremanaged/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
99

10+
- Added `rewind_orchestration()` to `DurableTaskSchedulerClient` and `AsyncDurableTaskSchedulerClient` (inherited from the base clients) to rewind a failed orchestration instance to its last known good state.
11+
12+
## v1.7.2
13+
14+
- Updates base dependency to durabletask v1.7.2.
15+
16+
## v1.7.1
17+
18+
- Updates base dependency to durabletask v1.7.1
19+
1020
## v1.7.0
1121

1222
- Updates base dependency to durabletask v1.7.0.

durabletask-azuremanaged/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
99

1010
[project]
1111
name = "durabletask.azuremanaged"
12-
version = "1.7.0"
12+
version = "1.7.2"
1313
description = "Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler"
1414
keywords = [
1515
"durable",
@@ -26,13 +26,13 @@ requires-python = ">=3.10"
2626
license = {file = "LICENSE"}
2727
readme = "README.md"
2828
dependencies = [
29-
"durabletask>=1.7.0",
29+
"durabletask>=1.7.2",
3030
"azure-identity>=1.19.0"
3131
]
3232

3333
[project.optional-dependencies]
3434
azure-blob-payloads = [
35-
"durabletask[azure-blob-payloads]>=1.7.0"
35+
"durabletask[azure-blob-payloads]>=1.7.2"
3636
]
3737

3838
[project.urls]

durabletask/client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ def SuspendInstance(self, request: pb.SuspendRequest) -> pb.SuspendResponse:
320320
def ResumeInstance(self, request: pb.ResumeRequest) -> pb.ResumeResponse:
321321
...
322322

323+
def RewindInstance(self, request: pb.RewindInstanceRequest) -> pb.RewindInstanceResponse:
324+
...
325+
323326
def RestartInstance(self, request: pb.RestartInstanceRequest) -> pb.RestartInstanceResponse:
324327
...
325328

@@ -381,6 +384,9 @@ async def SuspendInstance(self, request: pb.SuspendRequest) -> pb.SuspendRespons
381384
async def ResumeInstance(self, request: pb.ResumeRequest) -> pb.ResumeResponse:
382385
...
383386

387+
async def RewindInstance(self, request: pb.RewindInstanceRequest) -> pb.RewindInstanceResponse:
388+
...
389+
384390
async def RestartInstance(self, request: pb.RestartInstanceRequest) -> pb.RestartInstanceResponse:
385391
...
386392

@@ -782,6 +788,26 @@ def resume_orchestration(self, instance_id: str) -> None:
782788
self._logger.info(f"Resuming instance '{instance_id}'.")
783789
self._stub.ResumeInstance(req)
784790

791+
def rewind_orchestration(self, instance_id: str, *,
792+
reason: str | None = None) -> None:
793+
"""Rewinds a failed orchestration instance to its last known good state.
794+
795+
Rewind removes failed task and sub-orchestration results from the
796+
orchestration history and replays the orchestration from the last
797+
successful checkpoint. Activities that previously succeeded are
798+
not re-executed; only failed work is retried.
799+
800+
Args:
801+
instance_id: The ID of the orchestration instance to rewind.
802+
reason: An optional reason string describing why the orchestration is being rewound.
803+
"""
804+
req = pb.RewindInstanceRequest(
805+
instanceId=instance_id,
806+
reason=helpers.get_string_value(reason))
807+
808+
self._logger.info(f"Rewinding instance '{instance_id}'.")
809+
self._stub.RewindInstance(req)
810+
785811
def restart_orchestration(self, instance_id: str, *,
786812
restart_with_new_instance_id: bool = False) -> str:
787813
"""Restarts an existing orchestration instance.
@@ -1267,6 +1293,26 @@ async def resume_orchestration(self, instance_id: str) -> None:
12671293
self._logger.info(f"Resuming instance '{instance_id}'.")
12681294
await self._stub.ResumeInstance(req)
12691295

1296+
async def rewind_orchestration(self, instance_id: str, *,
1297+
reason: str | None = None) -> None:
1298+
"""Rewinds a failed orchestration instance to its last known good state.
1299+
1300+
Rewind removes failed task and sub-orchestration results from the
1301+
orchestration history and replays the orchestration from the last
1302+
successful checkpoint. Activities that previously succeeded are
1303+
not re-executed; only failed work is retried.
1304+
1305+
Args:
1306+
instance_id: The ID of the orchestration instance to rewind.
1307+
reason: An optional reason string describing why the orchestration is being rewound.
1308+
"""
1309+
req = pb.RewindInstanceRequest(
1310+
instanceId=instance_id,
1311+
reason=helpers.get_string_value(reason))
1312+
1313+
self._logger.info(f"Rewinding instance '{instance_id}'.")
1314+
await self._stub.RewindInstance(req)
1315+
12701316
async def restart_orchestration(self, instance_id: str, *,
12711317
restart_with_new_instance_id: bool = False) -> str:
12721318
"""Restarts an existing orchestration instance.

durabletask/internal/helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ def new_terminated_event(*, encoded_output: str | None = None) -> pb.HistoryEven
236236
)
237237

238238

239+
def new_execution_completed_event(
240+
status: pb.OrchestrationStatus,
241+
encoded_result: str | None = None,
242+
failure_details: pb.TaskFailureDetails | None = None) -> pb.HistoryEvent:
243+
return pb.HistoryEvent(
244+
eventId=-1,
245+
timestamp=timestamp_pb2.Timestamp(),
246+
executionCompleted=pb.ExecutionCompletedEvent(
247+
orchestrationStatus=status,
248+
result=get_string_value(encoded_result),
249+
failureDetails=failure_details,
250+
)
251+
)
252+
253+
239254
def get_string_value(val: str | None) -> wrappers_pb2.StringValue | None:
240255
if val is None:
241256
return None

durabletask/task.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import math
99
from abc import ABC, abstractmethod
1010
from collections.abc import Callable, Generator, Sequence
11+
from dataclasses import dataclass
1112
from datetime import datetime, timedelta, timezone
1213
from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar, cast, overload
1314

@@ -449,23 +450,11 @@ def isEnabledFor(self, level: int) -> bool:
449450
return self.logger.isEnabledFor(level)
450451

451452

453+
@dataclass(frozen=True)
452454
class FailureDetails:
453-
def __init__(self, message: str, error_type: str, stack_trace: str | None):
454-
self._message = message
455-
self._error_type = error_type
456-
self._stack_trace = stack_trace
457-
458-
@property
459-
def message(self) -> str:
460-
return self._message
461-
462-
@property
463-
def error_type(self) -> str:
464-
return self._error_type
465-
466-
@property
467-
def stack_trace(self) -> str | None:
468-
return self._stack_trace
455+
message: str
456+
error_type: str
457+
stack_trace: str | None
469458

470459

471460
class TaskFailedError(Exception):

durabletask/testing/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,7 @@ The in-memory backend is designed for testing and has some limitations compared
255255
1. **No persistence**: All state is lost when the backend is stopped
256256
2. **No distributed execution**: Runs in a single process
257257
3. **No history streaming**: StreamInstanceHistory is not implemented
258-
4. **No rewind**: RewindInstance is not implemented
259-
5. **No recursive termination**: Recursive termination is not supported
258+
4. **No recursive termination**: Recursive termination is not supported
260259

261260
### Best Practices
262261

0 commit comments

Comments
 (0)