Skip to content

Commit ec185c0

Browse files
author
Bernd Verst
committed
Modernize newly merged code for Python 3.11
Use native exception-group matching and preserve nested failure diagnostics and notes. Extend Self typing to immutable entity copy protocols, remove the obsolete asyncio backport dependency, and cover dispatch and provider compatibility. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6b07f3f-b3e1-49dd-9121-318968272d31
1 parent 3abd6dd commit ec185c0

15 files changed

Lines changed: 204 additions & 40 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@ by one or more task hubs.
1515
CHANGED
1616

1717
- Dropped Python 3.10 support. Python 3.11 or later is now required.
18+
- Removed the obsolete PyPI `asyncio` backport dependency; all supported
19+
Python versions use the standard-library module.
1820
- Context managers now preserve subclass types in type checkers, including
1921
Azure Managed clients and workers.
22+
- Entity ID copy and pickle hooks now preserve subclass types in type checkers.
2023
- Multiple task completion callback failures now raise the built-in
2124
`ExceptionGroup`, preserving each exception and its traceback. A single callback
2225
failure is still raised directly.
2326

2427
FIXED
2528

29+
- Grouped failures now retain nested exception messages, tracebacks, and notes
30+
in failure details, including `ExceptionGroup` failures from task callbacks.
2631
- Implemented `__hash__` and `__repr__` for `EntityInstanceId`, making identity attributes immutable to guarantee hash stability when used in sets or dict keys.
2732
- Large-payload externalization now checks every eligible payload's maximum
2833
size before uploading or replacing any protobuf values, including carryover

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ directly.
2727

2828
FIXED
2929

30+
- With the corresponding core SDK update, grouped failures retain nested
31+
exception messages, tracebacks, and notes in failure details.
3032
- With the corresponding core `durabletask` SDK fix, oversized messages fail
3133
large-payload validation before any uploads or payload replacements. Size
3234
errors now take precedence over hypothetical earlier storage failures.

‎durabletask-azuremanaged/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ directly.
1818

1919
FIXED
2020

21+
- With the corresponding core SDK update, grouped failures retain nested
22+
exception messages, tracebacks, and notes in failure details.
2123
- With the corresponding core `durabletask` SDK fix, oversized messages fail
2224
large-payload validation before any uploads or payload replacements. Size
2325
errors now take precedence over hypothetical earlier storage failures.

‎durabletask/entities/entity_instance_id.py‎

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

4+
from typing import Self
5+
46

57
class EntityInstanceId:
68
__slots__ = ("_entity", "_key")
@@ -29,13 +31,13 @@ def __delattr__(self, name: str) -> None:
2931
f"'{type(self).__name__}' object does not support attribute deletion"
3032
)
3133

32-
def __copy__(self) -> "EntityInstanceId":
34+
def __copy__(self) -> Self:
3335
return self
3436

35-
def __deepcopy__(self, memo: object) -> "EntityInstanceId":
37+
def __deepcopy__(self, memo: object) -> Self:
3638
return self
3739

38-
def __reduce__(self) -> tuple[type["EntityInstanceId"], tuple[str, str]]:
40+
def __reduce__(self) -> tuple[type[Self], tuple[str, str]]:
3941
return (type(self), (self._entity, self._key))
4042

4143
def __str__(self) -> str:

‎durabletask/internal/helpers.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,9 @@ def new_failure_details(
258258
errorType=get_qualified_name(type(ex)),
259259
errorMessage=str(ex),
260260
stackTrace=wrappers_pb2.StringValue(value=''.join(
261-
traceback.format_tb(ex.__traceback__) if isinstance(ex, Exception)
262-
else traceback.format_exception(type(ex), ex, ex.__traceback__))),
261+
traceback.format_tb(ex.__traceback__)
262+
if isinstance(ex, Exception) and not isinstance(ex, BaseExceptionGroup)
263+
else traceback.format_exception(ex))),
263264
innerFailure=(
264265
new_failure_details(
265266
inner, exception_properties_provider, logger, _visited)

‎durabletask/worker.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import json
88
import logging
99
import os
10-
import sys
1110
import time
1211
from collections.abc import Callable, Generator, Sequence
1312
from concurrent.futures import ThreadPoolExecutor
@@ -64,11 +63,11 @@
6463

6564

6665
def _is_control_flow_exception(error: BaseException) -> bool:
67-
if isinstance(error, (asyncio.CancelledError, GeneratorExit, KeyboardInterrupt, SystemExit)):
68-
return True
69-
if sys.version_info >= (3, 11) and isinstance(error, BaseExceptionGroup):
70-
return any(_is_control_flow_exception(inner) for inner in error.exceptions)
71-
return False
66+
control_flow_types = (asyncio.CancelledError, GeneratorExit, KeyboardInterrupt, SystemExit)
67+
if isinstance(error, BaseExceptionGroup):
68+
group = cast(BaseExceptionGroup[BaseException], error)
69+
return group.subgroup(control_flow_types) is not None
70+
return isinstance(error, control_flow_types)
7271

7372

7473
class ConcurrencyOptions:

‎pyproject.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ readme = "README.md"
3232
dependencies = [
3333
"grpcio",
3434
"protobuf",
35-
"asyncio",
3635
"packaging"
3736
]
3837

‎requirements.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ pytest-cov
88
azure-identity
99
azure-functions
1010
typing-extensions
11-
asyncio
1211
packaging
1312
opentelemetry-api
1413
opentelemetry-sdk

‎tests/azure-functions-durable/test_worker_compat.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ def orchestrator(context):
8484
assert len(response.actions) == 1
8585

8686

87+
@pytest.mark.parametrize("use_async", [False, True])
88+
async def test_worker_preserves_grouped_failure_diagnostics(monkeypatch, use_async):
89+
monkeypatch.setattr(payloads, "_payload_store", None)
90+
91+
def orchestrator(context):
92+
error = ValueError("nested failure")
93+
error.add_note("nested note")
94+
raise ExceptionGroup("activity group", [error, RuntimeError("other failure")])
95+
96+
worker = DurableFunctionsWorker()
97+
request = _encode_orchestrator_request("grouped-failure")
98+
if use_async:
99+
encoded = await worker.execute_orchestration_request_async(orchestrator, request)
100+
else:
101+
encoded = worker.execute_orchestration_request(orchestrator, request)
102+
response = _decode_orchestrator_response(encoded)
103+
completion = _get_completion_action(response)
104+
105+
assert completion.orchestrationStatus == pb.ORCHESTRATION_STATUS_FAILED
106+
assert completion.failureDetails.errorType == "builtins.ExceptionGroup"
107+
assert "ValueError: nested failure" in completion.failureDetails.stackTrace.value
108+
assert "RuntimeError: other failure" in completion.failureDetails.stackTrace.value
109+
assert "nested note" in completion.failureDetails.stackTrace.value
110+
111+
87112
def test_worker_created_before_configuration_hydrates_and_externalizes(monkeypatch, payload_store_factory):
88113
monkeypatch.setattr(payloads, "_payload_store", None)
89114
worker = DurableFunctionsWorker()

‎tests/durabletask-azuremanaged/test_azuremanaged_grpc_resiliency.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,32 @@ def orchestrator(ctx: task.OrchestrationContext, _: Any):
7171
assert response.actions[0].completeOrchestration.failureDetails.errorMessage == "deterministic teardown failure"
7272

7373

74+
def test_dts_worker_preserves_grouped_failure_diagnostics():
75+
def orchestrator(ctx: task.OrchestrationContext, _: Any):
76+
error = ValueError("nested failure")
77+
error.add_note("nested note")
78+
raise ExceptionGroup("activity group", [error, RuntimeError("other failure")])
79+
80+
with DurableTaskSchedulerWorker(
81+
host_address="localhost:4001", taskhub="hub", token_credential=None) as worker:
82+
name = worker.add_orchestrator(orchestrator)
83+
request = pb.OrchestratorRequest(instanceId="instance", newEvents=[
84+
helpers.new_orchestrator_started_event(),
85+
helpers.new_execution_started_event(name, "instance"),
86+
])
87+
stub = Mock()
88+
worker._execute_orchestrator(request, stub, "original-token")
89+
90+
stub.CompleteOrchestratorTask.assert_called_once()
91+
stub.AbandonTaskOrchestratorWorkItem.assert_not_called()
92+
completion = stub.CompleteOrchestratorTask.call_args.args[0].actions[0].completeOrchestration
93+
assert completion.orchestrationStatus == pb.ORCHESTRATION_STATUS_FAILED
94+
assert completion.failureDetails.errorType == "builtins.ExceptionGroup"
95+
assert "ValueError: nested failure" in completion.failureDetails.stackTrace.value
96+
assert "RuntimeError: other failure" in completion.failureDetails.stackTrace.value
97+
assert "nested note" in completion.failureDetails.stackTrace.value
98+
99+
74100
def test_dts_worker_passes_exception_properties_provider_to_base_worker():
75101
class PropertiesProvider:
76102
def get_exception_properties(self, exception: Exception) -> Mapping[str, Any] | None:

0 commit comments

Comments
 (0)