Skip to content

Commit c09493d

Browse files
committed
Merge main into send event branch
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 56111c85-99ce-477c-a97f-c2d57a162a55
2 parents 0072642 + ac9e5bb commit c09493d

51 files changed

Lines changed: 2058 additions & 219 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
- name: Set up Node.js (needed for Azurite and Functions Core Tools)
8181
uses: actions/setup-node@v4
8282
with:
83-
node-version: "20.x"
83+
node-version: "22.x"
8484

8585
- name: Install Azurite and Azure Functions Core Tools
8686
run: |

‎.github/workflows/durabletask.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
- name: Set up Node.js (needed for Azurite)
4949
uses: actions/setup-node@v4
5050
with:
51-
node-version: '20.x'
51+
node-version: '22.x'
5252

5353
- name: Cache npm
5454
uses: actions/cache@v3
@@ -57,7 +57,7 @@ jobs:
5757
key: ${{ runner.os }}-npm-azurite
5858

5959
- name: Install Azurite
60-
run: npm install -g azurite
60+
run: npm install -g azurite@latest
6161

6262
- name: Install Nox
6363
run: |

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ ADDED
1313
delivery from an orchestration to another orchestration instance. During
1414
replay, Python validates the target instance ID as well as the event name,
1515
intentionally detecting target changes that DurableTask.Core does not.
16+
- Added `emit_trace_spans=False` to `TaskHubGrpcWorker`,
17+
`TaskHubGrpcClient`, and `AsyncTaskHubGrpcClient` for hosts that own Durable
18+
Task lifecycle spans. In this propagate-only mode, W3C trace context remains
19+
active and is propagated without emitting duplicate SDK lifecycle spans.
1620
- Added asynchronous `AsyncScheduledTaskClient` / `AsyncScheduleClient` and
1721
`AsyncExportHistoryClient` / `AsyncExportHistoryJobClient` APIs. Applications
1822
using `AsyncTaskHubGrpcClient` can now manage scheduled tasks and history
@@ -37,6 +41,9 @@ import paths, `__all__`, `dir()`, and star-imports behave exactly as before.
3741

3842
FIXED
3943

44+
- Fixed `AsyncTaskHubGrpcClient` failing during construction when no current
45+
event loop was set. SDK-owned async gRPC channels are now created on first use,
46+
binding them to the event loop that performs the RPC.
4047
- Fixed the worker allocating one `asyncio` task per queued work item before applying the concurrency limit, which made memory use and event-loop scheduling overhead grow with the queue backlog during bursts. In-flight work item tasks are now bounded by the configured `ConcurrencyOptions` limits.
4148
- Fixed input/output type discovery raising `TypeError: unhashable type` for handlers that are unhashable callables. A callable object registered through `add_named_activity()`, `add_named_orchestrator()`, or `add_named_entity()` is unhashable whenever its class defines `__eq__` without `__hash__` — most commonly a `@dataclass` with a `__call__` method, since dataclasses default to `eq=True`. Annotations on such handlers are now discovered normally instead of failing.
4249

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
ADDED
1111

12+
- Distributed tracing now correlates OpenTelemetry spans created by orchestrator
13+
user code with the Durable Functions host trace while avoiding duplicate
14+
orchestration, activity, entity, client-start, and client-event lifecycle spans
15+
from the Python SDK.
1216
- Added `SyncDurableFunctionsClient`. `DFApp.durable_client_input()` now injects
1317
the synchronous client into synchronous functions and the asynchronous client
1418
into coroutine functions. Both clients support scheduled-task and history-export
1519
APIs without an async-to-sync bridge.
20+
- Added runnable 2.x samples for function chaining, fan-out/fan-in, human
21+
interaction, and durable entities, plus a migration guide for applications
22+
upgrading from 1.x.
23+
24+
CHANGED
25+
26+
- Reused host-driven orchestration and entity workers across invocations,
27+
avoiding repeated allocation of unused worker resources.
28+
29+
FIXED
30+
31+
- Fixed asynchronous durable-client construction failing after an application
32+
event loop had been closed or cleared.
33+
- Prevented Durable HTTP calls from forwarding managed identity tokens,
34+
authorization headers, cookies, proxy credentials, or function keys to
35+
cross-origin redirect and polling targets. Function keys are now removed from
36+
every `202 Accepted` poll, including same-origin polls, because the initial
37+
function-level key may not authorize the status endpoint. Direct client
38+
invocation of the internal HTTP polling orchestrator is now rejected.
1639

1740
## 2.0.0b1
1841

@@ -178,8 +201,3 @@ code:
178201
`show_history_output` flags for signature compatibility but ignore them, so
179202
the returned status has no `historyEvents`. Use
180203
`get_orchestration_history(...)` to retrieve history.
181-
- Distributed tracing is not yet wired up. The Durable Functions host delivers
182-
the parent trace context and emits the orchestration/activity spans itself,
183-
so orchestrator user-code spans in the Python worker are not yet correlated
184-
to it, and durabletask's own span emission is intentionally left disabled to
185-
avoid duplicating the host's spans.
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Migrate from Azure Functions Durable 1.x to 2.x
2+
3+
This guide explains how to move an application from
4+
`azure-functions-durable` 1.x to 2.x. Version 2.x is a ground-up rewrite built
5+
on the [`durabletask`](https://pypi.org/project/durabletask/) SDK and its gRPC
6+
runtime.
7+
8+
> [!WARNING]
9+
> `azure-functions-durable` 2.x is currently in preview. Its APIs may change
10+
> before the stable 2.0.0 release.
11+
12+
## Choose a migration path
13+
14+
Version 2.x includes compatibility shims for most 1.x APIs used by
15+
decorator-based apps. This supports a staged migration:
16+
17+
1. **Compatibility upgrade:** update the runtime and dependencies while keeping
18+
existing one-argument orchestrators, entities, and client calls. These APIs
19+
continue to work but emit `DeprecationWarning` where a native replacement
20+
exists.
21+
2. **Native API migration:** move to durabletask-native function signatures,
22+
client methods, retry policies, and entity types.
23+
24+
Use the compatibility upgrade first when minimizing application changes is more
25+
important than adopting the new API immediately.
26+
27+
## Requirements and breaking changes
28+
29+
Before upgrading, account for these 2.x requirements:
30+
31+
- **Python 3.13 or later is required.**
32+
- **Only the Azure Functions Python V2 programming model is supported.** Apps
33+
must use `DFApp` or `Blueprint` decorators. The classic programming model with
34+
one directory and `function.json` per function is not supported.
35+
- **A modern Durable Functions host is required.** Local sample apps use the
36+
preview extension bundle shown below.
37+
- **The OpenAI Agents integration was removed.** The
38+
`azure.durable_functions.openai_agents` package is not available in 2.x.
39+
- **The primary APIs now come from `durabletask`.** Compatibility aliases are
40+
available to stage the migration, but new code should use the native names.
41+
42+
If the app still uses the classic Python programming model, migrate it to the
43+
[decorator-based model](https://learn.microsoft.com/azure/azure-functions/functions-reference-python)
44+
before upgrading this package.
45+
46+
## Prepare running orchestrations
47+
48+
Orchestrator code must remain deterministic and replay-compatible for existing
49+
instances. Before changing function names, activity schedules, branching logic,
50+
or serialized inputs:
51+
52+
- allow existing instances to finish; or
53+
- deploy the migrated app with a new task hub and route new instances there.
54+
55+
Test replay behavior with representative orchestration histories in a
56+
non-production environment before updating a task hub that contains running
57+
instances.
58+
59+
## Perform a compatibility upgrade
60+
61+
### 1. Update the Python runtime
62+
63+
Update local development, CI, and the deployed Function App to Python 3.13 or
64+
later.
65+
66+
### 2. Update dependencies
67+
68+
Replace the 1.x package constraint in `requirements.txt`:
69+
70+
```text
71+
azure-functions>=2.3.0b2
72+
azure-functions-durable>=2.0.0b1,<3
73+
```
74+
75+
### 3. Update the extension bundle
76+
77+
Use the preview extension bundle that contains the required Durable Task gRPC
78+
runtime:
79+
80+
```json
81+
{
82+
"version": "2.0",
83+
"extensionBundle": {
84+
"id": "Microsoft.Azure.Functions.ExtensionBundle.Preview",
85+
"version": "[4.*, 5.0.0)"
86+
},
87+
"extensions": {
88+
"durableTask": {
89+
"storageProvider": {
90+
"type": "AzureStorage"
91+
}
92+
}
93+
}
94+
}
95+
```
96+
97+
### 4. Keep supported 1.x code temporarily
98+
99+
A decorator-based 1.x orchestrator and client starter can run through the 2.x
100+
compatibility layer without changing their function shape:
101+
102+
```python
103+
import azure.functions as func
104+
import azure.durable_functions as df
105+
106+
app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
107+
108+
109+
@app.orchestration_trigger(context_name="context")
110+
def hello(context: df.DurableOrchestrationContext):
111+
name = context.get_input()
112+
return (yield context.call_activity("say_hello", name))
113+
114+
115+
@app.route(route="start", methods=["POST"])
116+
@app.durable_client_input(client_name="client")
117+
async def start(
118+
req: func.HttpRequest,
119+
client: df.DurableFunctionsClient) -> func.HttpResponse:
120+
instance_id = await client.start_new("hello", client_input="Tokyo")
121+
return client.create_check_status_response(req, instance_id)
122+
```
123+
124+
The single-argument orchestrator receives the compatibility
125+
`DurableOrchestrationContext`, and `start_new` delegates to the native client.
126+
Use this state to validate the infrastructure upgrade before changing
127+
application APIs.
128+
129+
## Migrate to native orchestration APIs
130+
131+
Native orchestrators accept the durabletask context and input as separate
132+
arguments:
133+
134+
```python
135+
from typing import Any
136+
137+
import azure.functions as func
138+
import azure.durable_functions as df
139+
from durabletask import task
140+
141+
app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
142+
143+
144+
@app.orchestration_trigger(context_name="context")
145+
def hello(ctx: task.OrchestrationContext, name: Any):
146+
return (yield ctx.call_activity("say_hello", input=name))
147+
148+
149+
@app.route(route="start", methods=["POST"])
150+
@app.durable_client_input(client_name="client")
151+
async def start(
152+
req: func.HttpRequest,
153+
client: df.DurableFunctionsClient) -> func.HttpResponse:
154+
instance_id = await client.schedule_new_orchestration("hello", input="Tokyo")
155+
return client.create_check_status_response(req, instance_id)
156+
```
157+
158+
Activity functions can keep their existing single-input signature:
159+
160+
```python
161+
@app.activity_trigger(input_name="name")
162+
def say_hello(name: str) -> str:
163+
return f"Hello {name}!"
164+
```
165+
166+
### Orchestration API mapping
167+
168+
| 1.x API | Native 2.x API |
169+
| --- | --- |
170+
| `context.get_input()` | The orchestrator's second argument |
171+
| `context.call_activity(name, input_)` | `ctx.call_activity(name, input=input_)` |
172+
| `context.call_activity_with_retry(...)` | `ctx.call_activity(..., retry_policy=policy)` |
173+
| `context.call_sub_orchestrator(name, input_)` | `ctx.call_sub_orchestrator(name, input=input_)` |
174+
| `call_sub_orchestrator_with_retry` | `ctx.call_sub_orchestrator(..., retry_policy=...)` |
175+
| `context.task_all(tasks)` | `task.when_all(tasks)` |
176+
| `context.task_any(tasks)` | `task.when_any(tasks)` |
177+
| `context.new_guid()` | `ctx.new_uuid()` |
178+
| `df.EntityId(name, key)` | `entities.EntityInstanceId(name, key)` |
179+
180+
Methods including `create_timer`, `wait_for_external_event`,
181+
`continue_as_new`, `set_custom_status`, `call_entity`, and `signal_entity`
182+
retain their names. Activity, sub-orchestration, and entity calls use keyword
183+
`input` instead of the 1.x `input_` or `operationInput` names.
184+
`continue_as_new` uses `new_input`.
185+
186+
### Retry policy mapping
187+
188+
`RetryOptions` accepted millisecond values. Native `RetryPolicy` uses
189+
`timedelta`:
190+
191+
```python
192+
from datetime import timedelta
193+
194+
from durabletask import task
195+
196+
policy = task.RetryPolicy(
197+
first_retry_interval=timedelta(seconds=1),
198+
max_number_of_attempts=3,
199+
backoff_coefficient=2.0,
200+
)
201+
202+
result = yield ctx.call_activity(
203+
"call_service",
204+
input=request,
205+
retry_policy=policy,
206+
)
207+
```
208+
209+
## Migrate client APIs
210+
211+
`durable_client_input` supplies `DurableFunctionsClient` to coroutine functions
212+
and `SyncDurableFunctionsClient` to synchronous functions. Async client calls
213+
must be awaited; the corresponding sync calls must not be awaited.
214+
215+
| 1.x client API | Native 2.x client API |
216+
| --- | --- |
217+
| `start_new` | `schedule_new_orchestration` |
218+
| `get_status` | `get_orchestration_state` |
219+
| `get_status_all` | `get_all_orchestration_states` |
220+
| `get_status_by` | `get_all_orchestration_states` with `OrchestrationQuery` |
221+
| `raise_event` | `raise_orchestration_event` |
222+
| `terminate` | `terminate_orchestration` |
223+
| `suspend` | `suspend_orchestration` |
224+
| `resume` | `resume_orchestration` |
225+
| `restart` | `restart_orchestration` |
226+
| `rewind` | `rewind_orchestration` |
227+
| `purge_instance_history` | `purge_orchestration` |
228+
| `purge_instance_history_by` | `purge_orchestrations_by` |
229+
| `read_entity_state` | `get_entity` |
230+
| `get_client_response_links` | `create_http_management_payload` |
231+
232+
Native `restart_orchestration` reuses the current instance ID by default. Pass
233+
`restart_with_new_instance_id=True` to preserve the 1.x `restart` default.
234+
235+
Native status APIs return durabletask models rather than the compatibility
236+
wrappers `DurableOrchestrationStatus`, `PurgeHistoryResult`, and
237+
`EntityStateResponse`. Update code that serializes or checks those return
238+
values. Use `get_orchestration_history` when history events are required.
239+
240+
## Migrate durable entities
241+
242+
One-argument entity functions remain available through
243+
`DurableEntityContext`. Native code can use a two-argument entity function or a
244+
`DurableEntity` class. A class exposes each method as an entity operation:
245+
246+
```python
247+
from typing import Any
248+
249+
import azure.durable_functions as df
250+
from durabletask.entities import DurableEntity
251+
252+
253+
@app.entity_trigger(context_name="context")
254+
class Counter(DurableEntity):
255+
def add(self, input_: Any = None) -> int:
256+
value = self.get_state(int, 0) + (input_ or 0)
257+
self.set_state(value)
258+
return value
259+
260+
def get(self, input_: Any = None) -> int:
261+
return self.get_state(int, 0)
262+
```
263+
264+
Replace `df.EntityId` with `durabletask.entities.EntityInstanceId` at call
265+
sites. For function-based entities, return the operation result directly
266+
instead of calling `context.set_result`.
267+
268+
## Review compatibility limitations
269+
270+
- `DurableOrchestrationContext.histories` is unavailable. Retrieve history with
271+
`client.get_orchestration_history(instance_id)`.
272+
- The compatibility `show_history` and `show_history_output` status flags are
273+
accepted but ignored.
274+
- Distributed tracing is not yet wired through the Python provider.
275+
- Continuous history export is not supported by Azure Functions.
276+
- Unusual callable signatures can be misclassified by the compatibility layer.
277+
Keep compatibility orchestrators and entities to exactly one positional
278+
argument, and native functions to exactly two.
279+
280+
See the [changelog](CHANGELOG.md) for the complete compatibility surface and
281+
known limitations.
282+
283+
## Validate and deploy
284+
285+
Before production rollout:
286+
287+
1. Run unit tests against Python 3.13 or later.
288+
2. Run the app locally with Azurite and Azure Functions Core Tools.
289+
3. Start new orchestration, sub-orchestration, entity, retry, timer, and
290+
external-event scenarios used by the app.
291+
4. Verify status queries and HTTP management URLs.
292+
5. Test existing orchestration histories or drain existing instances.
293+
6. Deploy to a staging slot or non-production task hub before production.
294+
295+
The [2.x samples](samples/) provide runnable native examples for common
296+
orchestration patterns.

0 commit comments

Comments
 (0)