Skip to content

Commit af18afe

Browse files
committed
Fix gaps found via integration tests
1 parent 46570e9 commit af18afe

8 files changed

Lines changed: 98 additions & 10 deletions

File tree

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
11+
12+
- Added a `result` property to `Task` as a convenience alias for `get_result()`.
13+
14+
FIXED
15+
16+
- `OrchestrationContext.create_timer` now accepts timezone-aware `datetime`
17+
values, normalizing them to UTC instead of raising when compared against the
18+
orchestration's internal clock.
19+
1020
## v1.7.0
1121

1222
ADDED

azure-functions-durable/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- `durable_client_input` now injects a rich `DurableFunctionsClient` into the
13+
decorated function's client parameter (the binding's JSON string is converted
14+
to a client object). Previously the client parameter received the raw string.
15+
- `DurableFunctionsClient` now applies the host-provided
16+
`maxGrpcMessageSizeInBytes` to the gRPC channel's send/receive message limits
17+
(when provided), allowing large orchestration payloads to be retrieved. When
18+
the host does not supply a value, the gRPC library defaults are left in place.
19+
- `DurableOrchestrationContext.current_utc_datetime` is now timezone-aware
20+
(UTC), matching v1, so comparisons against timezone-aware datetimes (e.g. a
21+
parsed scheduled-start time) no longer raise.
22+
- `DurableOrchestrationStatus.to_json()` now emits orchestration payloads
23+
(`output`, `input`, `customStatus`) as their raw JSON representation instead
24+
of reconstructed Python objects, so the result is always JSON-serializable
25+
even when payloads are custom types.
26+
1027
### Added
1128

29+
- The `orchestration_trigger` decorator now accepts an `input_type` argument
30+
(v1 parity). When set, a v1-style `context.get_input()` decodes the input to
31+
that type; a call-site `expected_type` on `get_input` takes precedence.
1232
- One-argument (Azure Functions / v1-style) entity functions
1333
(``def entity(context):``) are now supported. The worker detects the entity's
1434
shape and, for single-argument functions, delivers a functional

azure-functions-durable/azure/durable_functions/client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
OrchestrationStatus,
1616
)
1717
from durabletask.entities import EntityInstanceId
18+
from durabletask.grpc_options import GrpcChannelOptions
1819
from .internal.azurefunctions_grpc_interceptor import AzureFunctionsAsyncDefaultClientInterceptorImpl
1920
from .internal.serialization import DEFAULT_FUNCTIONS_DATA_CONVERTER
2021
from .http import HttpManagementPayload
@@ -58,13 +59,23 @@ def __init__(self, client_as_string: str):
5859

5960
interceptors = [AzureFunctionsAsyncDefaultClientInterceptorImpl(self.taskHubName, self.requiredQueryStringParameters)]
6061

62+
# Only override the gRPC message size limits when the host explicitly
63+
# provides a value. When unset (0), we leave the gRPC library defaults
64+
# in place rather than applying a large default of our own.
65+
channel_options: GrpcChannelOptions | None = None
66+
if self.maxGrpcMessageSizeInBytes > 0:
67+
channel_options = GrpcChannelOptions(
68+
max_receive_message_length=self.maxGrpcMessageSizeInBytes,
69+
max_send_message_length=self.maxGrpcMessageSizeInBytes)
70+
6171
# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
6272
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
6373
super().__init__(
6474
host_address=self.rpcBaseUrl,
6575
secure_channel=False,
6676
metadata=None,
6777
interceptors=interceptors,
78+
channel_options=channel_options,
6879
data_converter=DEFAULT_FUNCTIONS_DATA_CONVERTER)
6980

7081
def _parse_client_configuration(self, client_as_string: str) -> None:

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

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

4+
import json
45
from datetime import datetime
56
from typing import Any, Optional
67

@@ -94,7 +95,13 @@ def history(self) -> Optional[list[Any]]:
9495
return None
9596

9697
def to_json(self) -> dict[str, Any]:
97-
"""Convert this status into a v1-compatible JSON dictionary."""
98+
"""Convert this status into a v1-compatible JSON dictionary.
99+
100+
Payload fields (``output``, ``input``, ``customStatus``) are emitted as
101+
their raw JSON representation rather than the reconstructed Python
102+
objects, so the result is always JSON-serializable even when the
103+
orchestration payloads are custom types.
104+
"""
98105
result: dict[str, Any] = {}
99106
if self.name is not None:
100107
result["name"] = self.name
@@ -104,12 +111,32 @@ def to_json(self) -> dict[str, Any]:
104111
result["createdTime"] = self.created_time.isoformat()
105112
if self.last_updated_time is not None:
106113
result["lastUpdatedTime"] = self.last_updated_time.isoformat()
107-
if self.output is not None:
108-
result["output"] = self.output
109-
if self.input_ is not None:
110-
result["input"] = self.input_
114+
output = self._raw_payload(
115+
self._state.serialized_output if self._state is not None else None)
116+
if output is not None:
117+
result["output"] = output
118+
input_ = self._raw_payload(
119+
self._state.serialized_input if self._state is not None else None)
120+
if input_ is not None:
121+
result["input"] = input_
111122
if self.runtime_status is not None:
112123
result["runtimeStatus"] = self.runtime_status.name
113-
if self.custom_status is not None:
114-
result["customStatus"] = self.custom_status
124+
custom_status = self._raw_payload(
125+
self._state.serialized_custom_status if self._state is not None else None)
126+
if custom_status is not None:
127+
result["customStatus"] = custom_status
115128
return result
129+
130+
@staticmethod
131+
def _raw_payload(serialized: Optional[str]) -> Any:
132+
"""Parse a serialized payload as plain JSON without reconstructing types.
133+
134+
Returns the parsed JSON value (which is always JSON-serializable), or the
135+
original string if it is not valid JSON, or ``None`` when absent.
136+
"""
137+
if serialized is None:
138+
return None
139+
try:
140+
return json.loads(serialized)
141+
except (TypeError, ValueError):
142+
return serialized

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Licensed under the MIT License.
33

44
import inspect
5-
from datetime import datetime
5+
from datetime import datetime, timezone
66
from typing import Any, Callable, Generator, Optional, cast
77
from uuid import UUID
88

@@ -61,8 +61,15 @@ def is_replaying(self) -> bool:
6161

6262
@property
6363
def current_utc_datetime(self) -> datetime:
64-
"""Get the replay-safe current UTC date/time."""
65-
return self._ctx.current_utc_datetime
64+
"""Get the replay-safe current UTC date/time.
65+
66+
Returned as a timezone-aware (UTC) datetime for v1 compatibility;
67+
durabletask exposes a naive UTC datetime.
68+
"""
69+
value = self._ctx.current_utc_datetime
70+
if value.tzinfo is None:
71+
return value.replace(tzinfo=timezone.utc)
72+
return value
6673

6774
@property
6875
def custom_status(self) -> Any:

durabletask/task.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,11 @@ def is_failed(self) -> bool:
503503
"""Returns True if the task has failed, False otherwise."""
504504
return self._exception is not None
505505

506+
@property
507+
def result(self) -> T:
508+
"""Returns the result of the task (alias for :meth:`get_result`)."""
509+
return self.get_result()
510+
506511
def get_result(self) -> T:
507512
"""Returns the result of the task."""
508513
if not self._is_complete:

durabletask/worker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,6 +1668,11 @@ def create_timer_internal(
16681668
else:
16691669
final_fire_at = fire_at
16701670

1671+
# Normalize timezone-aware datetimes to naive UTC so they can be safely
1672+
# compared against and combined with the orchestration's naive UTC clock.
1673+
if final_fire_at.tzinfo is not None:
1674+
final_fire_at = final_fire_at.astimezone(timezone.utc).replace(tzinfo=None)
1675+
16711676
next_fire_at: datetime = final_fire_at
16721677

16731678
if (

tests/azure-functions-durable/test_client_compat.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,9 @@ def _fake_state():
399399
created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
400400
last_updated_at=datetime(2026, 1, 2, tzinfo=timezone.utc),
401401
runtime_status=OrchestrationStatus.RUNNING,
402+
serialized_input='{"in": 1}',
403+
serialized_output='{"out": 2}',
404+
serialized_custom_status='"cs"',
402405
get_input=lambda: {"in": 1},
403406
get_output=lambda: {"out": 2},
404407
get_custom_status=lambda: "cs",

0 commit comments

Comments
 (0)