Skip to content

Commit 3abd6dd

Browse files
author
Bernd Verst
committed
Merge latest main into Python 3.11 migration
Preserve upstream payload preflight, entity identity, dispatch recovery, and task hub filtering improvements alongside the Python 3.11 baseline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6b07f3f-b3e1-49dd-9121-318968272d31
2 parents 4b79790 + 6a56be2 commit 3abd6dd

25 files changed

Lines changed: 2313 additions & 258 deletions

‎.github/workflows/codeql.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767

6868
# Initializes the CodeQL tools for scanning.
6969
- name: Initialize CodeQL
70-
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
70+
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
7171
with:
7272
languages: ${{ matrix.language }}
7373
build-mode: ${{ matrix.build-mode }}
@@ -96,6 +96,6 @@ jobs:
9696
exit 1
9797
9898
- name: Perform CodeQL Analysis
99-
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
99+
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
100100
with:
101101
category: "/language:${{matrix.language}}"

‎.github/workflows/durabletask-azurefunctions.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ jobs:
7878
python-version: "3.13"
7979

8080
- name: Set up Node.js (needed for Azurite and Functions Core Tools)
81-
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
81+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
8282
with:
8383
node-version: "22.x"
8484

@@ -106,7 +106,7 @@ jobs:
106106
# pytest summary does not surface, so upload it for diagnosis.
107107
- name: Upload Functions host logs
108108
if: failure()
109-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
109+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
110110
with:
111111
name: func-host-logs-strict-${{ matrix.strict-typing }}
112112
path: tests/azure-functions-durable/e2e/apps/*/_func_host.log

‎.github/workflows/durabletask.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
python-version: ${{ matrix.python-version }}
4747

4848
- name: Set up Node.js (needed for Azurite)
49-
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
49+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
5050
with:
5151
node-version: '22.x'
5252

‎CHANGELOG.md‎

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

88
## Unreleased
99

10+
ADDED
11+
12+
- Added `OrchestrationQuery.task_hub_names` to filter orchestration instances
13+
by one or more task hubs.
14+
1015
CHANGED
1116

1217
- Dropped Python 3.10 support. Python 3.11 or later is now required.
@@ -18,9 +23,27 @@ failure is still raised directly.
1823

1924
FIXED
2025

21-
- Orchestrator generators are now closed when a dispatch ends, so cleanup in
22-
`finally` blocks runs deterministically, including for async bridges that would
23-
otherwise retain tasks, event loops, and orchestration contexts.
26+
- Implemented `__hash__` and `__repr__` for `EntityInstanceId`, making identity attributes immutable to guarantee hash stability when used in sets or dict keys.
27+
- Large-payload externalization now checks every eligible payload's maximum
28+
size before uploading or replacing any protobuf values, including carryover
29+
events and entity payloads, for both synchronous and asynchronous calls.
30+
Oversized messages remain unchanged and cause no uploads; their size error
31+
now takes precedence over hypothetical earlier storage failures.
32+
- Orchestrations that exceed the configured external payload storage limit now
33+
fail without retrying and release their entity locks, allowing other callers to
34+
progress. Transient payload-store failures remain retryable.
35+
- Previously unhandled non-cancellation `BaseException` exits, including result
36+
serialization and generator teardown failures, now fail with a clear reason instead of leaving an accepted dispatch
37+
unfinished. Cancellation and process-control signals retain their semantics.
38+
The gRPC worker abandons work that exits before an outcome is sent and resets
39+
the originating work-item stream after an uncertain completion or abandonment
40+
RPC, allowing scheduler ownership recovery without retrying an uncertain outcome.
41+
- Orchestrator generators are now closed at the end of each dispatch, including
42+
while waiting for durable work, releasing resources held by async bridges.
43+
`finally` blocks and context-manager exits can run before orchestration completion
44+
and repeat across dispatches; see [cleanup semantics](docs/supported-patterns.md#cleanup-in-orchestrator-finally-blocks).
45+
- Async cancellation raised by generator teardown no longer replaces dispatch
46+
responses or version-rejection abandonment.
2447

2548
## v1.12.0
2649

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,18 @@ directly.
2727

2828
FIXED
2929

30+
- With the corresponding core `durabletask` SDK fix, oversized messages fail
31+
large-payload validation before any uploads or payload replacements. Size
32+
errors now take precedence over hypothetical earlier storage failures.
33+
- With the corresponding core `durabletask` SDK fix, previously unhandled non-cancellation
34+
`BaseException` exits from orchestration execution, result serialization, or teardown now produce a failed response
35+
with the exception's type, message, and stack trace instead of losing the response.
3036
- With the corresponding core `durabletask` SDK fix, orchestrator generators
31-
are closed when a dispatch ends, so cleanup in `finally` blocks runs
32-
deterministically, including for async bridges.
37+
are closed at the end of each dispatch, including while waiting for durable work.
38+
See [cleanup semantics](../docs/supported-patterns.md#cleanup-in-orchestrator-finally-blocks)
39+
for repeated finalizers, discarded teardown actions/status, and yielding restrictions.
40+
- With the corresponding core `durabletask` SDK fix, async cancellation raised by
41+
generator teardown no longer replaces dispatch responses or version-rejection abandonment.
3342

3443
## v2.0.0rc3
3544

‎docs/supported-patterns.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,74 @@ def sequence(ctx: task.OrchestrationContext, _):
2222

2323
See the full [function chaining example](../examples/activity_sequence.py).
2424

25+
## Cleanup in orchestrator finally blocks
26+
27+
The SDK closes an orchestrator generator at the end of every dispatch, including
28+
when it is waiting for durable work. Closing injects `GeneratorExit`, running
29+
active `finally` blocks and context-manager exits. Replay creates a fresh
30+
generator, so this cleanup may run many times before the orchestration completes.
31+
32+
Use these hooks to release dispatch-local resources, not as orchestration-completion
33+
or exactly-once compensation hooks. Do not release resources that an activity
34+
still needs: that activity may still be running when the dispatch ends.
35+
36+
The response is built before this forced teardown. Durable actions, timer
37+
cancellation, custom-status changes, and `continue_as_new()` issued only during
38+
teardown do not change that response. Keep workflow-level cleanup in normal
39+
orchestration control flow, as a yielded activity:
40+
41+
```python
42+
from io import StringIO
43+
from typing import Any, Generator
44+
from durabletask import task
45+
46+
47+
def orchestrator(
48+
ctx: task.OrchestrationContext, value: str,
49+
) -> Generator[task.Task[Any], Any, Any]:
50+
scratch = StringIO(value)
51+
try:
52+
result = yield ctx.call_activity("process", input=scratch.getvalue())
53+
finally:
54+
scratch.close() # Local buffer cleanup runs at each dispatch boundary.
55+
56+
yield ctx.call_activity("cleanup_workflow", input=value)
57+
return result
58+
```
59+
60+
`ctx.is_replaying` is normally false after new events are processed. Neither
61+
`not ctx.is_replaying` nor replay-safe logging makes teardown completion-only.
62+
Yielding during forced close raises `RuntimeError: generator ignored GeneratorExit`;
63+
the SDK logs a warning, but the generator remains open and may retain resources.
64+
A `finally` reached through normal execution, rather than forced close, can
65+
still yield durable work.
66+
67+
Garbage collection could already trigger finalization; completion-only timing
68+
was never guaranteed. Explicit dispatch teardown does not guarantee cleanup
69+
across process failure.
70+
71+
Unexpected non-cancellation `BaseException` exits from orchestration execution,
72+
result serialization, or forced generator teardown fail the orchestration with the exception's type,
73+
message, and stack trace. An existing orchestration failure is retained if
74+
teardown also fails. Outstanding acquired entity locks are released with the
75+
failed response; durable work and status changes made only by teardown are not
76+
committed.
77+
78+
The gRPC worker does not turn `asyncio.CancelledError`, `GeneratorExit`,
79+
`SystemExit`, or `KeyboardInterrupt` into orchestration failures. This also applies
80+
to nested exception groups containing these signals on Python 3.11 and later.
81+
The existing warning-only handling of direct `asyncio.CancelledError` during
82+
forced teardown is unchanged. Other cancellation or process-control exits
83+
propagate after the worker attempts to abandon the accepted dispatch.
84+
85+
If a completion or abandonment RPC raises, its server-side outcome may be
86+
unknown. The worker logs the uncertainty and closes the dispatch's originating
87+
work-item stream so the scheduler can recover ownership. It does not retry an
88+
uncertain RPC or send a conflicting completion or abandonment. Other work owned
89+
by that stream can also be redelivered. This is not an exactly-once execution
90+
guarantee or a bounded recovery-time guarantee. Cancelling an asyncio await of a
91+
still-running executor thread does not abandon that thread's dispatch.
92+
2593
## Fan-out/fan-in
2694

2795
An orchestration can fan-out a dynamic number of function calls in parallel and then fan-in the

‎durabletask-azuremanaged/CHANGELOG.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,23 @@ directly.
1818

1919
FIXED
2020

21+
- With the corresponding core `durabletask` SDK fix, oversized messages fail
22+
large-payload validation before any uploads or payload replacements. Size
23+
errors now take precedence over hypothetical earlier storage failures.
24+
- With the corresponding core `durabletask` SDK fix, orchestrations that exceed
25+
the configured external payload storage limit fail without retrying and release
26+
their entity locks. Transient payload-store failures remain retryable.
27+
- With the corresponding core `durabletask` SDK fix, previously unhandled
28+
non-cancellation `BaseException` exits, including result serialization failures,
29+
now fail with a clear reason. Cancellation and process-control
30+
exits recover dispatch ownership, and uncertain completion or abandonment RPCs
31+
reset the originating work-item stream rather than retaining unfinished work.
2132
- With the corresponding core `durabletask` SDK fix, orchestrator generators
22-
are closed when a dispatch ends, so cleanup in `finally` blocks runs
23-
deterministically, including for async bridges.
33+
are closed at the end of each dispatch, including while waiting for durable work.
34+
See [cleanup semantics](../docs/supported-patterns.md#cleanup-in-orchestrator-finally-blocks)
35+
for repeated finalizers, discarded teardown actions/status, and yielding restrictions.
36+
- With the corresponding core `durabletask` SDK fix, async cancellation raised by
37+
generator teardown no longer replaces dispatch responses or version-rejection abandonment.
2438

2539
## v1.12.0
2640

‎durabletask/client.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ class OrchestrationQuery:
197197
max_instance_count: int | None = (1 << 31) - 1
198198
fetch_inputs_and_outputs: bool = False
199199
instance_id_prefix: str | None = None
200+
task_hub_names: list[str] | None = None
200201

201202

202203
@dataclass

‎durabletask/entities/entity_instance_id.py‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4+
45
class EntityInstanceId:
6+
__slots__ = ("_entity", "_key")
7+
58
def __init__(self, entity: str, key: str):
69
EntityInstanceId.validate_entity_name(entity)
710
EntityInstanceId.validate_key(key)
8-
self.entity = entity.lower()
9-
self.key = key
11+
object.__setattr__(self, "_entity", entity.lower())
12+
object.__setattr__(self, "_key", key)
13+
14+
@property
15+
def entity(self) -> str:
16+
return self._entity
17+
18+
@property
19+
def key(self) -> str:
20+
return self._key
21+
22+
def __setattr__(self, name: str, value: object) -> None:
23+
raise AttributeError(
24+
f"'{type(self).__name__}' object does not support attribute assignment"
25+
)
26+
27+
def __delattr__(self, name: str) -> None:
28+
raise AttributeError(
29+
f"'{type(self).__name__}' object does not support attribute deletion"
30+
)
31+
32+
def __copy__(self) -> "EntityInstanceId":
33+
return self
34+
35+
def __deepcopy__(self, memo: object) -> "EntityInstanceId":
36+
return self
37+
38+
def __reduce__(self) -> tuple[type["EntityInstanceId"], tuple[str, str]]:
39+
return (type(self), (self._entity, self._key))
1040

1141
def __str__(self) -> str:
1242
return f"@{self.entity}@{self.key}"
@@ -21,6 +51,12 @@ def __lt__(self, other: object) -> bool:
2151
return NotImplemented
2252
return str(self) < str(other)
2353

54+
def __hash__(self) -> int:
55+
return hash((self.entity, self.key))
56+
57+
def __repr__(self) -> str:
58+
return f"EntityInstanceId(entity={self.entity!r}, key={self.key!r})"
59+
2460
@staticmethod
2561
def parse(entity_id: str) -> "EntityInstanceId":
2662
"""Parse a string representation of an entity ID into an EntityInstanceId object.

‎durabletask/internal/client_helpers.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def build_query_instances_req(
103103
runtimeStatus=[status.value for status in orchestration_query.runtime_status] if orchestration_query.runtime_status else None,
104104
createdTimeFrom=helpers.new_timestamp(orchestration_query.created_time_from) if orchestration_query.created_time_from else None,
105105
createdTimeTo=helpers.new_timestamp(orchestration_query.created_time_to) if orchestration_query.created_time_to else None,
106+
taskHubNames=[wrappers_pb2.StringValue(value=name) for name in orchestration_query.task_hub_names] if orchestration_query.task_hub_names else None,
106107
instanceIdPrefix=helpers.get_string_value(orchestration_query.instance_id_prefix),
107108
maxInstanceCount=orchestration_query.max_instance_count,
108109
fetchInputsAndOutputs=orchestration_query.fetch_inputs_and_outputs,

0 commit comments

Comments
 (0)