Skip to content

Commit 16f3cf1

Browse files
committed
Merge remote-tracking branch 'origin/main' into andystaples/add-functions-support
# Conflicts: # CHANGELOG.md # durabletask/worker.py
2 parents 60b6195 + a0e3d83 commit 16f3cf1

18 files changed

Lines changed: 706 additions & 82 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@ 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.
16+
17+
CHANGED
18+
19+
- 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.
1320

1421
FIXED
1522

1623
- `OrchestrationContext.create_timer` now accepts timezone-aware `datetime`
1724
values, normalizing them to UTC instead of raising when compared against the
1825
orchestration's internal clock.
26+
- 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.
27+
- 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.
28+
- 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.
29+
- Fixed schedules created with `durabletask.scheduled` not appearing in the Durable Task Scheduler dashboard. The schedule entity state is now persisted in a format compatible with the .NET SDK: the `status` is serialized as its numeric enum value, the `interval` as a .NET `TimeSpan` string, and the `orchestration_input` as a JSON-encoded string, all using .NET property names, so the dashboard can read it without a JSON deserialization error.
1930

2031
## v1.7.0
2132

azure-functions-durable/azure/durable_functions/internal/compat/orchestration_context.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,13 @@ def version(self) -> Optional[str]:
8787
return self._ctx.version
8888

8989
@property
90-
def parent_instance_id(self) -> str:
90+
def parent_instance_id(self) -> Optional[str]:
9191
"""Get the ID of the parent orchestration.
9292
93-
Not available: durabletask does not currently surface the parent
94-
instance ID on the orchestration context.
93+
Returns ``None`` for a top-level orchestration (i.e. one that was not
94+
started as a sub-orchestration).
9595
"""
96-
raise NotImplementedError(
97-
"parent_instance_id is not currently exposed by durabletask.")
96+
return self._ctx.parent_instance_id
9897

9998
@property
10099
def function_context(self) -> Any:

docs/features.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ pip install durabletask[azure-blob-payloads]
285285
#### How it works
286286

287287
1. When the worker or client sends a payload that exceeds the
288-
configured threshold (default 900 KB), the payload is
288+
configured threshold (default 256 KiB), the payload is
289289
compressed (GZip, enabled by default) and uploaded to the
290290
external store.
291291
2. The original payload in the gRPC message is replaced with a
@@ -307,7 +307,7 @@ from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPay
307307
store = BlobPayloadStore(BlobPayloadStoreOptions(
308308
connection_string="DefaultEndpointsProtocol=https;...",
309309
container_name="durabletask-payloads", # default
310-
threshold_bytes=900_000, # default (900 KB)
310+
threshold_bytes=262_144, # default (256 KiB)
311311
max_stored_payload_bytes=10_485_760, # default (10 MB)
312312
enable_compression=True, # default
313313
))
@@ -351,7 +351,7 @@ store = BlobPayloadStore(BlobPayloadStoreOptions(
351351

352352
| Option | Default | Description |
353353
|---|---|---|
354-
| `threshold_bytes` | 900,000 (900 KB) | Payloads larger than this are externalized |
354+
| `threshold_bytes` | 262,144 (256 KiB) | Payloads larger than this are externalized |
355355
| `max_stored_payload_bytes` | 10,485,760 (10 MB) | Maximum size for externalized payloads |
356356
| `enable_compression` | `True` | GZip-compress payloads before uploading |
357357
| `container_name` | `"durabletask-payloads"` | Azure Blob container name |

docs/supported-patterns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ with DurableTaskSchedulerWorker(
217217
state = c.wait_for_orchestration_completion(instance_id, timeout=60)
218218
```
219219

220-
In this example, any payload exceeding the threshold (default 900 KB) is compressed and uploaded to
220+
In this example, any payload exceeding the threshold (default 256 KiB) is compressed and uploaded to
221221
the configured Azure Blob container. When the worker or client reads the message, it downloads and
222222
decompresses the payload automatically.
223223

durabletask/internal/helpers.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import traceback
55
from datetime import datetime, timezone
6+
from typing import Any, cast
67

78
from google.protobuf import timestamp_pb2, wrappers_pb2
89

@@ -136,6 +137,59 @@ def new_failure_details(ex: Exception, _visited: set[int] | None = None) -> pb.T
136137
)
137138

138139

140+
def _failure_details_from_core_dict(fd: dict[str, Any]) -> pb.TaskFailureDetails:
141+
"""Convert a serialized DurableTask.Core ``FailureDetails`` dict to protobuf."""
142+
inner = fd.get("InnerFailure")
143+
stack_trace = fd.get("StackTrace")
144+
return pb.TaskFailureDetails(
145+
errorType=str(fd.get("ErrorType") or ""),
146+
errorMessage=str(fd.get("ErrorMessage") or ""),
147+
stackTrace=get_string_value(str(stack_trace) if stack_trace is not None else None),
148+
innerFailure=_failure_details_from_core_dict(cast(dict[str, Any], inner)) if isinstance(inner, dict) else None,
149+
isNonRetriable=bool(fd.get("IsNonRetriable", False)),
150+
)
151+
152+
153+
def entity_response_failure_details(
154+
response: dict[str, Any],
155+
error_content: Any = None) -> pb.TaskFailureDetails:
156+
"""Build failure details from a failed legacy-protocol entity ``ResponseMessage``.
157+
158+
Call this only for responses that :func:`is_entity_error_response` reports as
159+
failures. In the WebJobs "old protocol" ``ResponseMessage`` (see
160+
``EntityScheduler/ResponseMessage.cs``), a failed operation serializes the
161+
human-readable content into ``result`` while ``exceptionType`` carries only
162+
the exception's type name (a presence marker) -- it is *not* the message.
163+
This mirrors ``azure-functions-durable-python`` / ``-js``, which read the
164+
message from ``result`` and ignore ``exceptionType``'s value.
165+
166+
Parameters
167+
----------
168+
response:
169+
The deserialized ``ResponseMessage`` dict.
170+
error_content:
171+
The already-deserialized ``result`` payload, used as the failure
172+
message. A structured ``failureDetails`` object, if present, takes
173+
precedence (current-protocol shape).
174+
"""
175+
failure_details = response.get("failureDetails")
176+
if isinstance(failure_details, dict):
177+
return _failure_details_from_core_dict(cast(dict[str, Any], failure_details))
178+
error_type = str(response.get("exceptionType") or "")
179+
error_message = "" if error_content is None else str(error_content)
180+
return pb.TaskFailureDetails(errorType=error_type, errorMessage=error_message)
181+
182+
183+
def is_entity_error_response(response: dict[str, Any]) -> bool:
184+
"""Return ``True`` if a legacy-protocol entity ``ResponseMessage`` is a failure.
185+
186+
In the WebJobs "old protocol" a failed operation is marked by the presence of
187+
an ``exceptionType`` field (successful responses omit it). Current-protocol
188+
payloads may instead carry a structured ``failureDetails`` object.
189+
"""
190+
return "exceptionType" in response or isinstance(response.get("failureDetails"), dict)
191+
192+
139193
def new_event_sent_event(event_id: int, instance_id: str, input: str):
140194
return pb.HistoryEvent(
141195
eventId=event_id,

durabletask/payload/store.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ class LargePayloadStorageOptions:
2121
2222
Attributes:
2323
threshold_bytes: Payloads larger than this value (in bytes) will
24-
be externalized to the payload store. Defaults to 900,000
25-
(900 KB), matching the .NET SDK default.
24+
be externalized to the payload store. Defaults to 262,144
25+
(256 KiB), matching the .NET SDK default.
2626
max_stored_payload_bytes: Maximum payload size (in bytes) that
2727
can be stored externally. Payloads exceeding this limit
2828
will cause an error. Defaults to 10,485,760 (10 MB).
2929
enable_compression: When ``True`` (the default), payloads are
3030
GZip-compressed before uploading.
3131
"""
32-
threshold_bytes: int = 900_000
32+
threshold_bytes: int = 262_144
3333
max_stored_payload_bytes: int = 10 * 1024 * 1024 # 10 MB
3434
enable_compression: bool = True
3535

0 commit comments

Comments
 (0)