Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Task<T> — Base: result, exception, isComplete, isFailed, parent r
│ ├── RetryableTask — Policy-driven retries (exponential backoff)
│ └── RetryHandlerTask — Custom retry handler function
└── CompositeTask — Holds children with parent backlinks
├── WhenAllTask — Completes when ALL children done; fails fast on first failure
├── WhenAllTask — Completes when ALL children are terminal (waits for every child)
└── WhenAnyTask — Completes when ANY child done
```

Expand All @@ -123,11 +123,15 @@ Task<T> — Base: result, exception, isComplete, isFailed, parent r
**Important:** `CompositeTask` checks already-complete children in its constructor.
A composite task can be **immediately complete upon construction** — callers must handle this.

### WhenAll Fail-Fast Behavior
### WhenAll Wait-All Behavior

`WhenAllTask` marks itself complete on the **first** failed child. Other children may still
be in flight. The `isComplete` guard prevents double-completion, but the mental model is:
one failure = whole task fails immediately, remaining results ignored.
`WhenAllTask` completes only when **every** child is terminal (`_completedTasks == _tasks.length`) —
a failing child does **not** complete it early. This prevents a later failing sibling's `TaskFailed`
from being dropped against an already-terminal instance (issue #301). `_exception` is set only at
completion, so `isFailed` and `isComplete` flip together — otherwise `resume()` (which checks
`isFailed` before `isComplete`) would throw into the generator before the other siblings finish,
re-introducing fail-fast. On failure, all child exceptions are aggregated into an `AggregateError`
whose message inlines each child message.

---

Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
## Upcoming

### Breaking changes

- **`whenAll` no longer fails fast.** A `whenAll` task — and therefore the Durable Functions compat
`context.df.Task.all()`, which forwards straight to it — now completes only after **every** child
is terminal, instead of the moment the first child fails. Two user-visible effects: (1) **timing** —
the orchestrator resumes past the `whenAll` only once all siblings finish, not at the first failure;
(2) **error type** — on failure it throws a JS-native `AggregateError` (`.errors` populated, message
of the form `"<N> of <M> tasks in the whenAll failed: <child messages joined by '; '>"`) instead of
re-throwing the first child's own exception. This fixes a lost-failure bug where a later failing
sibling was dropped against an already-terminal `whenAll`
([#301](https://github.com/microsoft/durabletask-js/issues/301)). Fan-out/fan-in is the canonical
Durable Functions pattern, so review any `Task.all()` error handling that relied on early completion
or on catching a single child error.
- **Default sub-orchestration instance IDs now derive from the parent's execution ID.** When a
sub-orchestration is scheduled **without** an explicit `instanceId`, the auto-generated ID changed
from `${parentInstanceId}:${suffix}` to `${parentExecutionId}:${suffix}` (`suffix` is a 4-hex-digit
sequence number). Explicit `instanceId`s are untouched, and there is a **fallback**: backends that
do not populate an execution ID still get the legacy `${parentInstanceId}:${suffix}` form, so this
is not an unconditional format change. It fixes a continue-as-new collision — the sequence number
resets each work item and the parent instance ID is stable across generations, so a default-ID
sub-orchestration scheduled after continue-as-new (e.g. `callHttp`) re-derived the previous
generation's ID and failed with "instance already exists" — and keeps the ID constant-length
(~37 chars) at any nesting depth instead of concatenating every ancestor's ID, which would breach
the Durable Task Scheduler 100-character instance-ID limit at ~2 levels of nesting.

### New

### Fixes
Expand Down
100 changes: 97 additions & 3 deletions packages/azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,99 @@
# Changelog
## Upcoming

## TBD
First preview of the rewritten `durable-functions` provider. This is a **preview release**: it is
published under the `preview` npm dist-tag, and APIs may change before the stable `4.0.0`.
Comment on lines +3 to +4

- Details to be finalized at release time.
Install with:

```bash
npm install durable-functions@preview
```

### Why this is a new major version

v4 is a rewrite on top of `@microsoft/durabletask-js`: replay, activities, entities, retries, and
instance-management all come from that shared core, so behavior matches the .NET, Python, and Java
Durable providers. Durable work items flow over a single gRPC channel instead of the legacy
out-of-process HTTP protocol -- the same consolidation `durabletask-python` shipped as
`azure-functions-durable` 2.0.0b1. It supersedes the legacy `durable-functions` v3
([Azure/azure-functions-durable-js](https://github.com/Azure/azure-functions-durable-js)). Classic
`context.df.*` orchestrators and entities keep working through a compatibility layer, but several
surfaces changed (see below).

### Requirements

- **Node.js >= 22** (v3 supported Node 18 and 20; those are no longer supported).
- **Functions extension bundle** -- GA `Microsoft.Azure.Functions.ExtensionBundle` `[4.36.0, 5.0.0)`
or Preview `Microsoft.Azure.Functions.ExtensionBundle.Preview` `[4.29.0, 5.0.0)`. The provider needs
a durable extension that starts a gRPC server for Node
([Azure/azure-functions-durable-extension#3260](https://github.com/Azure/azure-functions-durable-extension/pull/3260));
on an earlier bundle the app starts normally but every orchestration starter hangs and times out
after ~60 s with no error. A fresh app on the default GA range works -- the trap is an explicit pin
at or below 4.32.0. GA and Preview version numbers are NOT comparable across feeds.

### Underlying packages

- `@microsoft/durabletask-js` `0.4.0` (exact pin).
- `@azure/functions` `^4.16.1`.
- `@azure/identity` `^4.0.0` as an **optional** peer dependency -- needed only for a `callHttp`
`tokenSource`, and not installed automatically.

### Breaking changes from durable-functions v3

- **Node.js >= 22** is required.
- **Classic contexts no longer extend `InvocationContext`** -- only `df` plus replay-safe log helpers
are available (the classic entity context is just `{ df }`).
- **Task result shape follows the core SDK** -- use `isComplete` / `isFailed` / `getResult()` instead
of v3's `isCompleted` / `isFaulted` / `result`. `context.df.createTimer(...)` still returns a
cancelable `TimerTask`.
- **Entity locking moved to the core context.** `context.df.lock` / `context.df.isLocked` and the
`DurableLock` / `LockState` / `LockingRulesViolationError` exports are removed and will **not** be
restored. Acquire locks with `context.entities.lockEntities(...)` (returns a `LockHandle` with
`release()`) and query with `context.entities.isInCriticalSection()`.
- **A plain non-generator classic orchestrator is no longer supported.** A synchronous,
single-argument, non-generator function is now treated as core-native and receives the core
`OrchestrationContext` (which has no `.df`); sync generators (`function*`) using `context.df.*` are
unaffected.
- **Some v3 top-level exports were removed** -- `DummyOrchestrationContext`, `DummyEntityContext`, and
the entity-lock types above. `TaskFailedError` is re-exported from the core SDK, and aggregate
failures now surface as JS-native `AggregateError`; use the core `TestOrchestrationWorker` /
`TestOrchestrationClient` for orchestration unit tests.

### Added

- **`context.df.callHttp(...)` is restored** as a worker-side durable HTTP call. It accepts the v3
`CallHttpOptions` (`method`, `url`, `body`, `headers`, `tokenSource`, `enablePolling`) and returns a
`Task<DurableHttpResponse>` (`{ statusCode, headers, content }`), including automatic `202 Accepted`
polling that honors `Retry-After` via durable timers. Caveats:
- **Trust-boundary change** -- in v3 the Functions **host** extension ran the request; here it runs
as a durable **activity inside your worker** (via `fetch`), so egress path, source identity, and
firewall/VNet rules follow the worker process -- re-verify IP allow-lists.
- **Cross-origin `202` poll credentials are stripped** -- when the callee-controlled `Location`
points to a different origin, the poll drops `Authorization`, `Cookie`, and the `tokenSource`;
`x-functions-key` is **always** dropped; same-origin polls still forward headers and the
`tokenSource`. Mirrors the .NET extension
([Azure/azure-functions-durable-extension#3443](https://github.com/Azure/azure-functions-durable-extension/pull/3443)).
- **The initial request follows redirects with `fetch` defaults**, which drop `Authorization` /
`Cookie` across origins but **not** custom credential headers like `x-functions-key`.
- **Default poll interval is 30 s** when a `202` carries no usable `Retry-After`.
- **Known incompatibility** -- a body on a `GET`/`HEAD` request throws (the Fetch Standard forbids
it, though v3, the .NET extension, and Python all send it).
- **Known incompatibility** -- `DurableHttpResponse` is a plain object, not a class, so
`response.getHeader(name)` is unavailable and existing calls **fail at runtime**; index
`response.headers[...]` by lower-cased key instead.
- A managed-identity `tokenSource` needs the optional `@azure/identity` package or it throws a clear
error.
- **`app.client.*` starter helpers** (`http`, `timer`, `storageBlob`, `storageQueue`,
`serviceBusQueue`, `serviceBusTopic`, `eventHub`, `eventGrid`, `cosmosDB`, `generic`) register a
normal trigger and inject the client as the handler's second argument, so
`(trigger, client, context)` works without manually wiring `df.input.durableClient()` +
`df.getClient(context)`.
- **`client.startNew()` supports the `version` option.**
- **`client.getStatus()` keeps the v3 shape** -- a non-optional `DurableOrchestrationStatus` that
throws when the instance is missing; `showInput` suppresses only the top-level input, `showHistory`
populates `history`, and `showHistoryOutput` toggles the per-entry input/result payloads; `history`
entries are core `HistoryEvent`s (v3 typed it `Array<unknown>`).

### Known limitations

- This is a preview release; the API surface may change before `4.0.0`.
18 changes: 16 additions & 2 deletions packages/azure-functions-durable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ changed:
`context.df.isLocked()` and the `DurableLock` / `LockState` / `LockingRulesViolationError` exports
are removed. Acquire locks with the core `context.entities.lockEntities(...entityIds)` (returns a
`LockHandle` — call `release()`, ideally in a `finally`) and query with
`context.entities.isInCriticalSection()`. Restoring the v3 `df.lock` / `isLocked` surface is tracked
in [#317](https://github.com/microsoft/durabletask-js/issues/317).
`context.entities.isInCriticalSection()`. The v3 `df.lock` / `isLocked` surface is **not supported
and not planned** — there is no tracking issue.
Comment on lines +45 to +46
- **`context.df.callHttp(...)` is restored** as a worker-side durable HTTP call
([#318](https://github.com/microsoft/durabletask-js/issues/318)) — though **not** as a drop-in, fully
v3-equivalent replacement: the known incompatibilities and behavior differences listed below are
Expand Down Expand Up @@ -106,6 +106,20 @@ changed:
sync **generators** (`function*`) using `context.df.*` — are unaffected; convert any non-generator
classic orchestrator to generator form, or to the core-native `ctx.*` API.

## Requirements

This provider reaches the Durable Task backend over the Functions host's **gRPC** channel, which
exists only in newer durable-extension builds. Your app's `host.json` must reference one of:

- GA bundle — `Microsoft.Azure.Functions.ExtensionBundle` at **`[4.36.0, 5.0.0)`**, or
- Preview bundle — `Microsoft.Azure.Functions.ExtensionBundle.Preview` at **`[4.29.0, 5.0.0)`**.

Earlier GA v4 bundles (**<= 4.32.0**) predate that gRPC endpoint, so orchestration starters **hang
for ~60 seconds and time out with no error**. A fresh app on the default GA range (`[4.*, 5.0.0)`)
resolves to the latest GA (>= 4.36.0) and works — the trap is an **explicit** pin at or below
4.32.0. GA and Preview are independent feeds whose version numbers are **not comparable** (a higher
Preview number does not imply the GA bundle contains the same code).

## Getting started

```typescript
Expand Down
Loading