Skip to content

Commit dfd91a6

Browse files
committed
Merge origin/main into tracing correlation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b872ac61-5bac-40e3-b9c7-daf295725fde
2 parents c5eba5a + b229e69 commit dfd91a6

26 files changed

Lines changed: 871 additions & 20 deletions

azure-functions-durable/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ from the Python SDK.
1717
the synchronous client into synchronous functions and the asynchronous client
1818
into coroutine functions. Both clients support scheduled-task and history-export
1919
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.
2028

2129
## 2.0.0b1
2230

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.

azure-functions-durable/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ built on top of the [`durabletask`](https://pypi.org/project/durabletask/) SDK.
77
> [!NOTE]
88
> 2.x is a ground-up rewrite of the Durable Functions Python SDK on top of the
99
> `durabletask` runtime. It is currently a preview (beta) release; APIs may
10-
> change before the stable 2.0.0. See [CHANGELOG.md](CHANGELOG.md) for the
11-
> migration summary from 1.x, including breaking changes.
10+
> change before the stable 2.0.0.
1211
1312
## Requirements
1413

@@ -35,6 +34,8 @@ calls (`context.call_http(...)`), recurring scheduled tasks, and history export.
3534

3635
## Links
3736

37+
- [2.x samples](samples/)
38+
- [Migration guide from 1.x](MIGRATION_GUIDE.md)
3839
- [Changelog](CHANGELOG.md)
3940
- [Durable Functions documentation](https://learn.microsoft.com/azure/azure-functions/durable/)
4041
- [`durabletask` on PyPI](https://pypi.org/project/durabletask/)

azure-functions-durable/azure/durable_functions/decorators/durable_app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ def decorator(entity_func: task.Entity[Any, Any]) -> FunctionBuilder:
233233
if entity_name is not None:
234234
entity_func.__durable_entity_name__ = entity_name # type: ignore[union-attr]
235235
# Construct an orchestrator based on the end-user code
236+
worker = DurableFunctionsWorker()
236237

237238
# TODO: Because this handle method is the one actually exposed to the Functions SDK decorator,
238239
# the parameter name will always be "context" here, even if the user specified a different name.
@@ -243,7 +244,7 @@ def decorator(entity_func: task.Entity[Any, Any]) -> FunctionBuilder:
243244
# binding converter to accept it; at runtime the host passes that
244245
# transport context (exposing ``.body``).
245246
def handle(context: func.EntityContext) -> str:
246-
return DurableFunctionsWorker().execute_entity_batch_request(entity_func, context)
247+
return worker.execute_entity_batch_request(entity_func, context)
247248

248249
handle.entity_function = entity_func # pyright: ignore[reportFunctionMemberAccess]
249250

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ class Orchestrator:
2020
function.
2121
"""
2222

23-
def __init__(self, orchestrator_func: Callable[..., Any]):
23+
def __init__(
24+
self,
25+
orchestrator_func: Callable[..., Any],
26+
worker: DurableFunctionsWorker | None = None,
27+
):
2428
"""Create a new orchestrator wrapper for a user orchestrator function.
2529
2630
The wrapped function may be a durabletask-native two-argument
@@ -30,6 +34,7 @@ def __init__(self, orchestrator_func: Callable[..., Any]):
3034
:param orchestrator_func: The user's orchestrator function to run.
3135
"""
3236
self.fn: Callable[..., Any] = orchestrator_func
37+
self._worker = worker if worker is not None else DurableFunctionsWorker()
3338

3439
def handle(self, context: func.OrchestrationContext) -> str:
3540
"""Handle the orchestration of the user defined generator function.
@@ -49,7 +54,7 @@ def handle(self, context: func.OrchestrationContext) -> str:
4954
Durable Functions host wire format) produced by this invocation.
5055
"""
5156
self.durable_context = context
52-
return DurableFunctionsWorker().execute_orchestration_request(self.fn, context)
57+
return self._worker.execute_orchestration_request(self.fn, context)
5358

5459
@classmethod
5560
def create(cls, fn: Callable[..., Any]) -> Callable[[Any], str]:
@@ -67,13 +72,15 @@ def create(cls, fn: Callable[..., Any]) -> Callable[[Any], str]:
6772
Handle function of the newly created orchestration client
6873
"""
6974

75+
worker = DurableFunctionsWorker()
76+
7077
# The generated handle is the function registered with the Azure
7178
# Functions host. Its ``context`` parameter must be annotated with
7279
# ``azure.functions.OrchestrationContext`` so the host's
7380
# orchestrationTrigger binding converter accepts it; at runtime the
7481
# host passes that transport context (exposing ``.body``).
7582
def handle(context: func.OrchestrationContext) -> str:
76-
return Orchestrator(fn).handle(context)
83+
return Orchestrator(fn, worker).handle(context)
7784

7885
handle.orchestrator_function = fn # pyright: ignore[reportFunctionMemberAccess]
7986

0 commit comments

Comments
 (0)