From 6868e139bc083ec0abaf2b54ac3d49467df220fe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:13:32 -0800 Subject: [PATCH 01/18] Add samples and sample validation ci --- .github/workflows/validate-samples.yaml | 221 ++++++++++++++ examples/azure-managed/README.md | 91 ++++++ .../hello-orchestrations/README.md | 67 +++++ .../hello-orchestrations/index.ts | 184 ++++++++++++ .../hello-orchestrations/sample.json | 5 + .../azure-managed/human-interaction/README.md | 63 ++++ .../azure-managed/human-interaction/index.ts | 208 +++++++++++++ .../human-interaction/sample.json | 5 + .../lifecycle-management/README.md | 78 +++++ .../lifecycle-management/index.ts | 196 ++++++++++++ .../lifecycle-management/sample.json | 5 + .../azure-managed/query-and-history/README.md | 74 +++++ .../azure-managed/query-and-history/index.ts | 209 +++++++++++++ .../query-and-history/sample.json | 5 + .../retry-and-error-handling/README.md | 68 +++++ .../retry-and-error-handling/index.ts | 248 ++++++++++++++++ .../retry-and-error-handling/sample.json | 5 + examples/azure-managed/sample.json | 6 + examples/azure-managed/unit-testing/README.md | 68 +++++ examples/azure-managed/unit-testing/index.ts | 279 ++++++++++++++++++ .../azure-managed/unit-testing/sample.json | 5 + examples/azure-managed/versioning/README.md | 62 ++++ examples/azure-managed/versioning/index.ts | 196 ++++++++++++ examples/azure-managed/versioning/sample.json | 5 + test/e2e-azuremanaged/retry-handler.spec.ts | 10 +- 25 files changed, 2358 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/validate-samples.yaml create mode 100644 examples/azure-managed/hello-orchestrations/README.md create mode 100644 examples/azure-managed/hello-orchestrations/index.ts create mode 100644 examples/azure-managed/hello-orchestrations/sample.json create mode 100644 examples/azure-managed/human-interaction/README.md create mode 100644 examples/azure-managed/human-interaction/index.ts create mode 100644 examples/azure-managed/human-interaction/sample.json create mode 100644 examples/azure-managed/lifecycle-management/README.md create mode 100644 examples/azure-managed/lifecycle-management/index.ts create mode 100644 examples/azure-managed/lifecycle-management/sample.json create mode 100644 examples/azure-managed/query-and-history/README.md create mode 100644 examples/azure-managed/query-and-history/index.ts create mode 100644 examples/azure-managed/query-and-history/sample.json create mode 100644 examples/azure-managed/retry-and-error-handling/README.md create mode 100644 examples/azure-managed/retry-and-error-handling/index.ts create mode 100644 examples/azure-managed/retry-and-error-handling/sample.json create mode 100644 examples/azure-managed/sample.json create mode 100644 examples/azure-managed/unit-testing/README.md create mode 100644 examples/azure-managed/unit-testing/index.ts create mode 100644 examples/azure-managed/unit-testing/sample.json create mode 100644 examples/azure-managed/versioning/README.md create mode 100644 examples/azure-managed/versioning/index.ts create mode 100644 examples/azure-managed/versioning/sample.json diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml new file mode 100644 index 0000000..af3d8ef --- /dev/null +++ b/.github/workflows/validate-samples.yaml @@ -0,0 +1,221 @@ +name: ๐Ÿงช Validate Samples + +# Validates all samples under examples/azure-managed/ on PRs and main pushes. +# Samples are auto-discovered: any subfolder containing a sample.json is treated as a sample. +# The "unit-testing" sample runs without emulator; emulator-dependent samples use Docker. + +on: + push: + branches: [main] + paths: + - "examples/**" + - "packages/**" + - "package.json" + - "tsconfig.base.json" + - ".github/workflows/validate-samples.yaml" + pull_request: + branches: [main] + paths: + - "examples/**" + - "packages/**" + - "package.json" + - "tsconfig.base.json" + - ".github/workflows/validate-samples.yaml" + +permissions: + contents: read + +jobs: + # ----------------------------------------------------------------------- + # 1. Discover all samples dynamically + # ----------------------------------------------------------------------- + discover: + runs-on: ubuntu-latest + outputs: + # JSON arrays of sample directory names + emulator-samples: ${{ steps.find.outputs.emulator }} + no-emulator-samples: ${{ steps.find.outputs.no_emulator }} + steps: + - uses: actions/checkout@v4 + + - name: ๐Ÿ” Discover samples via sample.json + id: find + run: | + SAMPLES_ROOT="examples/azure-managed" + + # Find all sample.json files in subdirectories (not the root sample.json) + emulator_samples="[]" + no_emulator_samples="[]" + + for sample_json in $(find "$SAMPLES_ROOT" -mindepth 2 -name "sample.json" | sort); do + dir=$(dirname "$sample_json") + name=$(basename "$dir") + requires_emulator=$(jq -r '.requiresEmulator // true' "$sample_json") + + echo "Found sample: $name (requiresEmulator=$requires_emulator)" + + if [ "$requires_emulator" = "false" ]; then + no_emulator_samples=$(echo "$no_emulator_samples" | jq --arg n "$name" '. + [$n]') + else + emulator_samples=$(echo "$emulator_samples" | jq --arg n "$name" '. + [$n]') + fi + done + + echo "emulator=$emulator_samples" >> "$GITHUB_OUTPUT" + echo "no_emulator=$no_emulator_samples" >> "$GITHUB_OUTPUT" + + echo "--- Emulator samples ---" + echo "$emulator_samples" | jq . + echo "--- No-emulator samples ---" + echo "$no_emulator_samples" | jq . + + # ----------------------------------------------------------------------- + # 2. Build the SDK (shared by all sample jobs) + # ----------------------------------------------------------------------- + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: ["22.x"] + steps: + - uses: actions/checkout@v4 + + - name: โš™๏ธ Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: ๐Ÿ“ฆ Install dependencies + run: npm ci + + - name: ๐Ÿ”จ Build SDK + run: npm run build + + - name: ๐Ÿ“ Cache build output + uses: actions/cache/save@v4 + with: + path: | + node_modules + packages/*/dist + packages/*/node_modules + key: sdk-build-${{ github.sha }}-node${{ matrix.node-version }} + + # ----------------------------------------------------------------------- + # 3. Run samples that DON'T need the emulator (e.g., unit-testing) + # ----------------------------------------------------------------------- + samples-no-emulator: + needs: [discover, build] + if: needs.discover.outputs.no-emulator-samples != '[]' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sample: ${{ fromJson(needs.discover.outputs.no-emulator-samples) }} + node-version: ["22.x"] + steps: + - uses: actions/checkout@v4 + + - name: โš™๏ธ Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: ๐Ÿ“ฆ Restore build cache + uses: actions/cache/restore@v4 + with: + path: | + node_modules + packages/*/dist + packages/*/node_modules + key: sdk-build-${{ github.sha }}-node${{ matrix.node-version }} + + - name: ๐Ÿงช Run sample โ€” ${{ matrix.sample }} + run: | + echo "Running sample: ${{ matrix.sample }}" + npx ts-node --swc ./examples/azure-managed/${{ matrix.sample }}/index.ts + timeout-minutes: 2 + + # ----------------------------------------------------------------------- + # 4. Run samples that need the DTS emulator (Docker) + # ----------------------------------------------------------------------- + samples-with-emulator: + needs: [discover, build] + if: needs.discover.outputs.emulator-samples != '[]' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sample: ${{ fromJson(needs.discover.outputs.emulator-samples) }} + node-version: ["22.x"] + + services: + dts-emulator: + image: mcr.microsoft.com/dts/dts-emulator:latest + ports: + - 8080:8080 + - 8082:8082 + options: >- + --health-cmd "curl -sf http://localhost:8082/ || exit 1" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default" + + steps: + - uses: actions/checkout@v4 + + - name: โš™๏ธ Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: ๐Ÿ“ฆ Restore build cache + uses: actions/cache/restore@v4 + with: + path: | + node_modules + packages/*/dist + packages/*/node_modules + key: sdk-build-${{ github.sha }}-node${{ matrix.node-version }} + + - name: โณ Wait for DTS emulator + run: | + echo "Waiting for DTS emulator to be ready..." + for i in $(seq 1 30); do + if curl -sf http://localhost:8082/ > /dev/null 2>&1; then + echo "DTS emulator is ready!" + break + fi + echo " Attempt $i/30 โ€” waiting 2s..." + sleep 2 + done + + - name: ๐Ÿงช Run sample โ€” ${{ matrix.sample }} + run: | + echo "Running sample: ${{ matrix.sample }}" + npx ts-node --swc ./examples/azure-managed/${{ matrix.sample }}/index.ts + timeout-minutes: 5 + + # ----------------------------------------------------------------------- + # 5. Summary gate โ€” all samples must pass + # ----------------------------------------------------------------------- + samples-gate: + needs: [samples-no-emulator, samples-with-emulator] + if: always() + runs-on: ubuntu-latest + steps: + - name: โœ… Check results + run: | + echo "No-emulator result: ${{ needs.samples-no-emulator.result }}" + echo "Emulator result: ${{ needs.samples-with-emulator.result }}" + + if [[ "${{ needs.samples-no-emulator.result }}" == "failure" ]] || \ + [[ "${{ needs.samples-with-emulator.result }}" == "failure" ]]; then + echo "โŒ Some samples failed!" + exit 1 + fi + + echo "โœ… All samples passed!" diff --git a/examples/azure-managed/README.md b/examples/azure-managed/README.md index 4045a13..0a8bec0 100644 --- a/examples/azure-managed/README.md +++ b/examples/azure-managed/README.md @@ -1,3 +1,94 @@ +# Azure Managed Durable Task Scheduler โ€” Samples + +Runnable samples demonstrating every major feature of the Durable Task JavaScript SDK with Azure Managed DTS. + +## Sample Index + +| Sample | Scenario | Key Features | Emulator Required | +|--------|----------|-------------|-------------------| +| [hello-orchestrations](hello-orchestrations/) | Core patterns | Activity sequence, fan-out/fan-in, sub-orchestrations, `whenAny`, deterministic GUID | Yes | +| [retry-and-error-handling](retry-and-error-handling/) | Fault tolerance | `RetryPolicy`, `handleFailure`, `AsyncRetryHandler`, sub-orchestration retry, `raiseIfFailed()` | Yes | +| [human-interaction](human-interaction/) | Event-driven workflows | External events, timers, `whenAny` race, `sendEvent`, custom status | Yes | +| [lifecycle-management](lifecycle-management/) | Orchestration control | Terminate (recursive), suspend/resume, restart, continue-as-new, purge, tags | Yes | +| [query-and-history](query-and-history/) | Monitoring & debugging | `getAllInstances`, pagination, `listInstanceIds`, `getOrchestrationHistory`, typed events | Yes | +| [versioning](versioning/) | Safe deployments | Version match strategies, failure strategies, `ctx.version`, `ctx.compareVersionTo()` | Yes | +| [unit-testing](unit-testing/) | Testing without infra | `InMemoryOrchestrationBackend`, `TestOrchestrationClient`, `TestOrchestrationWorker`, `ReplaySafeLogger` | **No** | +| [index.ts](index.ts) | Azure-managed basics | Connection strings, `DefaultAzureCredential`, `createAzureManagedClient` | Yes | +| [distributed-tracing.ts](distributed-tracing.ts) | OpenTelemetry tracing | `NodeSDK`, OTLP export, Jaeger, `DurableTaskAzureManagedClientBuilder` | Yes | + +### Quick Start + +```bash +npm install && npm run build # build SDK +cd examples/azure-managed && docker compose up -d # start emulator +cp .env.emulator .env # configure +cd ../.. +npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts +``` + +See each sample's README for details. See [Feature Coverage Map](#feature-coverage-map) below for full feature mapping. + +### CI Validation + +Samples are validated automatically by [`.github/workflows/validate-samples.yaml`](../../.github/workflows/validate-samples.yaml). Any subfolder with a `sample.json` is auto-discovered and tested on every PR. + +To add a new sample: create a subfolder with `sample.json`, `index.ts`, and `README.md`. CI picks it up automatically. + +### Running All Samples Locally + +```bash +for dir in examples/azure-managed/*/; do + if [ -f "$dir/sample.json" ] && [ -f "$dir/index.ts" ]; then + echo "--- Running $(basename $dir) ---" + npx ts-node --swc "$dir/index.ts" + fi +done +``` + +### Feature Coverage Map + +| Feature | Sample(s) | +|---------|-----------| +| `ctx.callActivity()` | hello-orchestrations, retry-and-error-handling | +| `whenAll()` | hello-orchestrations, unit-testing | +| `whenAny()` | hello-orchestrations, human-interaction | +| `ctx.callSubOrchestrator()` | hello-orchestrations, retry-and-error-handling, lifecycle-management | +| `ctx.newGuid()` | hello-orchestrations | +| `ctx.waitForExternalEvent()` | human-interaction, unit-testing | +| `client.raiseOrchestrationEvent()` | human-interaction, unit-testing | +| `ctx.createTimer()` | human-interaction, query-and-history, unit-testing | +| `ctx.sendEvent()` | human-interaction | +| `ctx.setCustomStatus()` | human-interaction | +| `RetryPolicy` | retry-and-error-handling | +| `handleFailure` predicate | retry-and-error-handling | +| `AsyncRetryHandler` | retry-and-error-handling | +| `state.raiseIfFailed()` | retry-and-error-handling | +| `terminateOrchestration()` | lifecycle-management, unit-testing | +| `terminateOptions()` (recursive) | lifecycle-management | +| `suspendOrchestration()` / `resumeOrchestration()` | lifecycle-management, unit-testing | +| `continueAsNew()` | lifecycle-management, unit-testing | +| `restartOrchestration()` | lifecycle-management | +| `purgeOrchestration()` | lifecycle-management | +| Orchestration tags | lifecycle-management | +| `getAllInstances()` / pagination | query-and-history | +| `listInstanceIds()` | query-and-history | +| `getOrchestrationHistory()` | query-and-history | +| Typed `HistoryEvent` | query-and-history | +| `VersionMatchStrategy` | versioning | +| `VersionFailureStrategy` | versioning | +| `ctx.version` / `ctx.compareVersionTo()` | versioning | +| `InMemoryOrchestrationBackend` | unit-testing | +| `TestOrchestrationClient/Worker` | unit-testing | +| `ReplaySafeLogger` | unit-testing | +| `NoOpLogger` | unit-testing | +| Connection strings | index.ts, all samples | +| `DefaultAzureCredential` | index.ts | +| `createAzureLogger()` | index.ts | +| Distributed tracing (OTel) | distributed-tracing.ts | +| `ConsoleLogger` | all samples | + +--- + # Distributed Tracing with Azure Managed Durable Task Scheduler This example demonstrates **OpenTelemetry distributed tracing** with the Durable Task JavaScript SDK and Azure Managed Durable Task Scheduler (DTS). Traces are exported to [Jaeger](https://www.jaegertracing.io/) so you can visualize the full orchestration lifecycle as connected spans. diff --git a/examples/azure-managed/hello-orchestrations/README.md b/examples/azure-managed/hello-orchestrations/README.md new file mode 100644 index 0000000..fb2209e --- /dev/null +++ b/examples/azure-managed/hello-orchestrations/README.md @@ -0,0 +1,67 @@ +# Hello Orchestrations + +Demonstrates the five fundamental orchestration patterns every Durable Task developer needs. + +## Features Covered + +| Feature | API | +|---------|-----| +| Activity sequence | `ctx.callActivity()` in a loop | +| Fan-out/fan-in | `whenAll()` with parallel `callActivity()` | +| Sub-orchestrations | `ctx.callSubOrchestrator()` | +| Race pattern | `whenAny()` | +| Deterministic GUID | `ctx.newGuid()` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +# From the repository root +cd examples/azure-managed +docker compose up -d # start DTS emulator +cp .env.emulator .env # configure for local emulator +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts +``` + +## Expected Output + +``` +=== 1. Activity Sequence === +Result: [1,2,3,4,5,6] + +=== 2. Fan-out/Fan-in === +Result: {"items":4,"totalChars":24} + +=== 3. Sub-orchestrations === +Result: {"result1":12,"result2":22} + +=== 4. whenAny (Race) === +Result: {"winnerResult":5} + +=== 5. Deterministic GUID === +Result: {"guid1":"","guid2":"","areDifferent":true} + +=== All orchestrations completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts 2>&1 | grep "All orchestrations completed successfully" +``` + +## Troubleshooting + +- **Connection refused**: Ensure the DTS emulator is running (`docker compose up -d` from the `examples/azure-managed` directory). +- **Worker timeout**: The emulator may need a few seconds to start. Retry the command. diff --git a/examples/azure-managed/hello-orchestrations/index.ts b/examples/azure-managed/hello-orchestrations/index.ts new file mode 100644 index 0000000..fd217c8 --- /dev/null +++ b/examples/azure-managed/hello-orchestrations/index.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates core orchestration patterns with the Azure Managed +// Durable Task Scheduler (DTS): +// 1. Activity sequence โ€” call activities one after another +// 2. Fan-out/fan-in โ€” run activities in parallel, aggregate results +// 3. Sub-orchestrations โ€” compose orchestrators hierarchically +// 4. whenAny โ€” race multiple tasks, use winner's result +// 5. Deterministic GUID โ€” generate replay-safe unique IDs + +import * as dotenv from "dotenv"; +import * as path from "path"; +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, + ConsoleLogger, +} from "@microsoft/durabletask-js-azuremanaged"; +import { ActivityContext } from "@microsoft/durabletask-js/dist/task/context/activity-context"; +import { OrchestrationContext } from "@microsoft/durabletask-js/dist/task/context/orchestration-context"; +import { TOrchestrator } from "@microsoft/durabletask-js/dist/types/orchestrator.type"; +import { whenAll, whenAny } from "@microsoft/durabletask-js/dist/task"; + +// --------------------------------------------------------------------------- +// Activities +// --------------------------------------------------------------------------- + +/** Add one to the input. */ +const plusOne = async (_ctx: ActivityContext, input: number): Promise => { + return input + 1; +}; + +/** Simulate work with variable latency. Returns item length. */ +const processItem = async (_ctx: ActivityContext, item: string): Promise => { + console.log(` [processItem] Processing "${item}"`); + return item.length; +}; + +/** Child orchestration: doubles each number via an activity. */ +const doubleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, value: number): any { + const doubled: number = yield ctx.callActivity(plusOne, value); + // Call plusOne again so we get value + 2 (close to doubling for small ints) + const result: number = yield ctx.callActivity(plusOne, doubled); + return result; +}; + +// --------------------------------------------------------------------------- +// Orchestrators +// --------------------------------------------------------------------------- + +/** 1. Activity Sequence โ€” calls plusOne 5 times in a loop. */ +const activitySequence: TOrchestrator = async function* (ctx: OrchestrationContext, startVal: number): any { + let current = startVal; + const numbers = [current]; + + for (let i = 0; i < 5; i++) { + current = yield ctx.callActivity(plusOne, current); + numbers.push(current); + } + + return numbers; // [1, 2, 3, 4, 5, 6] +}; + +/** 2. Fan-out/fan-in โ€” processes items in parallel, sums results. */ +const fanOutFanIn: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const items = ["alpha", "bravo", "charlie", "delta"]; + + // Fan-out: schedule all activities in parallel + const tasks = items.map((item) => ctx.callActivity(processItem, item)); + + // Fan-in: wait for all to complete + const lengths: number[] = yield whenAll(tasks); + + return { items: items.length, totalChars: lengths.reduce((a, b) => a + b, 0) }; +}; + +/** 3. Sub-orchestrations โ€” calls doubleOrchestrator as a child. */ +const parentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const result1: number = yield ctx.callSubOrchestrator(doubleOrchestrator, 10); + const result2: number = yield ctx.callSubOrchestrator(doubleOrchestrator, 20); + return { result1, result2 }; +}; + +/** 4. whenAny โ€” race two activities, return the first result. */ +const raceOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const taskA = ctx.callActivity(processItem, "short"); + const taskB = ctx.callActivity(processItem, "a-longer-item"); + + const winner = yield whenAny([taskA, taskB]); + + // winner is the task that completed first + return { winnerResult: winner.getResult() }; +}; + +/** 5. Deterministic GUID โ€” generates replay-safe unique IDs. */ +const guidOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + // Call an activity so the generator has at least one yield point + const label: string = yield ctx.callActivity(greet, "GUID demo"); + const guid1 = ctx.newGuid(); + const guid2 = ctx.newGuid(); + + // These GUIDs are deterministic: same values across replays + return { label, guid1, guid2, areDifferent: guid1 !== guid2 }; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + const logger = new ConsoleLogger(); + const connectionString = + process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING || + "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + + const client = new DurableTaskAzureManagedClientBuilder() + .connectionString(connectionString) + .logger(logger) + .build(); + + const worker = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .addOrchestrator(activitySequence) + .addOrchestrator(fanOutFanIn) + .addOrchestrator(parentOrchestrator) + .addOrchestrator(doubleOrchestrator) + .addOrchestrator(raceOrchestrator) + .addOrchestrator(guidOrchestrator) + .addActivity(plusOne) + .addActivity(processItem) + .build(); + + try { + await worker.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // --- 1. Activity Sequence --- + console.log("\n=== 1. Activity Sequence ==="); + const seqId = await client.scheduleNewOrchestration(activitySequence, 1); + const seqState = await client.waitForOrchestrationCompletion(seqId, true, 30); + console.log(`Result: ${seqState?.serializedOutput}`); + // Expected: [1,2,3,4,5,6] + + // --- 2. Fan-out/Fan-in --- + console.log("\n=== 2. Fan-out/Fan-in ==="); + const fanId = await client.scheduleNewOrchestration(fanOutFanIn); + const fanState = await client.waitForOrchestrationCompletion(fanId, true, 30); + console.log(`Result: ${fanState?.serializedOutput}`); + // Expected: {"items":4,"totalChars":24} + + // --- 3. Sub-orchestrations --- + console.log("\n=== 3. Sub-orchestrations ==="); + const subId = await client.scheduleNewOrchestration(parentOrchestrator); + const subState = await client.waitForOrchestrationCompletion(subId, true, 30); + console.log(`Result: ${subState?.serializedOutput}`); + // Expected: {"result1":12,"result2":22} + + // --- 4. whenAny (Race) --- + console.log("\n=== 4. whenAny (Race) ==="); + const raceId = await client.scheduleNewOrchestration(raceOrchestrator); + const raceState = await client.waitForOrchestrationCompletion(raceId, true, 30); + console.log(`Result: ${raceState?.serializedOutput}`); + // Expected: {"winnerResult":} + + // --- 5. Deterministic GUID --- + console.log("\n=== 5. Deterministic GUID ==="); + const guidId = await client.scheduleNewOrchestration(guidOrchestrator); + const guidState = await client.waitForOrchestrationCompletion(guidId, true, 30); + console.log(`Result: ${guidState?.serializedOutput}`); + // Expected: {"guid1":"","guid2":"","areDifferent":true} + + console.log("\n=== All orchestrations completed successfully! ==="); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } finally { + await worker.stop(); + await client.stop(); + process.exit(0); + } +})(); diff --git a/examples/azure-managed/hello-orchestrations/sample.json b/examples/azure-managed/hello-orchestrations/sample.json new file mode 100644 index 0000000..4b437df --- /dev/null +++ b/examples/azure-managed/hello-orchestrations/sample.json @@ -0,0 +1,5 @@ +{ + "name": "hello-orchestrations", + "description": "Core orchestration patterns: activity sequences, fan-out/fan-in, sub-orchestrations, whenAny, and deterministic GUIDs", + "requiresEmulator": true +} diff --git a/examples/azure-managed/human-interaction/README.md b/examples/azure-managed/human-interaction/README.md new file mode 100644 index 0000000..382a588 --- /dev/null +++ b/examples/azure-managed/human-interaction/README.md @@ -0,0 +1,63 @@ +# Human Interaction + +Demonstrates event-driven orchestration patterns for workflows that involve human actors or external systems. + +## Features Covered + +| Feature | API | +|---------|-----| +| External events | `ctx.waitForExternalEvent()`, `client.raiseOrchestrationEvent()` | +| Durable timers | `ctx.createTimer()` | +| Race pattern | `whenAny()` โ€” event vs timer | +| Custom status | `ctx.setCustomStatus()` | +| Orchestration-to-orchestration events | `ctx.sendEvent()` | +| Multiple events | Sequential `waitForExternalEvent()` calls | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +cd examples/azure-managed +docker compose up -d +cp .env.emulator .env +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/human-interaction/index.ts +``` + +## Expected Output + +``` +=== 1. Approval Workflow (client sends approval) === +Orchestration started: +Custom status: {"stage":"Awaiting approval","requestId":"REQ-..."} +Sent approval event from client +Result: "Order placed" + +=== 2. Approval Workflow (timeout โ€” no event sent) === +Result: "Timed out โ€” auto-rejected" + +=== 3. sendEvent (orchestration โ†’ orchestration) === +Target result: "Order placed" +Notifier result: "Sent approval to " + +=== 4. Multiple External Events === +Result: {"event1":"Hello","event2":"World"} + +=== All human-interaction demos completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/human-interaction/index.ts 2>&1 | grep "All human-interaction demos completed successfully" +``` diff --git a/examples/azure-managed/human-interaction/index.ts b/examples/azure-managed/human-interaction/index.ts new file mode 100644 index 0000000..fd86b05 --- /dev/null +++ b/examples/azure-managed/human-interaction/index.ts @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates event-driven orchestration patterns: +// 1. External events โ€” wait for human approval via waitForExternalEvent +// 2. Timer-based timeout โ€” race approval against a deadline with whenAny +// 3. Custom status โ€” publish orchestration progress via setCustomStatus +// 4. sendEvent โ€” one orchestration sends an event to another +// 5. Multiple external events โ€” wait for several events in sequence + +import * as dotenv from "dotenv"; +import * as path from "path"; +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, + ConsoleLogger, +} from "@microsoft/durabletask-js-azuremanaged"; +import { + OrchestrationContext, + ActivityContext, + TOrchestrator, + whenAny, +} from "@microsoft/durabletask-js"; + +// --------------------------------------------------------------------------- +// Activities +// --------------------------------------------------------------------------- + +const submitRequest = async (_ctx: ActivityContext, request: { amount: number }): Promise => { + console.log(` [submitRequest] Purchase request submitted: $${request.amount}`); + return `REQ-${Date.now()}`; +}; + +const processApproval = async (_ctx: ActivityContext, data: { requestId: string; approved: boolean }): Promise => { + console.log(` [processApproval] Request ${data.requestId}: ${data.approved ? "APPROVED" : "REJECTED"}`); + return data.approved ? "Order placed" : "Order cancelled"; +}; + +const notifyTimeout = async (_ctx: ActivityContext, requestId: string): Promise => { + console.log(` [notifyTimeout] Request ${requestId} timed out โ€” auto-rejected`); + return "Timed out โ€” auto-rejected"; +}; + +// --------------------------------------------------------------------------- +// Orchestrators +// --------------------------------------------------------------------------- + +/** + * 1 & 2 & 3. Approval workflow with timeout and custom status. + * + * - Submits a purchase request + * - Sets custom status to "Awaiting approval" + * - Waits for an external "approval" event OR a 10-second timer + * - Uses whenAny to race the event against the timer + * - Updates custom status based on outcome + */ +const approvalOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, amount: number): any { + // Step 1: Submit the request + const requestId: string = yield ctx.callActivity(submitRequest, { amount }); + ctx.setCustomStatus({ stage: "Awaiting approval", requestId }); + + // Step 2: Race external event vs timer + const approvalEvent = ctx.waitForExternalEvent<{ approved: boolean }>("approval"); + const timeout = ctx.createTimer(10_000); // 10 seconds + + const winner = yield whenAny([approvalEvent, timeout]); + + let result: string; + if (winner === approvalEvent) { + // Human responded in time + const decision = approvalEvent.getResult(); + ctx.setCustomStatus({ stage: "Processing", requestId, approved: decision.approved }); + result = yield ctx.callActivity(processApproval, { requestId, approved: decision.approved }); + } else { + // Timer fired first โ€” timed out + ctx.setCustomStatus({ stage: "Timed out", requestId }); + result = yield ctx.callActivity(notifyTimeout, requestId); + } + + ctx.setCustomStatus({ stage: "Completed", requestId }); + return result; +}; + +/** + * 4. sendEvent โ€” one orchestration sends an event to another. + * + * This "notifier" orchestration sends an approval event to a target + * orchestration identified by its instance ID (passed as input). + */ +const notifierOrchestrator: TOrchestrator = async function* ( + ctx: OrchestrationContext, + targetInstanceId: string, +): any { + // Wait a moment before sending (simulate some processing) + yield ctx.createTimer(1_000); + + // Send approval event to the target orchestration + ctx.sendEvent(targetInstanceId, "approval", { approved: true }); + + return `Sent approval to ${targetInstanceId}`; +}; + +/** + * 5. Multiple external events โ€” waits for two events in sequence. + */ +const multiEventOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + ctx.setCustomStatus("Waiting for event 1"); + const event1: string = yield ctx.waitForExternalEvent("event1"); + + ctx.setCustomStatus("Waiting for event 2"); + const event2: string = yield ctx.waitForExternalEvent("event2"); + + ctx.setCustomStatus("All events received"); + return { event1, event2 }; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + const logger = new ConsoleLogger(); + const connectionString = + process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING || + "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + + const client = new DurableTaskAzureManagedClientBuilder() + .connectionString(connectionString) + .logger(logger) + .build(); + + const worker = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .addOrchestrator(approvalOrchestrator) + .addOrchestrator(notifierOrchestrator) + .addOrchestrator(multiEventOrchestrator) + .addActivity(submitRequest) + .addActivity(processApproval) + .addActivity(notifyTimeout) + .build(); + + try { + await worker.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // --- 1. Approval with client-raised event (approved) --- + console.log("\n=== 1. Approval Workflow (client sends approval) ==="); + const approvalId = await client.scheduleNewOrchestration(approvalOrchestrator, 500); + console.log(`Orchestration started: ${approvalId}`); + + // Wait for it to reach "Awaiting approval", then send approval from client + await new Promise((r) => setTimeout(r, 3000)); + const midState = await client.getOrchestrationState(approvalId, true); + console.log(`Custom status: ${midState?.serializedCustomStatus}`); + + await client.raiseOrchestrationEvent(approvalId, "approval", { approved: true }); + console.log("Sent approval event from client"); + + const finalState = await client.waitForOrchestrationCompletion(approvalId, true, 30); + console.log(`Result: ${finalState?.serializedOutput}`); + console.log(`Final custom status: ${finalState?.serializedCustomStatus}`); + + // --- 2. Approval with timeout (no event sent) --- + console.log("\n=== 2. Approval Workflow (timeout โ€” no event sent) ==="); + const timeoutId = await client.scheduleNewOrchestration(approvalOrchestrator, 1000); + // Don't send any event โ€” let the timer expire + const timeoutState = await client.waitForOrchestrationCompletion(timeoutId, true, 30); + console.log(`Result: ${timeoutState?.serializedOutput}`); + + // --- 3. sendEvent (orchestration-to-orchestration) --- + console.log("\n=== 3. sendEvent (orchestration โ†’ orchestration) ==="); + const targetId = await client.scheduleNewOrchestration(approvalOrchestrator, 250); + console.log(`Target orchestration: ${targetId}`); + + // Start a notifier that will send the approval event + const notifierId = await client.scheduleNewOrchestration(notifierOrchestrator, targetId); + console.log(`Notifier orchestration: ${notifierId}`); + + const targetState = await client.waitForOrchestrationCompletion(targetId, true, 30); + console.log(`Target result: ${targetState?.serializedOutput}`); + + const notifierState = await client.waitForOrchestrationCompletion(notifierId, true, 30); + console.log(`Notifier result: ${notifierState?.serializedOutput}`); + + // --- 4. Multiple external events --- + console.log("\n=== 4. Multiple External Events ==="); + const multiId = await client.scheduleNewOrchestration(multiEventOrchestrator); + await new Promise((r) => setTimeout(r, 2000)); + + await client.raiseOrchestrationEvent(multiId, "event1", "Hello"); + await client.raiseOrchestrationEvent(multiId, "event2", "World"); + + const multiState = await client.waitForOrchestrationCompletion(multiId, true, 30); + console.log(`Result: ${multiState?.serializedOutput}`); + + console.log("\n=== All human-interaction demos completed successfully! ==="); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } finally { + await worker.stop(); + await client.stop(); + process.exit(0); + } +})(); diff --git a/examples/azure-managed/human-interaction/sample.json b/examples/azure-managed/human-interaction/sample.json new file mode 100644 index 0000000..091a9f4 --- /dev/null +++ b/examples/azure-managed/human-interaction/sample.json @@ -0,0 +1,5 @@ +{ + "name": "human-interaction", + "description": "External events, timers, sendEvent (orchestration-to-orchestration), custom status, and approval patterns", + "requiresEmulator": true +} diff --git a/examples/azure-managed/lifecycle-management/README.md b/examples/azure-managed/lifecycle-management/README.md new file mode 100644 index 0000000..4859402 --- /dev/null +++ b/examples/azure-managed/lifecycle-management/README.md @@ -0,0 +1,78 @@ +# Lifecycle Management + +Demonstrates all orchestration lifecycle operations: terminate, suspend/resume, restart, continue-as-new, purge, and tags. + +## Features Covered + +| Feature | API | +|---------|-----| +| Terminate | `client.terminateOrchestration()` | +| Terminate with output | `terminateOptions({ output })` | +| Recursive terminate | `terminateOptions({ recursive: true })` | +| Suspend | `client.suspendOrchestration()` | +| Resume | `client.resumeOrchestration()` | +| Continue-as-new | `ctx.continueAsNew()` | +| Restart | `client.restartOrchestration()` | +| Purge | `client.purgeOrchestration()` | +| Tags | `scheduleNewOrchestration(..., { tags })` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +cd examples/azure-managed +docker compose up -d +cp .env.emulator .env +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/lifecycle-management/index.ts +``` + +## Expected Output + +``` +=== 1. Terminate (with output) === +Status: TERMINATED +Output: "Cancelled by admin" + +=== 2. Terminate (recursive โ€” parent + child) === +Parent status: TERMINATED + +=== 3. Suspend / Resume === +After suspend: SUSPENDED +After resume: RUNNING +Final result: "Completed normally" + +=== 4. Continue-as-new === +Status: COMPLETED +Result: {"status":"all batches done","batchNum":3,"processed":9} + +=== 5. Restart Orchestration === +Restarted as new ID: +Result: "Done: original-run" + +=== 6. Purge Orchestration === +Purged instances: 1 +State after purge: undefined (deleted) + +=== 7. Orchestration Tags === +Tags: {"environment":"staging","owner":"demo-user","priority":"high"} +Result: "Done: tagged-run" + +=== All lifecycle demos completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/lifecycle-management/index.ts 2>&1 | grep "All lifecycle demos completed successfully" +``` diff --git a/examples/azure-managed/lifecycle-management/index.ts b/examples/azure-managed/lifecycle-management/index.ts new file mode 100644 index 0000000..f94a6eb --- /dev/null +++ b/examples/azure-managed/lifecycle-management/index.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates orchestration lifecycle management: +// 1. Terminate โ€” cancel a running orchestration (with output) +// 2. Terminate (recursive) โ€” cancel parent + child orchestrations +// 3. Suspend / Resume โ€” pause and unpause an orchestration +// 4. Continue-as-new โ€” restart an orchestration with new input +// 5. Restart โ€” re-run a completed orchestration +// 6. Purge โ€” delete orchestration history +// 7. Tags โ€” attach metadata to orchestrations + +import * as dotenv from "dotenv"; +import * as path from "path"; +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, + ConsoleLogger, +} from "@microsoft/durabletask-js-azuremanaged"; +import { + OrchestrationContext, + ActivityContext, + TOrchestrator, + OrchestrationStatus, + terminateOptions, +} from "@microsoft/durabletask-js"; + +// --------------------------------------------------------------------------- +// Activities +// --------------------------------------------------------------------------- + +const doWork = async (_ctx: ActivityContext, label: string): Promise => { + console.log(` [doWork] Processing: ${label}`); + return `Done: ${label}`; +}; + +// --------------------------------------------------------------------------- +// Orchestrators +// --------------------------------------------------------------------------- + +/** Long-running orchestration (waits for an event, useful for terminate/suspend demos). */ +const longRunning: TOrchestrator = async function* (ctx: OrchestrationContext): any { + yield ctx.callActivity(doWork, "step-1"); + // Wait for an event that may never come โ€” keeps orchestration alive + const signal: string = yield ctx.waitForExternalEvent("proceed"); + yield ctx.callActivity(doWork, `step-2-${signal}`); + return "Completed normally"; +}; + +/** Parent orchestration with a child (for recursive terminate demo). */ +const parentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const childResult: string = yield ctx.callSubOrchestrator(longRunning); + return `Parent got: ${childResult}`; +}; + +/** + * Continue-as-new orchestration โ€” processes a batch, then restarts with new input. + * Stops after processing 3 batches total. + */ +const batchProcessor: TOrchestrator = async function* ( + ctx: OrchestrationContext, + state: { batchNum: number; processed: number }, +): any { + const batchSize = 3; + const maxBatches = 3; + + yield ctx.callActivity(doWork, `batch-${state.batchNum}`); + const newState = { batchNum: state.batchNum + 1, processed: state.processed + batchSize }; + + if (newState.batchNum >= maxBatches) { + return { status: "all batches done", ...newState }; + } + + // Continue-as-new: restart with updated state (no history accumulation) + ctx.continueAsNew(newState); +}; + +/** Simple orchestration for restart/purge demos. */ +const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: string): any { + const result: string = yield ctx.callActivity(doWork, input); + return result; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + const logger = new ConsoleLogger(); + const connectionString = + process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING || + "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + + const client = new DurableTaskAzureManagedClientBuilder() + .connectionString(connectionString) + .logger(logger) + .build(); + + const worker = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .addOrchestrator(longRunning) + .addOrchestrator(parentOrchestrator) + .addOrchestrator(batchProcessor) + .addOrchestrator(simpleOrchestrator) + .addActivity(doWork) + .build(); + + try { + await worker.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // --- 1. Terminate with output --- + console.log("\n=== 1. Terminate (with output) ==="); + const termId = await client.scheduleNewOrchestration(longRunning); + await client.waitForOrchestrationStart(termId); + await client.terminateOrchestration(termId, terminateOptions({ output: "Cancelled by admin" })); + const termState = await client.waitForOrchestrationCompletion(termId, true, 15); + console.log(`Status: ${OrchestrationStatus[termState!.runtimeStatus]}`); + console.log(`Output: ${termState?.serializedOutput}`); + + // --- 2. Terminate (recursive) --- + console.log("\n=== 2. Terminate (recursive โ€” parent + child) ==="); + const parentId = await client.scheduleNewOrchestration(parentOrchestrator); + await new Promise((r) => setTimeout(r, 3000)); // let child start + await client.terminateOrchestration(parentId, terminateOptions({ output: "Force stop", recursive: true })); + const parentState = await client.waitForOrchestrationCompletion(parentId, true, 15); + console.log(`Parent status: ${OrchestrationStatus[parentState!.runtimeStatus]}`); + + // --- 3. Suspend / Resume --- + console.log("\n=== 3. Suspend / Resume ==="); + const suspId = await client.scheduleNewOrchestration(longRunning); + await client.waitForOrchestrationStart(suspId); + + await client.suspendOrchestration(suspId); + let suspState = await client.getOrchestrationState(suspId); + console.log(`After suspend: ${OrchestrationStatus[suspState!.runtimeStatus]}`); + + await client.resumeOrchestration(suspId); + suspState = await client.getOrchestrationState(suspId); + console.log(`After resume: ${OrchestrationStatus[suspState!.runtimeStatus]}`); + + // Send event so it completes, then clean up + await client.raiseOrchestrationEvent(suspId, "proceed", "resumed"); + const suspFinal = await client.waitForOrchestrationCompletion(suspId, true, 15); + console.log(`Final result: ${suspFinal?.serializedOutput}`); + + // --- 4. Continue-as-new --- + console.log("\n=== 4. Continue-as-new ==="); + const canId = await client.scheduleNewOrchestration(batchProcessor, { batchNum: 0, processed: 0 }); + const canState = await client.waitForOrchestrationCompletion(canId, true, 30); + console.log(`Status: ${OrchestrationStatus[canState!.runtimeStatus]}`); + console.log(`Result: ${canState?.serializedOutput}`); + + // --- 5. Restart --- + console.log("\n=== 5. Restart Orchestration ==="); + const origId = await client.scheduleNewOrchestration(simpleOrchestrator, "original-run"); + await client.waitForOrchestrationCompletion(origId, true, 15); + console.log(`Original completed: ${origId}`); + + const restartedId = await client.restartOrchestration(origId, true); // new instance ID + const restartState = await client.waitForOrchestrationCompletion(restartedId, true, 15); + console.log(`Restarted as new ID: ${restartedId}`); + console.log(`Result: ${restartState?.serializedOutput}`); + + // --- 6. Purge --- + console.log("\n=== 6. Purge Orchestration ==="); + const purgeId = await client.scheduleNewOrchestration(simpleOrchestrator, "to-be-purged"); + await client.waitForOrchestrationCompletion(purgeId, true, 15); + + const purgeResult = await client.purgeOrchestration(purgeId); + console.log(`Purged instances: ${purgeResult?.deletedInstanceCount}`); + + const purgedState = await client.getOrchestrationState(purgeId); + console.log(`State after purge: ${purgedState ?? "undefined (deleted)"}`); + + // --- 7. Tags --- + console.log("\n=== 7. Orchestration Tags ==="); + const tags = { environment: "staging", owner: "demo-user", priority: "high" }; + const tagId = await client.scheduleNewOrchestration(simpleOrchestrator, "tagged-run", { tags }); + const tagState = await client.waitForOrchestrationCompletion(tagId, true, 15); + console.log(`Tags: ${JSON.stringify(tagState?.tags)}`); + console.log(`Result: ${tagState?.serializedOutput}`); + + console.log("\n=== All lifecycle demos completed successfully! ==="); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } finally { + await worker.stop(); + await client.stop(); + process.exit(0); + } +})(); diff --git a/examples/azure-managed/lifecycle-management/sample.json b/examples/azure-managed/lifecycle-management/sample.json new file mode 100644 index 0000000..49b7b1b --- /dev/null +++ b/examples/azure-managed/lifecycle-management/sample.json @@ -0,0 +1,5 @@ +{ + "name": "lifecycle-management", + "description": "Terminate, suspend/resume, restart, continue-as-new, purge, and orchestration tags", + "requiresEmulator": true +} diff --git a/examples/azure-managed/query-and-history/README.md b/examples/azure-managed/query-and-history/README.md new file mode 100644 index 0000000..c12eff6 --- /dev/null +++ b/examples/azure-managed/query-and-history/README.md @@ -0,0 +1,74 @@ +# Query and History + +Demonstrates the query and history inspection APIs for monitoring and debugging orchestrations. + +## Features Covered + +| Feature | API | +|---------|-----| +| Query instances | `client.getAllInstances(query)` | +| Page-by-page iteration | `pageable.asPages()` | +| Item-by-item iteration | `for await (const item of pageable)` | +| Query filters | `OrchestrationQuery` (status, time range, pageSize) | +| List instance IDs | `client.listInstanceIds()` | +| Cursor pagination | `continuationToken` / `lastInstanceKey` | +| Fetch history | `client.getOrchestrationHistory()` | +| Typed history events | `HistoryEventType`, typed event interfaces | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +cd examples/azure-managed +docker compose up -d +cp .env.emulator .env +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/query-and-history/index.ts +``` + +## Expected Output + +``` +Creating orchestration instances... +Created 6 orchestration instances. + +=== 1. getAllInstances (query with filters) === + Page 1: 3 instances (hasMore: true) + - [simpleOrchestrator] = "Done: item-1" + ... + Total instances across pages: 6 + +=== 2. listInstanceIds (cursor pagination) === + Page 1: 3 IDs (hasMore: true) + Page 2: 3 IDs (hasMore: ...) + +=== 3. getOrchestrationHistory === + History for : N events + [0] ExecutionStarted @ ... + [1] TimerCreated @ ... + ... + +=== 4. Typed History Event Inspection === + ExecutionStarted: name=richOrchestrator, input="World" + TaskScheduled events: 1 + TaskCompleted events: 1 + TimerCreated events: 1 + +=== All query/history demos completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/query-and-history/index.ts 2>&1 | grep "All query/history demos completed successfully" +``` diff --git a/examples/azure-managed/query-and-history/index.ts b/examples/azure-managed/query-and-history/index.ts new file mode 100644 index 0000000..1693a60 --- /dev/null +++ b/examples/azure-managed/query-and-history/index.ts @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates the query and history inspection APIs: +// 1. getAllInstances โ€” query orchestration instances with filters and pagination +// 2. listInstanceIds โ€” list instance IDs with cursor-based pagination +// 3. getOrchestrationHistory โ€” retrieve detailed history events +// 4. Typed history events โ€” inspect individual event types + +import * as dotenv from "dotenv"; +import * as path from "path"; +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, + ConsoleLogger, +} from "@microsoft/durabletask-js-azuremanaged"; +import { + OrchestrationContext, + ActivityContext, + TOrchestrator, + OrchestrationStatus, + HistoryEventType, +} from "@microsoft/durabletask-js"; +import type { + OrchestrationQuery, + HistoryEvent, + ExecutionStartedEvent, + TaskScheduledEvent, + TaskCompletedEvent, + TimerCreatedEvent, +} from "@microsoft/durabletask-js"; + +// --------------------------------------------------------------------------- +// Activities & Orchestrators +// --------------------------------------------------------------------------- + +const greet = async (_ctx: ActivityContext, name: string): Promise => { + return `Hello, ${name}!`; +}; + +/** Creates a timer and calls an activity โ€” produces rich history. */ +const richOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, name: string): any { + // Create a short timer + yield ctx.createTimer(500); + + // Call an activity + const greeting: string = yield ctx.callActivity(greet, name); + + return greeting; +}; + +/** Simple orchestrator used to create multiple instances for querying. */ +const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, label: string): any { + yield ctx.callActivity(greet, label); + return `Done: ${label}`; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + const logger = new ConsoleLogger(); + const connectionString = + process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING || + "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + + const client = new DurableTaskAzureManagedClientBuilder() + .connectionString(connectionString) + .logger(logger) + .build(); + + const worker = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .addOrchestrator(richOrchestrator) + .addOrchestrator(simpleOrchestrator) + .addActivity(greet) + .build(); + + try { + await worker.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // Create several orchestration instances for querying + console.log("Creating orchestration instances..."); + const instanceIds: string[] = []; + + for (let i = 1; i <= 5; i++) { + const id = await client.scheduleNewOrchestration(simpleOrchestrator, `item-${i}`); + instanceIds.push(id); + } + + // Also create the rich orchestrator for history inspection + const richId = await client.scheduleNewOrchestration(richOrchestrator, "World"); + + // Wait for all to complete + for (const id of [...instanceIds, richId]) { + await client.waitForOrchestrationCompletion(id, false, 30); + } + console.log(`Created ${instanceIds.length + 1} orchestration instances.`); + + // --- 1. getAllInstances with filters --- + console.log("\n=== 1. getAllInstances (query with filters) ==="); + + // Query completed instances + const query: OrchestrationQuery = { + statuses: [OrchestrationStatus.COMPLETED], + fetchInputsAndOutputs: true, + pageSize: 3, // small page size to demonstrate pagination + }; + + const pageable = client.getAllInstances(query); + + // Iterate page by page + let pageNum = 0; + let totalInstances = 0; + for await (const page of pageable.asPages()) { + pageNum++; + console.log(` Page ${pageNum}: ${page.values.length} instances (hasMore: ${page.hasMoreResults})`); + for (const instance of page.values) { + totalInstances++; + console.log(` - ${instance.instanceId} [${instance.name}] = ${instance.serializedOutput?.substring(0, 50)}`); + } + if (pageNum >= 3) break; // safety limit + } + console.log(`Total instances across pages: ${totalInstances}`); + + // Iterate item by item + console.log("\n Item-by-item iteration (first 5):"); + const pageable2 = client.getAllInstances({ statuses: [OrchestrationStatus.COMPLETED], pageSize: 10 }); + let itemCount = 0; + for await (const instance of pageable2) { + itemCount++; + console.log(` ${itemCount}. ${instance.instanceId} [${instance.name}]`); + if (itemCount >= 5) break; + } + + // --- 2. listInstanceIds --- + console.log("\n=== 2. listInstanceIds (cursor pagination) ==="); + const page1 = await client.listInstanceIds({ pageSize: 3 }); + console.log(` Page 1: ${page1.values.length} IDs (hasMore: ${page1.hasMoreResults})`); + for (const id of page1.values) { + console.log(` - ${id}`); + } + + if (page1.hasMoreResults && page1.continuationToken) { + const page2 = await client.listInstanceIds({ + pageSize: 3, + lastInstanceKey: page1.continuationToken, + }); + console.log(` Page 2: ${page2.values.length} IDs (hasMore: ${page2.hasMoreResults})`); + for (const id of page2.values) { + console.log(` - ${id}`); + } + } + + // --- 3. getOrchestrationHistory --- + console.log("\n=== 3. getOrchestrationHistory ==="); + const history: HistoryEvent[] = await client.getOrchestrationHistory(richId); + console.log(` History for ${richId}: ${history.length} events`); + for (const event of history) { + const eventTypeName = HistoryEventType[event.eventType] || `Unknown(${event.eventType})`; + console.log(` [${event.eventId}] ${eventTypeName} @ ${event.timestamp?.toISOString()}`); + } + + // --- 4. Typed history events --- + console.log("\n=== 4. Typed History Event Inspection ==="); + + const executionStarted = history.find( + (e) => e.eventType === HistoryEventType.ExecutionStarted, + ) as ExecutionStartedEvent | undefined; + if (executionStarted) { + console.log(` ExecutionStarted: name=${executionStarted.name}, input=${executionStarted.input}`); + } + + const taskScheduled = history.filter( + (e) => e.eventType === HistoryEventType.TaskScheduled, + ) as TaskScheduledEvent[]; + console.log(` TaskScheduled events: ${taskScheduled.length}`); + for (const ts of taskScheduled) { + console.log(` - name=${ts.name}, input=${ts.input}`); + } + + const taskCompleted = history.filter( + (e) => e.eventType === HistoryEventType.TaskCompleted, + ) as TaskCompletedEvent[]; + console.log(` TaskCompleted events: ${taskCompleted.length}`); + + const timerCreated = history.filter( + (e) => e.eventType === HistoryEventType.TimerCreated, + ) as TimerCreatedEvent[]; + console.log(` TimerCreated events: ${timerCreated.length}`); + for (const tc of timerCreated) { + console.log(` - fireAt=${tc.fireAt?.toISOString()}`); + } + + console.log("\n=== All query/history demos completed successfully! ==="); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } finally { + await worker.stop(); + await client.stop(); + process.exit(0); + } +})(); diff --git a/examples/azure-managed/query-and-history/sample.json b/examples/azure-managed/query-and-history/sample.json new file mode 100644 index 0000000..a839514 --- /dev/null +++ b/examples/azure-managed/query-and-history/sample.json @@ -0,0 +1,5 @@ +{ + "name": "query-and-history", + "description": "Query orchestration instances, pagination, filters, and history event inspection", + "requiresEmulator": true +} diff --git a/examples/azure-managed/retry-and-error-handling/README.md b/examples/azure-managed/retry-and-error-handling/README.md new file mode 100644 index 0000000..7c71900 --- /dev/null +++ b/examples/azure-managed/retry-and-error-handling/README.md @@ -0,0 +1,68 @@ +# Retry and Error Handling + +Demonstrates every retry mechanism and error handling pattern available in the Durable Task SDK. + +## Features Covered + +| Feature | API | +|---------|-----| +| Declarative retry | `RetryPolicy` with exponential backoff | +| Failure predicate | `RetryPolicy.handleFailure` | +| Custom retry handler | `AsyncRetryHandler` with custom delays | +| Sub-orchestration retry | `callSubOrchestrator` with retry options | +| Error inspection | `state.raiseIfFailed()`, `state.failureDetails` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +cd examples/azure-managed +docker compose up -d +cp .env.emulator .env +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/retry-and-error-handling/index.ts +``` + +## Expected Output + +``` +=== 1. RetryPolicy (exponential backoff) === +Status: COMPLETED +Result: "Success on attempt 3 for \"retry-policy-...\"" + +=== 2. handleFailure Predicate === +Status: COMPLETED +Result: "Success on attempt 3 for \"handle-failure-...\"" + +=== 3. Custom AsyncRetryHandler === +Status: COMPLETED +Result: "Success on attempt 3 for \"custom-handler-...\"" + +=== 4. Sub-orchestration Retry === +Status: COMPLETED +Result: {"sum":15} + +=== 5. Error Handling (raiseIfFailed) === +Status: FAILED +raiseIfFailed() threw: OrchestrationFailedError โ€” ... +Failure type: Error +Failure message: FatalError: This operation cannot succeed + +=== All retry/error demos completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/retry-and-error-handling/index.ts 2>&1 | grep "All retry/error demos completed successfully" +``` diff --git a/examples/azure-managed/retry-and-error-handling/index.ts b/examples/azure-managed/retry-and-error-handling/index.ts new file mode 100644 index 0000000..965159a --- /dev/null +++ b/examples/azure-managed/retry-and-error-handling/index.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates retry and error handling in the Durable Task SDK: +// 1. RetryPolicy โ€” declarative retry with exponential backoff +// 2. handleFailure predicate โ€” selectively retry based on error type +// 3. AsyncRetryHandler โ€” imperative retry logic with custom delays +// 4. Sub-orchestration retry โ€” retry an entire sub-orchestration +// 5. raiseIfFailed() โ€” convenient error checking on orchestration state + +import * as dotenv from "dotenv"; +import * as path from "path"; +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, + ConsoleLogger, +} from "@microsoft/durabletask-js-azuremanaged"; +import { + OrchestrationContext, + ActivityContext, + TOrchestrator, + RetryPolicy, + OrchestrationStatus, +} from "@microsoft/durabletask-js"; +import type { AsyncRetryHandler, RetryContext, TaskRetryOptions } from "@microsoft/durabletask-js"; + +// --------------------------------------------------------------------------- +// Activities +// --------------------------------------------------------------------------- + +// Track call counts across retries (per activity name + instance) +const callCounts = new Map(); + +/** Activity that fails the first N-1 times, then succeeds on the Nth call. */ +const unreliableActivity = async (_ctx: ActivityContext, input: { key: string; failCount: number }): Promise => { + const count = (callCounts.get(input.key) ?? 0) + 1; + callCounts.set(input.key, count); + + if (count <= input.failCount) { + throw new Error(`TransientError: attempt ${count}/${input.failCount + 1} for "${input.key}"`); + } + + return `Success on attempt ${count} for "${input.key}"`; +}; + +/** Activity that always fails with a specific error type. */ +const permanentFailure = async (_ctx: ActivityContext, errorType: string): Promise => { + throw new Error(`${errorType}: This operation cannot succeed`); +}; + +/** Simple activity for sub-orchestration retry demo. */ +const addNumbers = async (_ctx: ActivityContext, nums: number[]): Promise => { + return nums.reduce((a, b) => a + b, 0); +}; + +// --------------------------------------------------------------------------- +// Orchestrators +// --------------------------------------------------------------------------- + +/** + * 1. Declarative RetryPolicy with exponential backoff. + * The activity fails twice then succeeds on the 3rd attempt. + */ +const retryPolicyOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const retryPolicy = new RetryPolicy({ + maxNumberOfAttempts: 5, + firstRetryIntervalInMilliseconds: 500, + backoffCoefficient: 2.0, + maxRetryIntervalInMilliseconds: 5000, + }); + + const result: string = yield ctx.callActivity( + unreliableActivity, + { key: `retry-policy-${ctx.instanceId}`, failCount: 2 }, + { retry: retryPolicy }, + ); + + return result; +}; + +/** + * 2. RetryPolicy with handleFailure predicate. + * Only retries TransientError; stops immediately on PermanentError. + */ +const handleFailureOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const retryPolicy = new RetryPolicy({ + maxNumberOfAttempts: 5, + firstRetryIntervalInMilliseconds: 200, + handleFailure: (failure) => { + // Only retry if the error message contains "TransientError" + return failure.message?.includes("TransientError") ?? false; + }, + }); + + // This should succeed โ€” fails twice with TransientError then succeeds + const result: string = yield ctx.callActivity( + unreliableActivity, + { key: `handle-failure-${ctx.instanceId}`, failCount: 2 }, + { retry: retryPolicy }, + ); + + return result; +}; + +/** + * 3. AsyncRetryHandler โ€” custom imperative retry logic. + * Implements a custom delay strategy: 100ms, 200ms, 400ms, then gives up. + */ +const customRetryHandlerOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const maxAttempts = 4; + + const customRetryHandler: AsyncRetryHandler = async (retryCtx: RetryContext) => { + if (retryCtx.lastAttemptNumber >= maxAttempts) { + return false; // give up + } + // Exponential delay: 100ms, 200ms, 400ms + return 100 * Math.pow(2, retryCtx.lastAttemptNumber - 1); + }; + + const result: string = yield ctx.callActivity( + unreliableActivity, + { key: `custom-handler-${ctx.instanceId}`, failCount: 2 }, + { retry: customRetryHandler as TaskRetryOptions }, + ); + + return result; +}; + +/** + * 4. Sub-orchestration with retry โ€” retries the entire child orchestration. + */ +const childOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: number[]): any { + const sum: number = yield ctx.callActivity(addNumbers, input); + return sum; +}; + +const parentWithRetryOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const retryPolicy = new RetryPolicy({ + maxNumberOfAttempts: 3, + firstRetryIntervalInMilliseconds: 200, + }); + + const result: number = yield ctx.callSubOrchestrator( + childOrchestrator, + [1, 2, 3, 4, 5], + { retry: retryPolicy }, + ); + + return { sum: result }; +}; + +/** + * 5. Error handling โ€” demonstrates raiseIfFailed() and failed state inspection. + */ +const alwaysFailOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + yield ctx.callActivity(permanentFailure, "FatalError"); + return "unreachable"; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + const logger = new ConsoleLogger(); + const connectionString = + process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING || + "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + + const client = new DurableTaskAzureManagedClientBuilder() + .connectionString(connectionString) + .logger(logger) + .build(); + + const worker = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .addOrchestrator(retryPolicyOrchestrator) + .addOrchestrator(handleFailureOrchestrator) + .addOrchestrator(customRetryHandlerOrchestrator) + .addOrchestrator(childOrchestrator) + .addOrchestrator(parentWithRetryOrchestrator) + .addOrchestrator(alwaysFailOrchestrator) + .addActivity(unreliableActivity) + .addActivity(permanentFailure) + .addActivity(addNumbers) + .build(); + + try { + await worker.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // --- 1. RetryPolicy with exponential backoff --- + console.log("\n=== 1. RetryPolicy (exponential backoff) ==="); + const id1 = await client.scheduleNewOrchestration(retryPolicyOrchestrator); + const state1 = await client.waitForOrchestrationCompletion(id1, true, 60); + console.log(`Status: ${OrchestrationStatus[state1!.runtimeStatus]}`); + console.log(`Result: ${state1?.serializedOutput}`); + + // --- 2. handleFailure predicate --- + console.log("\n=== 2. handleFailure Predicate ==="); + const id2 = await client.scheduleNewOrchestration(handleFailureOrchestrator); + const state2 = await client.waitForOrchestrationCompletion(id2, true, 60); + console.log(`Status: ${OrchestrationStatus[state2!.runtimeStatus]}`); + console.log(`Result: ${state2?.serializedOutput}`); + + // --- 3. Custom AsyncRetryHandler --- + console.log("\n=== 3. Custom AsyncRetryHandler ==="); + const id3 = await client.scheduleNewOrchestration(customRetryHandlerOrchestrator); + const state3 = await client.waitForOrchestrationCompletion(id3, true, 60); + console.log(`Status: ${OrchestrationStatus[state3!.runtimeStatus]}`); + console.log(`Result: ${state3?.serializedOutput}`); + + // --- 4. Sub-orchestration with retry --- + console.log("\n=== 4. Sub-orchestration Retry ==="); + const id4 = await client.scheduleNewOrchestration(parentWithRetryOrchestrator); + const state4 = await client.waitForOrchestrationCompletion(id4, true, 60); + console.log(`Status: ${OrchestrationStatus[state4!.runtimeStatus]}`); + console.log(`Result: ${state4?.serializedOutput}`); + + // --- 5. Error handling with raiseIfFailed() --- + console.log("\n=== 5. Error Handling (raiseIfFailed) ==="); + const id5 = await client.scheduleNewOrchestration(alwaysFailOrchestrator); + const state5 = await client.waitForOrchestrationCompletion(id5, true, 60); + console.log(`Status: ${OrchestrationStatus[state5!.runtimeStatus]}`); + try { + state5?.raiseIfFailed(); + console.log("ERROR: raiseIfFailed() should have thrown!"); + } catch (e: any) { + console.log(`raiseIfFailed() threw: ${e.constructor.name} โ€” ${e.message.substring(0, 80)}`); + } + if (state5?.failureDetails) { + console.log(`Failure type: ${state5.failureDetails.errorType}`); + console.log(`Failure message: ${state5.failureDetails.errorMessage?.substring(0, 80)}`); + } + + console.log("\n=== All retry/error demos completed successfully! ==="); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } finally { + await worker.stop(); + await client.stop(); + process.exit(0); + } +})(); diff --git a/examples/azure-managed/retry-and-error-handling/sample.json b/examples/azure-managed/retry-and-error-handling/sample.json new file mode 100644 index 0000000..5a46a92 --- /dev/null +++ b/examples/azure-managed/retry-and-error-handling/sample.json @@ -0,0 +1,5 @@ +{ + "name": "retry-and-error-handling", + "description": "Retry policies, custom retry handlers, error handling, and failure predicates", + "requiresEmulator": true +} diff --git a/examples/azure-managed/sample.json b/examples/azure-managed/sample.json new file mode 100644 index 0000000..80098a7 --- /dev/null +++ b/examples/azure-managed/sample.json @@ -0,0 +1,6 @@ +{ + "name": "azure-managed-basics", + "description": "Basic Azure-managed DTS usage with connection strings and DefaultAzureCredential", + "requiresEmulator": true, + "entrypoint": "index.ts" +} diff --git a/examples/azure-managed/unit-testing/README.md b/examples/azure-managed/unit-testing/README.md new file mode 100644 index 0000000..b7b0b68 --- /dev/null +++ b/examples/azure-managed/unit-testing/README.md @@ -0,0 +1,68 @@ +# Unit Testing + +Demonstrates how to unit-test orchestrations using the built-in in-memory testing framework โ€” **no Docker, no emulator, no network** required. + +## Features Covered + +| Feature | API | +|---------|-----| +| In-memory backend | `InMemoryOrchestrationBackend` | +| Test client | `TestOrchestrationClient` (same API as `TaskHubGrpcClient`) | +| Test worker | `TestOrchestrationWorker` (same API as `TaskHubGrpcWorker`) | +| Replay-safe logger | `ctx.createReplaySafeLogger()` | +| NoOp logger | `NoOpLogger` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- **No Docker required** โ€” everything runs in-process + +## Setup + +```bash +# From the repository root +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/unit-testing/index.ts +``` + +## Expected Output + +``` +=== Unit Testing with In-Memory Backend === + +--- Test Results --- + [PASS] Activity Sequence + [PASS] Fan-out/Fan-in + [PASS] Timer + [PASS] External Event + [PASS] Continue-as-new + [PASS] Terminate + [PASS] Suspend / Resume + [PASS] NoOpLogger + +8/8 tests passed. + +=== All unit-testing demos completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/unit-testing/index.ts 2>&1 | grep "All unit-testing demos completed successfully" +``` + +## When to Use + +Use the in-memory backend for: +- **Unit tests** โ€” fast, deterministic, no infrastructure +- **CI pipelines** โ€” runs anywhere without Docker +- **Local development** โ€” instant feedback loop + +Use the DTS Emulator for: +- **Integration tests** โ€” validates gRPC communication +- **End-to-end tests** โ€” tests full stack behavior diff --git a/examples/azure-managed/unit-testing/index.ts b/examples/azure-managed/unit-testing/index.ts new file mode 100644 index 0000000..868cac5 --- /dev/null +++ b/examples/azure-managed/unit-testing/index.ts @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates the in-memory testing framework for unit-testing +// orchestrations WITHOUT any external dependencies (no Docker, no emulator, no network). +// +// Features demonstrated: +// 1. InMemoryOrchestrationBackend โ€” full in-memory orchestration engine +// 2. TestOrchestrationClient โ€” same API as TaskHubGrpcClient +// 3. TestOrchestrationWorker โ€” same API as TaskHubGrpcWorker +// 4. ReplaySafeLogger โ€” suppress duplicate logs during orchestration replay +// 5. Testing all patterns: sequence, fan-out/fan-in, timers, events, continue-as-new + +import { + InMemoryOrchestrationBackend, + TestOrchestrationClient, + TestOrchestrationWorker, + OrchestrationContext, + ActivityContext, + TOrchestrator, + OrchestrationStatus, + whenAll, + ConsoleLogger, + NoOpLogger, +} from "@microsoft/durabletask-js"; + +// --------------------------------------------------------------------------- +// Application code (same code you'd use with the real TaskHubGrpcWorker) +// --------------------------------------------------------------------------- + +const addNumbers = async (_ctx: ActivityContext, nums: number[]): Promise => { + return nums.reduce((a, b) => a + b, 0); +}; + +const greet = async (_ctx: ActivityContext, name: string): Promise => { + return `Hello, ${name}!`; +}; + +/** Sequence: calls greet for each city. */ +const sequenceOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + // Demonstrate ReplaySafeLogger โ€” only logs when NOT replaying + const safeLogger = ctx.createReplaySafeLogger(new ConsoleLogger()); + safeLogger.info("Orchestration started (only printed once, not during replay)"); + + const cities = ["Tokyo", "London", "Paris"]; + const results: string[] = []; + + for (const city of cities) { + const greeting: string = yield ctx.callActivity(greet, city); + results.push(greeting); + } + + safeLogger.info("Orchestration finishing"); + return results; +}; + +/** Fan-out/fan-in: parallel sum. */ +const parallelSumOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const batches = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + const tasks = batches.map((batch) => ctx.callActivity(addNumbers, batch)); + const partialSums: number[] = yield whenAll(tasks); + return partialSums.reduce((a, b) => a + b, 0); // total = 45 +}; + +/** Timer: waits for a timer then returns. */ +const timerOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + yield ctx.createTimer(100); // 100ms timer + return "Timer fired"; +}; + +/** External event: waits for an event, returns its data. */ +const eventOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const data: string = yield ctx.waitForExternalEvent("myEvent"); + return `Received: ${data}`; +}; + +/** Continue-as-new: increments a counter until it reaches 3. */ +const counterOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, count: number): any { + if (count >= 3) { + return `Final count: ${count}`; + } + yield; // yield before continue-as-new + ctx.continueAsNew(count + 1); +}; + +// --------------------------------------------------------------------------- +// Test runner (lightweight, no framework needed) +// --------------------------------------------------------------------------- + +interface TestResult { + name: string; + passed: boolean; + error?: string; +} + +async function runTest( + name: string, + fn: (backend: InMemoryOrchestrationBackend, client: TestOrchestrationClient, worker: TestOrchestrationWorker) => Promise, +): Promise { + const backend = new InMemoryOrchestrationBackend(); + const client = new TestOrchestrationClient(backend); + const worker = new TestOrchestrationWorker(backend); + + try { + await fn(backend, client, worker); + await worker.stop(); + await client.stop(); + backend.reset(); + return { name, passed: true }; + } catch (error: any) { + await worker.stop(); + await client.stop(); + backend.reset(); + return { name, passed: false, error: error.message }; + } +} + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + console.log("=== Unit Testing with In-Memory Backend ===\n"); + + const results: TestResult[] = []; + + // Test 1: Activity Sequence + results.push( + await runTest("Activity Sequence", async (_backend, client, worker) => { + worker.addOrchestrator(sequenceOrchestrator); + worker.addActivity(greet); + await worker.start(); + + const id = await client.scheduleNewOrchestration(sequenceOrchestrator); + const state = await client.waitForOrchestrationCompletion(id, true, 10); + + assert(state !== undefined, "State should be defined"); + assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should be completed"); + const output = JSON.parse(state!.serializedOutput!); + assert(output.length === 3, "Should have 3 greetings"); + assert(output[0] === "Hello, Tokyo!", "First greeting should be Tokyo"); + }), + ); + + // Test 2: Fan-out/Fan-in + results.push( + await runTest("Fan-out/Fan-in", async (_backend, client, worker) => { + worker.addOrchestrator(parallelSumOrchestrator); + worker.addActivity(addNumbers); + await worker.start(); + + const id = await client.scheduleNewOrchestration(parallelSumOrchestrator); + const state = await client.waitForOrchestrationCompletion(id, true, 10); + + assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should be completed"); + assert(state!.serializedOutput === "45", `Sum should be 45, got ${state!.serializedOutput}`); + }), + ); + + // Test 3: Timer + results.push( + await runTest("Timer", async (_backend, client, worker) => { + worker.addOrchestrator(timerOrchestrator); + await worker.start(); + + const id = await client.scheduleNewOrchestration(timerOrchestrator); + const state = await client.waitForOrchestrationCompletion(id, true, 10); + + assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should be completed"); + assert(state!.serializedOutput === '"Timer fired"', "Should return timer result"); + }), + ); + + // Test 4: External Event + results.push( + await runTest("External Event", async (_backend, client, worker) => { + worker.addOrchestrator(eventOrchestrator); + await worker.start(); + + const id = await client.scheduleNewOrchestration(eventOrchestrator); + await client.waitForOrchestrationStart(id); + + // Raise the event + await client.raiseOrchestrationEvent(id, "myEvent", "test-data"); + + const state = await client.waitForOrchestrationCompletion(id, true, 10); + assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should be completed"); + assert(state!.serializedOutput === '"Received: test-data"', `Got: ${state!.serializedOutput}`); + }), + ); + + // Test 5: Continue-as-new + results.push( + await runTest("Continue-as-new", async (_backend, client, worker) => { + worker.addOrchestrator(counterOrchestrator); + await worker.start(); + + const id = await client.scheduleNewOrchestration(counterOrchestrator, 0); + const state = await client.waitForOrchestrationCompletion(id, true, 10); + + assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should be completed"); + assert(state!.serializedOutput === '"Final count: 3"', `Got: ${state!.serializedOutput}`); + }), + ); + + // Test 6: Terminate + results.push( + await runTest("Terminate", async (_backend, client, worker) => { + worker.addOrchestrator(eventOrchestrator); + await worker.start(); + + const id = await client.scheduleNewOrchestration(eventOrchestrator); + await client.waitForOrchestrationStart(id); + + await client.terminateOrchestration(id, "Cancelled"); + const state = await client.waitForOrchestrationCompletion(id, true, 10); + + assert(state!.runtimeStatus === OrchestrationStatus.TERMINATED, "Should be terminated"); + }), + ); + + // Test 7: Suspend / Resume + results.push( + await runTest("Suspend / Resume", async (_backend, client, worker) => { + worker.addOrchestrator(eventOrchestrator); + await worker.start(); + + const id = await client.scheduleNewOrchestration(eventOrchestrator); + await client.waitForOrchestrationStart(id); + + await client.suspendOrchestration(id); + let state = await client.getOrchestrationState(id); + assert(state!.runtimeStatus === OrchestrationStatus.SUSPENDED, "Should be suspended"); + + await client.resumeOrchestration(id); + await client.raiseOrchestrationEvent(id, "myEvent", "after-resume"); + state = await client.waitForOrchestrationCompletion(id, true, 10); + assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should complete after resume"); + }), + ); + + // Test 8: NoOpLogger (verify it doesn't throw) + results.push( + await runTest("NoOpLogger", async () => { + const logger = new NoOpLogger(); + logger.info("This should do nothing"); + logger.error("This too"); + logger.warn("And this"); + logger.debug("Silent"); + // If we get here without throwing, the test passes + }), + ); + + // --- Report --- + console.log("\n--- Test Results ---"); + let allPassed = true; + for (const result of results) { + const status = result.passed ? "PASS" : "FAIL"; + console.log(` [${status}] ${result.name}${result.error ? ` โ€” ${result.error}` : ""}`); + if (!result.passed) allPassed = false; + } + + const passCount = results.filter((r) => r.passed).length; + console.log(`\n${passCount}/${results.length} tests passed.`); + + if (allPassed) { + console.log("\n=== All unit-testing demos completed successfully! ==="); + process.exit(0); + } else { + console.error("\nSome tests failed!"); + process.exit(1); + } +})(); diff --git a/examples/azure-managed/unit-testing/sample.json b/examples/azure-managed/unit-testing/sample.json new file mode 100644 index 0000000..b0597d4 --- /dev/null +++ b/examples/azure-managed/unit-testing/sample.json @@ -0,0 +1,5 @@ +{ + "name": "unit-testing", + "description": "In-memory testing framework: TestOrchestrationClient, TestOrchestrationWorker, ReplaySafeLogger", + "requiresEmulator": false +} diff --git a/examples/azure-managed/versioning/README.md b/examples/azure-managed/versioning/README.md new file mode 100644 index 0000000..e55b6b0 --- /dev/null +++ b/examples/azure-managed/versioning/README.md @@ -0,0 +1,62 @@ +# Versioning + +Demonstrates orchestration versioning for safe side-by-side deployment of different orchestration versions. + +## Features Covered + +| Feature | API | +|---------|-----| +| Schedule with version | `scheduleNewOrchestration(..., { version: "1.0.0" })` | +| Version in orchestrator | `ctx.version` | +| Compare versions | `ctx.compareVersionTo("2.0.0")` | +| Strict matching | `VersionMatchStrategy.Strict` | +| Current-or-older matching | `VersionMatchStrategy.CurrentOrOlder` | +| Fail on mismatch | `VersionFailureStrategy.Fail` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +cd examples/azure-managed +docker compose up -d +cp .env.emulator .env +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/versioning/index.ts +``` + +## Expected Output + +``` +=== 1. Schedule Orchestration with Version === +v1.0.0 result: {"version":"1.0.0","result":"Processed: v1-logic (version=1.0.0)","comparedTo2":-1} +v2.5.0 result: {"version":"2.5.0","result":"Processed: v2-logic (version=2.5.0)","comparedTo2":1} + +=== 2. VersionMatchStrategy.Strict === +Exact match (v2.0.0): COMPLETED โ€” "Processed: version=2.0.0" +Mismatch (v1.0.0): FAILED + Failure: Version mismatch: ... + +=== 3. VersionMatchStrategy.CurrentOrOlder === +Older (v2.0.0): COMPLETED โ€” "Processed: version=2.0.0" +Same (v3.0.0): COMPLETED โ€” "Processed: version=3.0.0" +Newer (v4.0.0): FAILED + Failure: Version mismatch: ... + +=== All versioning demos completed successfully! === +``` + +## Smoke Test + +```bash +npm run example -- ./examples/azure-managed/versioning/index.ts 2>&1 | grep "All versioning demos completed successfully" +``` diff --git a/examples/azure-managed/versioning/index.ts b/examples/azure-managed/versioning/index.ts new file mode 100644 index 0000000..56f69da --- /dev/null +++ b/examples/azure-managed/versioning/index.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This sample demonstrates orchestration versioning: +// 1. Schedule orchestrations with a version +// 2. VersionMatchStrategy.Strict โ€” only exact version match processes +// 3. VersionMatchStrategy.CurrentOrOlder โ€” worker processes same or older versions +// 4. ctx.version and ctx.compareVersionTo() โ€” version-aware orchestration logic +// 5. defaultVersion on client โ€” auto-tag all orchestrations + +import * as dotenv from "dotenv"; +import * as path from "path"; +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +import { + DurableTaskAzureManagedClientBuilder, + DurableTaskAzureManagedWorkerBuilder, + ConsoleLogger, + VersionMatchStrategy, + VersionFailureStrategy, +} from "@microsoft/durabletask-js-azuremanaged"; +import { + OrchestrationContext, + ActivityContext, + TOrchestrator, + OrchestrationStatus, +} from "@microsoft/durabletask-js"; + +// --------------------------------------------------------------------------- +// Activities +// --------------------------------------------------------------------------- + +const doWork = async (_ctx: ActivityContext, label: string): Promise => { + return `Processed: ${label}`; +}; + +// --------------------------------------------------------------------------- +// Orchestrators +// --------------------------------------------------------------------------- + +/** Version-aware orchestrator โ€” behavior changes based on version. */ +const versionedOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const version = ctx.version; + const comparison = ctx.compareVersionTo("2.0.0"); + + let result: string; + if (comparison >= 0) { + // v2.0.0 or newer โ€” uses new logic + result = yield ctx.callActivity(doWork, `v2-logic (version=${version})`); + } else { + // Older than v2.0.0 โ€” uses legacy logic + result = yield ctx.callActivity(doWork, `v1-logic (version=${version})`); + } + + return { version, result, comparedTo2: comparison }; +}; + +/** Simple orchestrator used with different workers. */ +const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const result: string = yield ctx.callActivity(doWork, `version=${ctx.version}`); + return result; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +(async () => { + const logger = new ConsoleLogger(); + const connectionString = + process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING || + "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default"; + + // --- 1. Schedule with version --- + console.log("\n=== 1. Schedule Orchestration with Version ==="); + + // Client with a default version + const clientV1 = new DurableTaskAzureManagedClientBuilder() + .connectionString(connectionString) + .logger(logger) + .build(); + + // Worker with no versioning (MatchStrategy.None = processes everything) + const worker = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .addOrchestrator(versionedOrchestrator) + .addOrchestrator(simpleOrchestrator) + .addActivity(doWork) + .build(); + + try { + await worker.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // Schedule with explicit version + const id1 = await clientV1.scheduleNewOrchestration(versionedOrchestrator, undefined, { version: "1.0.0" }); + const state1 = await clientV1.waitForOrchestrationCompletion(id1, true, 30); + console.log(`v1.0.0 result: ${state1?.serializedOutput}`); + + const id2 = await clientV1.scheduleNewOrchestration(versionedOrchestrator, undefined, { version: "2.5.0" }); + const state2 = await clientV1.waitForOrchestrationCompletion(id2, true, 30); + console.log(`v2.5.0 result: ${state2?.serializedOutput}`); + + await worker.stop(); + } catch (error) { + console.error("Error in demo 1:", error); + await worker.stop(); + } + + // --- 2. Strict version matching --- + console.log("\n=== 2. VersionMatchStrategy.Strict ==="); + + const workerStrict = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .versioning({ + version: "2.0.0", + matchStrategy: VersionMatchStrategy.Strict, + failureStrategy: VersionFailureStrategy.Fail, + }) + .addOrchestrator(simpleOrchestrator) + .addActivity(doWork) + .build(); + + try { + await workerStrict.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // This one should match (version 2.0.0 == worker version 2.0.0) + const matchId = await clientV1.scheduleNewOrchestration(simpleOrchestrator, undefined, { version: "2.0.0" }); + const matchState = await clientV1.waitForOrchestrationCompletion(matchId, true, 30); + console.log(`Exact match (v2.0.0): ${OrchestrationStatus[matchState!.runtimeStatus]} โ€” ${matchState?.serializedOutput}`); + + // This one should fail (version 1.0.0 != worker version 2.0.0) + const mismatchId = await clientV1.scheduleNewOrchestration(simpleOrchestrator, undefined, { version: "1.0.0" }); + const mismatchState = await clientV1.waitForOrchestrationCompletion(mismatchId, true, 15); + console.log(`Mismatch (v1.0.0): ${OrchestrationStatus[mismatchState!.runtimeStatus]}`); + if (mismatchState?.failureDetails) { + console.log(` Failure: ${mismatchState.failureDetails.errorMessage?.substring(0, 80)}`); + } + + await workerStrict.stop(); + } catch (error) { + console.error("Error in demo 2:", error); + await workerStrict.stop(); + } + + // --- 3. CurrentOrOlder matching --- + console.log("\n=== 3. VersionMatchStrategy.CurrentOrOlder ==="); + + const workerCurrentOrOlder = new DurableTaskAzureManagedWorkerBuilder() + .connectionString(connectionString) + .logger(logger) + .versioning({ + version: "3.0.0", + matchStrategy: VersionMatchStrategy.CurrentOrOlder, + failureStrategy: VersionFailureStrategy.Fail, + }) + .addOrchestrator(simpleOrchestrator) + .addActivity(doWork) + .build(); + + try { + await workerCurrentOrOlder.start(); + await new Promise((r) => setTimeout(r, 2000)); + + // Older version โ€” should be processed + const olderId = await clientV1.scheduleNewOrchestration(simpleOrchestrator, undefined, { version: "2.0.0" }); + const olderState = await clientV1.waitForOrchestrationCompletion(olderId, true, 30); + console.log(`Older (v2.0.0): ${OrchestrationStatus[olderState!.runtimeStatus]} โ€” ${olderState?.serializedOutput}`); + + // Same version โ€” should be processed + const sameId = await clientV1.scheduleNewOrchestration(simpleOrchestrator, undefined, { version: "3.0.0" }); + const sameState = await clientV1.waitForOrchestrationCompletion(sameId, true, 30); + console.log(`Same (v3.0.0): ${OrchestrationStatus[sameState!.runtimeStatus]} โ€” ${sameState?.serializedOutput}`); + + // Newer version โ€” should fail (worker is 3.0.0, orchestration is 4.0.0) + const newerId = await clientV1.scheduleNewOrchestration(simpleOrchestrator, undefined, { version: "4.0.0" }); + const newerState = await clientV1.waitForOrchestrationCompletion(newerId, true, 15); + console.log(`Newer (v4.0.0): ${OrchestrationStatus[newerState!.runtimeStatus]}`); + if (newerState?.failureDetails) { + console.log(` Failure: ${newerState.failureDetails.errorMessage?.substring(0, 80)}`); + } + + await workerCurrentOrOlder.stop(); + } catch (error) { + console.error("Error in demo 3:", error); + await workerCurrentOrOlder.stop(); + } + + console.log("\n=== All versioning demos completed successfully! ==="); + + await clientV1.stop(); + process.exit(0); +})(); diff --git a/examples/azure-managed/versioning/sample.json b/examples/azure-managed/versioning/sample.json new file mode 100644 index 0000000..5a9e234 --- /dev/null +++ b/examples/azure-managed/versioning/sample.json @@ -0,0 +1,5 @@ +{ + "name": "versioning", + "description": "Orchestration versioning: match strategies, failure strategies, version comparison, and default version", + "requiresEmulator": true +} diff --git a/test/e2e-azuremanaged/retry-handler.spec.ts b/test/e2e-azuremanaged/retry-handler.spec.ts index c2ff2e3..e4488fe 100644 --- a/test/e2e-azuremanaged/retry-handler.spec.ts +++ b/test/e2e-azuremanaged/retry-handler.spec.ts @@ -316,7 +316,7 @@ describe("Retry Handler E2E Tests", () => { }; // Retry handler returning delay in ms (fixed 500ms delay) - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const retryHandler: any = async (ctx: RetryContext): Promise => { if (ctx.lastAttemptNumber >= 5) { return false; @@ -366,7 +366,7 @@ describe("Retry Handler E2E Tests", () => { }; // Retry handler implementing manual exponential backoff: 200ms, 400ms, 800ms - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const retryHandler: any = async (ctx: RetryContext): Promise => { if (ctx.lastAttemptNumber >= 5) { return false; @@ -417,7 +417,7 @@ describe("Retry Handler E2E Tests", () => { }; // Retry handler returning fixed delay - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const retryHandler: any = async (ctx: RetryContext): Promise => { if (ctx.lastAttemptNumber >= 5) { return false; @@ -463,7 +463,7 @@ describe("Retry Handler E2E Tests", () => { }; // Handler: returns delay for TransientError, false for FatalError - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const retryHandler: any = async (ctx: RetryContext): Promise => { if (ctx.lastFailure.errorType === "TransientError") { return 200; // retry after 200ms @@ -502,7 +502,7 @@ describe("Retry Handler E2E Tests", () => { }; // Sync handler returning a delay - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const syncHandler: any = (ctx: RetryContext): boolean | number => { if (ctx.lastAttemptNumber >= 5) { return false; From b9a6572059027403eb741b50fa1b5561de7b4f60 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:22:56 -0800 Subject: [PATCH 02/18] Fix output formatting for emulator and non-emulator samples in validation workflow --- .github/workflows/validate-samples.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index af3d8ef..8f583d5 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -61,8 +61,8 @@ jobs: fi done - echo "emulator=$emulator_samples" >> "$GITHUB_OUTPUT" - echo "no_emulator=$no_emulator_samples" >> "$GITHUB_OUTPUT" + echo "emulator=$(echo "$emulator_samples" | jq -c .)" >> "$GITHUB_OUTPUT" + echo "no_emulator=$(echo "$no_emulator_samples" | jq -c .)" >> "$GITHUB_OUTPUT" echo "--- Emulator samples ---" echo "$emulator_samples" | jq . From 65ddfb18e847855eecacd9034bd4e07528d772eb Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 9 Feb 2026 12:33:10 -0800 Subject: [PATCH 03/18] Update examples/azure-managed/hello-orchestrations/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/azure-managed/hello-orchestrations/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/azure-managed/hello-orchestrations/index.ts b/examples/azure-managed/hello-orchestrations/index.ts index fd217c8..dfb1201 100644 --- a/examples/azure-managed/hello-orchestrations/index.ts +++ b/examples/azure-managed/hello-orchestrations/index.ts @@ -18,10 +18,13 @@ import { DurableTaskAzureManagedWorkerBuilder, ConsoleLogger, } from "@microsoft/durabletask-js-azuremanaged"; -import { ActivityContext } from "@microsoft/durabletask-js/dist/task/context/activity-context"; -import { OrchestrationContext } from "@microsoft/durabletask-js/dist/task/context/orchestration-context"; -import { TOrchestrator } from "@microsoft/durabletask-js/dist/types/orchestrator.type"; -import { whenAll, whenAny } from "@microsoft/durabletask-js/dist/task"; +import { + ActivityContext, + OrchestrationContext, + TOrchestrator, + whenAll, + whenAny, +} from "@microsoft/durabletask-js"; // --------------------------------------------------------------------------- // Activities From d55aed641caa9d3e242d25a8bc2c64fa119b5b1d Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 9 Feb 2026 12:33:56 -0800 Subject: [PATCH 04/18] Update test/e2e-azuremanaged/retry-handler.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/e2e-azuremanaged/retry-handler.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e-azuremanaged/retry-handler.spec.ts b/test/e2e-azuremanaged/retry-handler.spec.ts index e4488fe..514a507 100644 --- a/test/e2e-azuremanaged/retry-handler.spec.ts +++ b/test/e2e-azuremanaged/retry-handler.spec.ts @@ -316,7 +316,6 @@ describe("Retry Handler E2E Tests", () => { }; // Retry handler returning delay in ms (fixed 500ms delay) - const retryHandler: any = async (ctx: RetryContext): Promise => { if (ctx.lastAttemptNumber >= 5) { return false; From 70f0b4163291bc4544920e069ed87efa90a7313a Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 9 Feb 2026 12:34:15 -0800 Subject: [PATCH 05/18] Update .github/workflows/validate-samples.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/validate-samples.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index 8f583d5..c6d4cf7 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -43,11 +43,11 @@ jobs: run: | SAMPLES_ROOT="examples/azure-managed" - # Find all sample.json files in subdirectories (not the root sample.json) + # Find all sample.json files under the samples root (including the root sample.json) emulator_samples="[]" no_emulator_samples="[]" - for sample_json in $(find "$SAMPLES_ROOT" -mindepth 2 -name "sample.json" | sort); do + for sample_json in $(find "$SAMPLES_ROOT" -mindepth 1 -name "sample.json" | sort); do dir=$(dirname "$sample_json") name=$(basename "$dir") requires_emulator=$(jq -r '.requiresEmulator // true' "$sample_json") From c609f8d9d66bf89f30bddcfeb68d635140a650db Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 9 Feb 2026 15:00:54 -0800 Subject: [PATCH 06/18] Update examples/azure-managed/hello-orchestrations/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/azure-managed/hello-orchestrations/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/azure-managed/hello-orchestrations/index.ts b/examples/azure-managed/hello-orchestrations/index.ts index dfb1201..7e23811 100644 --- a/examples/azure-managed/hello-orchestrations/index.ts +++ b/examples/azure-managed/hello-orchestrations/index.ts @@ -41,10 +41,10 @@ const processItem = async (_ctx: ActivityContext, item: string): Promise return item.length; }; -/** Child orchestration: doubles each number via an activity. */ +/** Child orchestration: adds two to the input via two plusOne activity calls. */ const doubleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, value: number): any { const doubled: number = yield ctx.callActivity(plusOne, value); - // Call plusOne again so we get value + 2 (close to doubling for small ints) + // Call plusOne again so we get value + 2 const result: number = yield ctx.callActivity(plusOne, doubled); return result; }; From 399bce7b683c9ee4cf94d02769a3056e39adb53b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:03:59 -0800 Subject: [PATCH 07/18] Add greeting activity to hello-orchestrations sample and remove commented-out client version in versioning sample --- examples/azure-managed/hello-orchestrations/index.ts | 6 ++++++ examples/azure-managed/versioning/index.ts | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/azure-managed/hello-orchestrations/index.ts b/examples/azure-managed/hello-orchestrations/index.ts index 7e23811..dc73f81 100644 --- a/examples/azure-managed/hello-orchestrations/index.ts +++ b/examples/azure-managed/hello-orchestrations/index.ts @@ -41,6 +41,11 @@ const processItem = async (_ctx: ActivityContext, item: string): Promise return item.length; }; +/** Return a greeting string for the given name. */ +const greet = async (_ctx: ActivityContext, name: string): Promise => { + return `Hello, ${name}!`; +}; + /** Child orchestration: adds two to the input via two plusOne activity calls. */ const doubleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, value: number): any { const doubled: number = yield ctx.callActivity(plusOne, value); @@ -134,6 +139,7 @@ const guidOrchestrator: TOrchestrator = async function* (ctx: OrchestrationConte .addOrchestrator(guidOrchestrator) .addActivity(plusOne) .addActivity(processItem) + .addActivity(greet) .build(); try { diff --git a/examples/azure-managed/versioning/index.ts b/examples/azure-managed/versioning/index.ts index 56f69da..11b9632 100644 --- a/examples/azure-managed/versioning/index.ts +++ b/examples/azure-managed/versioning/index.ts @@ -74,7 +74,6 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon // --- 1. Schedule with version --- console.log("\n=== 1. Schedule Orchestration with Version ==="); - // Client with a default version const clientV1 = new DurableTaskAzureManagedClientBuilder() .connectionString(connectionString) .logger(logger) From df57748d3cc81f0c0064d06aaf9eb359b19371a1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:53:17 -0800 Subject: [PATCH 08/18] cleanup --- .github/workflows/validate-samples.yaml | 6 +- examples/azure-managed/.env.example | 16 +++- examples/azure-managed/README.md | 74 ++++++++++++++++++- .../hello-orchestrations/README.md | 38 ++++++++-- .../hello-orchestrations/index.ts | 26 ------- .../hello-orchestrations/sample.json | 2 +- .../azure-managed/human-interaction/README.md | 32 ++++++++ .../lifecycle-management/README.md | 32 ++++++++ .../azure-managed/query-and-history/README.md | 32 ++++++++ .../retry-and-error-handling/README.md | 32 ++++++++ examples/azure-managed/versioning/README.md | 32 ++++++++ 11 files changed, 282 insertions(+), 40 deletions(-) diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index c6d4cf7..e57ae9f 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -212,9 +212,9 @@ jobs: echo "No-emulator result: ${{ needs.samples-no-emulator.result }}" echo "Emulator result: ${{ needs.samples-with-emulator.result }}" - if [[ "${{ needs.samples-no-emulator.result }}" == "failure" ]] || \ - [[ "${{ needs.samples-with-emulator.result }}" == "failure" ]]; then - echo "โŒ Some samples failed!" + if [[ ! "${{ needs.samples-no-emulator.result }}" =~ ^(success|skipped)$ ]] || \ + [[ ! "${{ needs.samples-with-emulator.result }}" =~ ^(success|skipped)$ ]]; then + echo "โŒ Some samples failed or were cancelled!" exit 1 fi diff --git a/examples/azure-managed/.env.example b/examples/azure-managed/.env.example index a9fd951..38e3cfd 100644 --- a/examples/azure-managed/.env.example +++ b/examples/azure-managed/.env.example @@ -1,10 +1,22 @@ # Azure Managed Durable Task Scheduler (DTS) Configuration # Copy this file to .env and update the values for your environment. +# +# To find your endpoint, run: +# az durabletask scheduler show --resource-group --name --query endpoint -o tsv +# +# Make sure you have the "Durable Task Data Contributor" role assigned on the scheduler resource. +# Authenticate via: az login + # Option 1: Using connection string (recommended) -# Supported authentication types: DefaultAzure, ManagedIdentity, WorkloadIdentity, +# Supported authentication types: DefaultAzure, ManagedIdentity, WorkloadIdentity, # Environment, AzureCli, AzurePowerShell, VisualStudioCode, InteractiveBrowser, None +# Use Authentication=None only for the local emulator. DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + # Option 2: Using explicit parameters (uses DefaultAzureCredential) # Uncomment these lines and comment out DURABLE_TASK_SCHEDULER_CONNECTION_STRING above # AZURE_DTS_ENDPOINT=https://your-scheduler.eastus.durabletask.io -# AZURE_DTS_TASKHUB=your-taskhub \ No newline at end of file +# AZURE_DTS_TASKHUB=your-taskhub + +# Optional: OTLP endpoint for distributed tracing (Jaeger, Azure Monitor, Aspire Dashboard, etc.) +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ No newline at end of file diff --git a/examples/azure-managed/README.md b/examples/azure-managed/README.md index 0a8bec0..2a4c4f1 100644 --- a/examples/azure-managed/README.md +++ b/examples/azure-managed/README.md @@ -6,7 +6,7 @@ Runnable samples demonstrating every major feature of the Durable Task JavaScrip | Sample | Scenario | Key Features | Emulator Required | |--------|----------|-------------|-------------------| -| [hello-orchestrations](hello-orchestrations/) | Core patterns | Activity sequence, fan-out/fan-in, sub-orchestrations, `whenAny`, deterministic GUID | Yes | +| [hello-orchestrations](hello-orchestrations/) | Core patterns | Activity sequence, fan-out/fan-in, sub-orchestrations, `whenAny` | Yes | | [retry-and-error-handling](retry-and-error-handling/) | Fault tolerance | `RetryPolicy`, `handleFailure`, `AsyncRetryHandler`, sub-orchestration retry, `raiseIfFailed()` | Yes | | [human-interaction](human-interaction/) | Event-driven workflows | External events, timers, `whenAny` race, `sendEvent`, custom status | Yes | | [lifecycle-management](lifecycle-management/) | Orchestration control | Terminate (recursive), suspend/resume, restart, continue-as-new, purge, tags | Yes | @@ -16,7 +16,7 @@ Runnable samples demonstrating every major feature of the Durable Task JavaScrip | [index.ts](index.ts) | Azure-managed basics | Connection strings, `DefaultAzureCredential`, `createAzureManagedClient` | Yes | | [distributed-tracing.ts](distributed-tracing.ts) | OpenTelemetry tracing | `NodeSDK`, OTLP export, Jaeger, `DurableTaskAzureManagedClientBuilder` | Yes | -### Quick Start +### Quick Start (Local Emulator) ```bash npm install && npm run build # build SDK @@ -26,6 +26,74 @@ cd ../.. npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts ``` +### Quick Start (Azure Managed DTS โ€” Cloud) + +To run samples against a **real Azure Managed Durable Task Scheduler** instead of the local emulator: + +#### 1. Create a Durable Task Scheduler resource + +If you haven't already, create a Durable Task Scheduler and a Task Hub in Azure: + +```bash +# Install the Durable Task Scheduler CLI extension +az extension add --name durabletask + +# Create a scheduler +az durabletask scheduler create \ + --resource-group \ + --name \ + --location \ + --sku free + +# Create a task hub +az durabletask taskhub create \ + --resource-group \ + --scheduler-name \ + --name +``` + +#### 2. Assign yourself the "Durable Task Data Contributor" role + +```bash +SCHEDULER_ID=$(az durabletask scheduler show \ + --resource-group \ + --name \ + --query id -o tsv) + +az role assignment create \ + --assignee $(az ad signed-in-user show --query id -o tsv) \ + --role "Durable Task Data Contributor" \ + --scope $SCHEDULER_ID +``` + +#### 3. Configure your `.env` file + +```bash +cd examples/azure-managed +cp .env.example .env +``` + +Edit `.env` with your scheduler's endpoint and task hub name: + +```env +# Option A: Connection string (recommended) +DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub= + +# Option B: Explicit parameters +# AZURE_DTS_ENDPOINT=https://.eastus.durabletask.io +# AZURE_DTS_TASKHUB= +``` + +#### 4. Authenticate and run + +```bash +az login # authenticate with Azure +cd ../.. # back to repo root +npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts +``` + +> **Supported authentication types** in the connection string: `DefaultAzure`, `ManagedIdentity`, `WorkloadIdentity`, `Environment`, `AzureCli`, `AzurePowerShell`, `VisualStudioCode`, `InteractiveBrowser`. + See each sample's README for details. See [Feature Coverage Map](#feature-coverage-map) below for full feature mapping. ### CI Validation @@ -53,7 +121,7 @@ done | `whenAll()` | hello-orchestrations, unit-testing | | `whenAny()` | hello-orchestrations, human-interaction | | `ctx.callSubOrchestrator()` | hello-orchestrations, retry-and-error-handling, lifecycle-management | -| `ctx.newGuid()` | hello-orchestrations | + | `ctx.waitForExternalEvent()` | human-interaction, unit-testing | | `client.raiseOrchestrationEvent()` | human-interaction, unit-testing | | `ctx.createTimer()` | human-interaction, query-and-history, unit-testing | diff --git a/examples/azure-managed/hello-orchestrations/README.md b/examples/azure-managed/hello-orchestrations/README.md index fb2209e..be695de 100644 --- a/examples/azure-managed/hello-orchestrations/README.md +++ b/examples/azure-managed/hello-orchestrations/README.md @@ -1,6 +1,6 @@ # Hello Orchestrations -Demonstrates the five fundamental orchestration patterns every Durable Task developer needs. +Demonstrates four fundamental orchestration patterns every Durable Task developer needs. ## Features Covered @@ -10,7 +10,6 @@ Demonstrates the five fundamental orchestration patterns every Durable Task deve | Fan-out/fan-in | `whenAll()` with parallel `callActivity()` | | Sub-orchestrations | `ctx.callSubOrchestrator()` | | Race pattern | `whenAny()` | -| Deterministic GUID | `ctx.newGuid()` | ## Prerequisites @@ -49,9 +48,6 @@ Result: {"result1":12,"result2":22} === 4. whenAny (Race) === Result: {"winnerResult":5} -=== 5. Deterministic GUID === -Result: {"guid1":"","guid2":"","areDifferent":true} - === All orchestrations completed successfully! === ``` @@ -65,3 +61,35 @@ npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts 2>&1 | - **Connection refused**: Ensure the DTS emulator is running (`docker compose up -d` from the `examples/azure-managed` directory). - **Worker timeout**: The emulator may need a few seconds to start. Retry the command. + +## Running Against Azure Managed DTS (Cloud) + +To run this sample against a real [Azure Managed Durable Task Scheduler](https://learn.microsoft.com/azure/durable-task-scheduler/) instead of the local emulator: + +1. **Create a scheduler and task hub** (if you haven't already) โ€” see the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for `az durabletask` commands. + +2. **Configure `.env`** for your cloud endpoint: + + ```bash + cd examples/azure-managed + cp .env.example .env + # Edit .env with your scheduler endpoint and task hub name + ``` + + Example `.env`: + + ```env + DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + ``` + +3. **Authenticate** with Azure: + + ```bash + az login + ``` + +4. **Run** (no Docker needed): + + ```bash + npm run example -- ./examples/azure-managed/hello-orchestrations/index.ts + ``` diff --git a/examples/azure-managed/hello-orchestrations/index.ts b/examples/azure-managed/hello-orchestrations/index.ts index dc73f81..d229080 100644 --- a/examples/azure-managed/hello-orchestrations/index.ts +++ b/examples/azure-managed/hello-orchestrations/index.ts @@ -7,7 +7,6 @@ // 2. Fan-out/fan-in โ€” run activities in parallel, aggregate results // 3. Sub-orchestrations โ€” compose orchestrators hierarchically // 4. whenAny โ€” race multiple tasks, use winner's result -// 5. Deterministic GUID โ€” generate replay-safe unique IDs import * as dotenv from "dotenv"; import * as path from "path"; @@ -41,11 +40,6 @@ const processItem = async (_ctx: ActivityContext, item: string): Promise return item.length; }; -/** Return a greeting string for the given name. */ -const greet = async (_ctx: ActivityContext, name: string): Promise => { - return `Hello, ${name}!`; -}; - /** Child orchestration: adds two to the input via two plusOne activity calls. */ const doubleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, value: number): any { const doubled: number = yield ctx.callActivity(plusOne, value); @@ -102,17 +96,6 @@ const raceOrchestrator: TOrchestrator = async function* (ctx: OrchestrationConte return { winnerResult: winner.getResult() }; }; -/** 5. Deterministic GUID โ€” generates replay-safe unique IDs. */ -const guidOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { - // Call an activity so the generator has at least one yield point - const label: string = yield ctx.callActivity(greet, "GUID demo"); - const guid1 = ctx.newGuid(); - const guid2 = ctx.newGuid(); - - // These GUIDs are deterministic: same values across replays - return { label, guid1, guid2, areDifferent: guid1 !== guid2 }; -}; - // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -136,10 +119,8 @@ const guidOrchestrator: TOrchestrator = async function* (ctx: OrchestrationConte .addOrchestrator(parentOrchestrator) .addOrchestrator(doubleOrchestrator) .addOrchestrator(raceOrchestrator) - .addOrchestrator(guidOrchestrator) .addActivity(plusOne) .addActivity(processItem) - .addActivity(greet) .build(); try { @@ -174,13 +155,6 @@ const guidOrchestrator: TOrchestrator = async function* (ctx: OrchestrationConte console.log(`Result: ${raceState?.serializedOutput}`); // Expected: {"winnerResult":} - // --- 5. Deterministic GUID --- - console.log("\n=== 5. Deterministic GUID ==="); - const guidId = await client.scheduleNewOrchestration(guidOrchestrator); - const guidState = await client.waitForOrchestrationCompletion(guidId, true, 30); - console.log(`Result: ${guidState?.serializedOutput}`); - // Expected: {"guid1":"","guid2":"","areDifferent":true} - console.log("\n=== All orchestrations completed successfully! ==="); } catch (error) { console.error("Error:", error); diff --git a/examples/azure-managed/hello-orchestrations/sample.json b/examples/azure-managed/hello-orchestrations/sample.json index 4b437df..0f2a412 100644 --- a/examples/azure-managed/hello-orchestrations/sample.json +++ b/examples/azure-managed/hello-orchestrations/sample.json @@ -1,5 +1,5 @@ { "name": "hello-orchestrations", - "description": "Core orchestration patterns: activity sequences, fan-out/fan-in, sub-orchestrations, whenAny, and deterministic GUIDs", + "description": "Core orchestration patterns: activity sequences, fan-out/fan-in, sub-orchestrations, whenAny", "requiresEmulator": true } diff --git a/examples/azure-managed/human-interaction/README.md b/examples/azure-managed/human-interaction/README.md index 382a588..9890ca0 100644 --- a/examples/azure-managed/human-interaction/README.md +++ b/examples/azure-managed/human-interaction/README.md @@ -61,3 +61,35 @@ Result: {"event1":"Hello","event2":"World"} ```bash npm run example -- ./examples/azure-managed/human-interaction/index.ts 2>&1 | grep "All human-interaction demos completed successfully" ``` + +## Running Against Azure Managed DTS (Cloud) + +To run this sample against a real [Azure Managed Durable Task Scheduler](https://learn.microsoft.com/azure/durable-task-scheduler/) instead of the local emulator: + +1. **Create a scheduler and task hub** (if you haven't already) โ€” see the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for `az durabletask` commands. + +2. **Configure `.env`** for your cloud endpoint: + + ```bash + cd examples/azure-managed + cp .env.example .env + # Edit .env with your scheduler endpoint and task hub name + ``` + + Example `.env`: + + ```env + DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + ``` + +3. **Authenticate** with Azure: + + ```bash + az login + ``` + +4. **Run** (no Docker needed): + + ```bash + npm run example -- ./examples/azure-managed/human-interaction/index.ts + ``` diff --git a/examples/azure-managed/lifecycle-management/README.md b/examples/azure-managed/lifecycle-management/README.md index 4859402..e69fceb 100644 --- a/examples/azure-managed/lifecycle-management/README.md +++ b/examples/azure-managed/lifecycle-management/README.md @@ -76,3 +76,35 @@ Result: "Done: tagged-run" ```bash npm run example -- ./examples/azure-managed/lifecycle-management/index.ts 2>&1 | grep "All lifecycle demos completed successfully" ``` + +## Running Against Azure Managed DTS (Cloud) + +To run this sample against a real [Azure Managed Durable Task Scheduler](https://learn.microsoft.com/azure/durable-task-scheduler/) instead of the local emulator: + +1. **Create a scheduler and task hub** (if you haven't already) โ€” see the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for `az durabletask` commands. + +2. **Configure `.env`** for your cloud endpoint: + + ```bash + cd examples/azure-managed + cp .env.example .env + # Edit .env with your scheduler endpoint and task hub name + ``` + + Example `.env`: + + ```env + DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + ``` + +3. **Authenticate** with Azure: + + ```bash + az login + ``` + +4. **Run** (no Docker needed): + + ```bash + npm run example -- ./examples/azure-managed/lifecycle-management/index.ts + ``` diff --git a/examples/azure-managed/query-and-history/README.md b/examples/azure-managed/query-and-history/README.md index c12eff6..90a7c1e 100644 --- a/examples/azure-managed/query-and-history/README.md +++ b/examples/azure-managed/query-and-history/README.md @@ -72,3 +72,35 @@ Created 6 orchestration instances. ```bash npm run example -- ./examples/azure-managed/query-and-history/index.ts 2>&1 | grep "All query/history demos completed successfully" ``` + +## Running Against Azure Managed DTS (Cloud) + +To run this sample against a real [Azure Managed Durable Task Scheduler](https://learn.microsoft.com/azure/durable-task-scheduler/) instead of the local emulator: + +1. **Create a scheduler and task hub** (if you haven't already) โ€” see the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for `az durabletask` commands. + +2. **Configure `.env`** for your cloud endpoint: + + ```bash + cd examples/azure-managed + cp .env.example .env + # Edit .env with your scheduler endpoint and task hub name + ``` + + Example `.env`: + + ```env + DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + ``` + +3. **Authenticate** with Azure: + + ```bash + az login + ``` + +4. **Run** (no Docker needed): + + ```bash + npm run example -- ./examples/azure-managed/query-and-history/index.ts + ``` diff --git a/examples/azure-managed/retry-and-error-handling/README.md b/examples/azure-managed/retry-and-error-handling/README.md index 7c71900..5c78c97 100644 --- a/examples/azure-managed/retry-and-error-handling/README.md +++ b/examples/azure-managed/retry-and-error-handling/README.md @@ -66,3 +66,35 @@ Failure message: FatalError: This operation cannot succeed ```bash npm run example -- ./examples/azure-managed/retry-and-error-handling/index.ts 2>&1 | grep "All retry/error demos completed successfully" ``` + +## Running Against Azure Managed DTS (Cloud) + +To run this sample against a real [Azure Managed Durable Task Scheduler](https://learn.microsoft.com/azure/durable-task-scheduler/) instead of the local emulator: + +1. **Create a scheduler and task hub** (if you haven't already) โ€” see the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for `az durabletask` commands. + +2. **Configure `.env`** for your cloud endpoint: + + ```bash + cd examples/azure-managed + cp .env.example .env + # Edit .env with your scheduler endpoint and task hub name + ``` + + Example `.env`: + + ```env + DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + ``` + +3. **Authenticate** with Azure: + + ```bash + az login + ``` + +4. **Run** (no Docker needed): + + ```bash + npm run example -- ./examples/azure-managed/retry-and-error-handling/index.ts + ``` diff --git a/examples/azure-managed/versioning/README.md b/examples/azure-managed/versioning/README.md index e55b6b0..1a7928d 100644 --- a/examples/azure-managed/versioning/README.md +++ b/examples/azure-managed/versioning/README.md @@ -60,3 +60,35 @@ Newer (v4.0.0): FAILED ```bash npm run example -- ./examples/azure-managed/versioning/index.ts 2>&1 | grep "All versioning demos completed successfully" ``` + +## Running Against Azure Managed DTS (Cloud) + +To run this sample against a real [Azure Managed Durable Task Scheduler](https://learn.microsoft.com/azure/durable-task-scheduler/) instead of the local emulator: + +1. **Create a scheduler and task hub** (if you haven't already) โ€” see the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for `az durabletask` commands. + +2. **Configure `.env`** for your cloud endpoint: + + ```bash + cd examples/azure-managed + cp .env.example .env + # Edit .env with your scheduler endpoint and task hub name + ``` + + Example `.env`: + + ```env + DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://your-scheduler.eastus.durabletask.io;Authentication=DefaultAzure;TaskHub=your-taskhub + ``` + +3. **Authenticate** with Azure: + + ```bash + az login + ``` + +4. **Run** (no Docker needed): + + ```bash + npm run example -- ./examples/azure-managed/versioning/index.ts + ``` From 097e16d8ffaa79fe3abd4bebef36ed12ba897715 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:01:36 -0800 Subject: [PATCH 09/18] Refactor DTS emulator setup in validate-samples workflow to use Docker commands directly --- .github/workflows/validate-samples.yaml | 36 +++++++++---------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index e57ae9f..e020788 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -149,24 +149,20 @@ jobs: sample: ${{ fromJson(needs.discover.outputs.emulator-samples) }} node-version: ["22.x"] - services: - dts-emulator: - image: mcr.microsoft.com/dts/dts-emulator:latest - ports: - - 8080:8080 - - 8082:8082 - options: >- - --health-cmd "curl -sf http://localhost:8082/ || exit 1" - --health-interval 5s - --health-timeout 5s - --health-retries 10 - env: DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;Authentication=None;TaskHub=default" steps: - uses: actions/checkout@v4 + - name: ๐Ÿณ Start DTS emulator + run: | + docker pull mcr.microsoft.com/dts/dts-emulator:latest + docker run --name dtsemulator -d -p 8080:8080 mcr.microsoft.com/dts/dts-emulator:latest + + - name: โณ Wait for DTS emulator + run: sleep 10 + - name: โš™๏ธ Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -181,24 +177,16 @@ jobs: packages/*/node_modules key: sdk-build-${{ github.sha }}-node${{ matrix.node-version }} - - name: โณ Wait for DTS emulator - run: | - echo "Waiting for DTS emulator to be ready..." - for i in $(seq 1 30); do - if curl -sf http://localhost:8082/ > /dev/null 2>&1; then - echo "DTS emulator is ready!" - break - fi - echo " Attempt $i/30 โ€” waiting 2s..." - sleep 2 - done - - name: ๐Ÿงช Run sample โ€” ${{ matrix.sample }} run: | echo "Running sample: ${{ matrix.sample }}" npx ts-node --swc ./examples/azure-managed/${{ matrix.sample }}/index.ts timeout-minutes: 5 + - name: ๐Ÿงน Stop DTS emulator + if: always() + run: docker rm -f dtsemulator || true + # ----------------------------------------------------------------------- # 5. Summary gate โ€” all samples must pass # ----------------------------------------------------------------------- From 08645bbcd10fbd8b05e60e74be5795d1ae039b13 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:07:08 -0800 Subject: [PATCH 10/18] Update sample discovery logic to skip root sample.json in validate-samples workflow --- .github/workflows/validate-samples.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index e020788..0f3dffe 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -43,11 +43,11 @@ jobs: run: | SAMPLES_ROOT="examples/azure-managed" - # Find all sample.json files under the samples root (including the root sample.json) + # Find all sample.json files in subdirectories (skip the root sample.json) emulator_samples="[]" no_emulator_samples="[]" - for sample_json in $(find "$SAMPLES_ROOT" -mindepth 1 -name "sample.json" | sort); do + for sample_json in $(find "$SAMPLES_ROOT" -mindepth 2 -name "sample.json" | sort); do dir=$(dirname "$sample_json") name=$(basename "$dir") requires_emulator=$(jq -r '.requiresEmulator // true' "$sample_json") From 163a5e961eff835ba0060774cfc5af61ab31dcf2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:14:53 -0800 Subject: [PATCH 11/18] Add Azure Managed DTS samples for basic usage and distributed tracing --- examples/azure-managed/{ => basics}/index.ts | 0 examples/azure-managed/{ => basics}/sample.json | 0 .../{distributed-tracing.ts => distributed-tracing/index.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename examples/azure-managed/{ => basics}/index.ts (100%) rename examples/azure-managed/{ => basics}/sample.json (100%) rename examples/azure-managed/{distributed-tracing.ts => distributed-tracing/index.ts} (100%) diff --git a/examples/azure-managed/index.ts b/examples/azure-managed/basics/index.ts similarity index 100% rename from examples/azure-managed/index.ts rename to examples/azure-managed/basics/index.ts diff --git a/examples/azure-managed/sample.json b/examples/azure-managed/basics/sample.json similarity index 100% rename from examples/azure-managed/sample.json rename to examples/azure-managed/basics/sample.json diff --git a/examples/azure-managed/distributed-tracing.ts b/examples/azure-managed/distributed-tracing/index.ts similarity index 100% rename from examples/azure-managed/distributed-tracing.ts rename to examples/azure-managed/distributed-tracing/index.ts From 708f34538687aa66a48546b25176209223e5bc14 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:16:42 -0800 Subject: [PATCH 12/18] Add samples for basics and distributed tracing, update validation logic, and fix dotenv path --- .github/workflows/validate-samples.yaml | 4 +- examples/azure-managed/README.md | 22 +++++----- examples/azure-managed/basics/README.md | 39 ++++++++++++++++++ examples/azure-managed/basics/index.ts | 2 +- .../distributed-tracing/README.md | 40 +++++++++++++++++++ .../distributed-tracing/index.ts | 2 +- .../distributed-tracing/sample.json | 5 +++ .../azure-managed/query-and-history/index.ts | 4 +- examples/azure-managed/unit-testing/index.ts | 2 +- 9 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 examples/azure-managed/basics/README.md create mode 100644 examples/azure-managed/distributed-tracing/README.md create mode 100644 examples/azure-managed/distributed-tracing/sample.json diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index 0f3dffe..eeed163 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -43,11 +43,11 @@ jobs: run: | SAMPLES_ROOT="examples/azure-managed" - # Find all sample.json files in subdirectories (skip the root sample.json) + # Find all sample.json files under the samples root emulator_samples="[]" no_emulator_samples="[]" - for sample_json in $(find "$SAMPLES_ROOT" -mindepth 2 -name "sample.json" | sort); do + for sample_json in $(find "$SAMPLES_ROOT" -mindepth 1 -name "sample.json" | sort); do dir=$(dirname "$sample_json") name=$(basename "$dir") requires_emulator=$(jq -r '.requiresEmulator // true' "$sample_json") diff --git a/examples/azure-managed/README.md b/examples/azure-managed/README.md index 2a4c4f1..6cc1ddf 100644 --- a/examples/azure-managed/README.md +++ b/examples/azure-managed/README.md @@ -13,8 +13,8 @@ Runnable samples demonstrating every major feature of the Durable Task JavaScrip | [query-and-history](query-and-history/) | Monitoring & debugging | `getAllInstances`, pagination, `listInstanceIds`, `getOrchestrationHistory`, typed events | Yes | | [versioning](versioning/) | Safe deployments | Version match strategies, failure strategies, `ctx.version`, `ctx.compareVersionTo()` | Yes | | [unit-testing](unit-testing/) | Testing without infra | `InMemoryOrchestrationBackend`, `TestOrchestrationClient`, `TestOrchestrationWorker`, `ReplaySafeLogger` | **No** | -| [index.ts](index.ts) | Azure-managed basics | Connection strings, `DefaultAzureCredential`, `createAzureManagedClient` | Yes | -| [distributed-tracing.ts](distributed-tracing.ts) | OpenTelemetry tracing | `NodeSDK`, OTLP export, Jaeger, `DurableTaskAzureManagedClientBuilder` | Yes | +| [basics](basics/) | Azure-managed basics | Connection strings, `DefaultAzureCredential`, `createAzureManagedClient` | Yes | +| [distributed-tracing](distributed-tracing/) | OpenTelemetry tracing | `NodeSDK`, OTLP export, Jaeger, `DurableTaskAzureManagedClientBuilder` | Yes | ### Quick Start (Local Emulator) @@ -149,10 +149,10 @@ done | `TestOrchestrationClient/Worker` | unit-testing | | `ReplaySafeLogger` | unit-testing | | `NoOpLogger` | unit-testing | -| Connection strings | index.ts, all samples | -| `DefaultAzureCredential` | index.ts | -| `createAzureLogger()` | index.ts | -| Distributed tracing (OTel) | distributed-tracing.ts | +| Connection strings | basics, all samples | +| `DefaultAzureCredential` | basics | +| `createAzureLogger()` | basics | +| Distributed tracing (OTel) | distributed-tracing | | `ConsoleLogger` | all samples | --- @@ -263,7 +263,7 @@ npm run build ### 5. Run the Example ```bash -npm run example -- ./examples/azure-managed/distributed-tracing.ts +npm run example -- ./examples/azure-managed/distributed-tracing/index.ts ``` You should see output like: @@ -321,7 +321,7 @@ az login ### 3. Run ```bash -npm run example -- ./examples/azure-managed/distributed-tracing.ts +npm run example -- ./examples/azure-managed/distributed-tracing/index.ts ``` --- @@ -334,7 +334,7 @@ To export traces to **Azure Monitor** instead of Jaeger, replace the OTLP export npm install --no-save @azure/monitor-opentelemetry-exporter ``` -Then modify the OpenTelemetry setup in `distributed-tracing.ts`: +Then modify the OpenTelemetry setup in `distributed-tracing/index.ts`: ```typescript import { AzureMonitorTraceExporter } from "@azure/monitor-opentelemetry-exporter"; @@ -404,8 +404,8 @@ docker compose down | File | Description | |------|-------------| -| `distributed-tracing.ts` | Main example โ€“ OTel setup + orchestrations | +| `distributed-tracing/` | Distributed tracing sample โ€“ OTel setup + orchestrations | +| `basics/` | Basic sample โ€“ connection strings, DefaultAzureCredential | | `docker-compose.yml` | DTS Emulator + Jaeger stack | | `.env.emulator` | Pre-configured env vars for the local emulator | | `.env.example` | Template for Azure Managed DTS (cloud) | -| `index.ts` | Basic example (no tracing) | diff --git a/examples/azure-managed/basics/README.md b/examples/azure-managed/basics/README.md new file mode 100644 index 0000000..e71c233 --- /dev/null +++ b/examples/azure-managed/basics/README.md @@ -0,0 +1,39 @@ +# Basics + +Demonstrates the fundamentals of using the Azure Managed Durable Task Scheduler (DTS) with the JavaScript SDK, including connection strings and `DefaultAzureCredential` authentication. + +## Features Covered + +| Feature | API | +|---------|-----| +| Connection string auth | `createAzureManagedClient(connectionString)` | +| DefaultAzureCredential | `createAzureManagedClient(endpoint, taskHub, credential)` | +| Activity sequence | `ctx.callActivity()` in a loop | +| Fan-out/fan-in | `whenAll()` with parallel `callActivity()` | +| Azure logger | `createAzureLogger()` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator) + +## Setup + +```bash +# From the repository root +cd examples/azure-managed +docker compose up -d # start DTS emulator +cp .env.emulator .env # configure for local emulator +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/basics/index.ts +``` + +## Running Against Azure + +See the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for instructions on running against a real Azure Managed DTS resource. diff --git a/examples/azure-managed/basics/index.ts b/examples/azure-managed/basics/index.ts index 8bebec5..f04edac 100644 --- a/examples/azure-managed/basics/index.ts +++ b/examples/azure-managed/basics/index.ts @@ -8,7 +8,7 @@ // Load environment variables from .env file (recommended for local development) import * as dotenv from "dotenv"; import * as path from "path"; -dotenv.config({ path: path.join(__dirname, ".env") }); +dotenv.config({ path: path.join(__dirname, "..", ".env") }); import { DefaultAzureCredential } from "@azure/identity"; import { diff --git a/examples/azure-managed/distributed-tracing/README.md b/examples/azure-managed/distributed-tracing/README.md new file mode 100644 index 0000000..f17cea2 --- /dev/null +++ b/examples/azure-managed/distributed-tracing/README.md @@ -0,0 +1,40 @@ +# Distributed Tracing + +Demonstrates how to enable OpenTelemetry distributed tracing with the Azure Managed Durable Task Scheduler (DTS). Traces are exported to a local Jaeger or OTLP-compatible collector so you can visualize the full orchestration lifecycle. + +## Features Covered + +| Feature | API | +|---------|-----| +| OpenTelemetry setup | `NodeSDK`, `OTLPTraceExporter`, `SimpleSpanProcessor` | +| Builder pattern | `DurableTaskAzureManagedClientBuilder`, `DurableTaskAzureManagedWorkerBuilder` | +| Data pipeline | Fan-out/fan-in with chained activities | +| Console logger | `ConsoleLogger` | + +## Prerequisites + +- Node.js โ‰ฅ 22 +- Docker (for the DTS Emulator and Jaeger) + +## Setup + +```bash +# From the repository root +cd examples/azure-managed +docker compose up -d # start DTS emulator (and Jaeger if configured) +cp .env.emulator .env # configure for local emulator +cd ../.. +npm install && npm run build +``` + +## Run + +```bash +npm run example -- ./examples/azure-managed/distributed-tracing/index.ts +``` + +After the orchestrations complete, open the Jaeger UI at [http://localhost:16686](http://localhost:16686) and search for service `durabletask-js-tracing-example` to view traces. + +## Running Against Azure + +See the [parent README](../README.md#quick-start-azure-managed-dts--cloud) for instructions on running against a real Azure Managed DTS resource. diff --git a/examples/azure-managed/distributed-tracing/index.ts b/examples/azure-managed/distributed-tracing/index.ts index dd3f76f..0ca3f6e 100644 --- a/examples/azure-managed/distributed-tracing/index.ts +++ b/examples/azure-managed/distributed-tracing/index.ts @@ -20,7 +20,7 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; // Load environment variables from .env file import * as dotenv from "dotenv"; import * as path from "path"; -dotenv.config({ path: path.join(__dirname, ".env") }); +dotenv.config({ path: path.join(__dirname, "..", ".env") }); // Read the OTLP endpoint from the environment (defaults to Jaeger's OTLP HTTP port) const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318"; diff --git a/examples/azure-managed/distributed-tracing/sample.json b/examples/azure-managed/distributed-tracing/sample.json new file mode 100644 index 0000000..71cfa2f --- /dev/null +++ b/examples/azure-managed/distributed-tracing/sample.json @@ -0,0 +1,5 @@ +{ + "name": "distributed-tracing", + "description": "OpenTelemetry distributed tracing with Jaeger/OTLP export", + "requiresEmulator": true +} diff --git a/examples/azure-managed/query-and-history/index.ts b/examples/azure-managed/query-and-history/index.ts index 1693a60..b82cfc0 100644 --- a/examples/azure-managed/query-and-history/index.ts +++ b/examples/azure-managed/query-and-history/index.ts @@ -42,8 +42,8 @@ const greet = async (_ctx: ActivityContext, name: string): Promise => { /** Creates a timer and calls an activity โ€” produces rich history. */ const richOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, name: string): any { - // Create a short timer - yield ctx.createTimer(500); + // Create a short timer (createTimer takes seconds, not milliseconds) + yield ctx.createTimer(1); // Call an activity const greeting: string = yield ctx.callActivity(greet, name); diff --git a/examples/azure-managed/unit-testing/index.ts b/examples/azure-managed/unit-testing/index.ts index 868cac5..7e8a925 100644 --- a/examples/azure-managed/unit-testing/index.ts +++ b/examples/azure-managed/unit-testing/index.ts @@ -64,7 +64,7 @@ const parallelSumOrchestrator: TOrchestrator = async function* (ctx: Orchestrati /** Timer: waits for a timer then returns. */ const timerOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { - yield ctx.createTimer(100); // 100ms timer + yield ctx.createTimer(1); // 1 second timer return "Timer fired"; }; From ded5ef41394e22f3092b3831851303a63b48fa57 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:30:28 -0800 Subject: [PATCH 13/18] Enhance sample validation workflow to support skipCi flag and adjust timeout durations in human interaction samples --- .github/workflows/validate-samples.yaml | 8 ++- .../distributed-tracing/sample.json | 3 +- .../azure-managed/human-interaction/index.ts | 12 ++--- examples/azure-managed/unit-testing/index.ts | 49 ++----------------- 4 files changed, 18 insertions(+), 54 deletions(-) diff --git a/.github/workflows/validate-samples.yaml b/.github/workflows/validate-samples.yaml index eeed163..5e04cf5 100644 --- a/.github/workflows/validate-samples.yaml +++ b/.github/workflows/validate-samples.yaml @@ -51,8 +51,14 @@ jobs: dir=$(dirname "$sample_json") name=$(basename "$dir") requires_emulator=$(jq -r '.requiresEmulator // true' "$sample_json") + skip_ci=$(jq -r '.skipCi // false' "$sample_json") - echo "Found sample: $name (requiresEmulator=$requires_emulator)" + echo "Found sample: $name (requiresEmulator=$requires_emulator, skipCi=$skip_ci)" + + if [ "$skip_ci" = "true" ]; then + echo " โญ๏ธ Skipping $name (skipCi=true)" + continue + fi if [ "$requires_emulator" = "false" ]; then no_emulator_samples=$(echo "$no_emulator_samples" | jq --arg n "$name" '. + [$n]') diff --git a/examples/azure-managed/distributed-tracing/sample.json b/examples/azure-managed/distributed-tracing/sample.json index 71cfa2f..dbf91fa 100644 --- a/examples/azure-managed/distributed-tracing/sample.json +++ b/examples/azure-managed/distributed-tracing/sample.json @@ -1,5 +1,6 @@ { "name": "distributed-tracing", "description": "OpenTelemetry distributed tracing with Jaeger/OTLP export", - "requiresEmulator": true + "requiresEmulator": true, + "skipCi": true } diff --git a/examples/azure-managed/human-interaction/index.ts b/examples/azure-managed/human-interaction/index.ts index fd86b05..f2e6a44 100644 --- a/examples/azure-managed/human-interaction/index.ts +++ b/examples/azure-managed/human-interaction/index.ts @@ -63,7 +63,7 @@ const approvalOrchestrator: TOrchestrator = async function* (ctx: OrchestrationC // Step 2: Race external event vs timer const approvalEvent = ctx.waitForExternalEvent<{ approved: boolean }>("approval"); - const timeout = ctx.createTimer(10_000); // 10 seconds + const timeout = ctx.createTimer(5_000); // 5 seconds const winner = yield whenAny([approvalEvent, timeout]); @@ -159,7 +159,7 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio await client.raiseOrchestrationEvent(approvalId, "approval", { approved: true }); console.log("Sent approval event from client"); - const finalState = await client.waitForOrchestrationCompletion(approvalId, true, 30); + const finalState = await client.waitForOrchestrationCompletion(approvalId, true, 60); console.log(`Result: ${finalState?.serializedOutput}`); console.log(`Final custom status: ${finalState?.serializedCustomStatus}`); @@ -167,7 +167,7 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio console.log("\n=== 2. Approval Workflow (timeout โ€” no event sent) ==="); const timeoutId = await client.scheduleNewOrchestration(approvalOrchestrator, 1000); // Don't send any event โ€” let the timer expire - const timeoutState = await client.waitForOrchestrationCompletion(timeoutId, true, 30); + const timeoutState = await client.waitForOrchestrationCompletion(timeoutId, true, 60); console.log(`Result: ${timeoutState?.serializedOutput}`); // --- 3. sendEvent (orchestration-to-orchestration) --- @@ -179,10 +179,10 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio const notifierId = await client.scheduleNewOrchestration(notifierOrchestrator, targetId); console.log(`Notifier orchestration: ${notifierId}`); - const targetState = await client.waitForOrchestrationCompletion(targetId, true, 30); + const targetState = await client.waitForOrchestrationCompletion(targetId, true, 60); console.log(`Target result: ${targetState?.serializedOutput}`); - const notifierState = await client.waitForOrchestrationCompletion(notifierId, true, 30); + const notifierState = await client.waitForOrchestrationCompletion(notifierId, true, 60); console.log(`Notifier result: ${notifierState?.serializedOutput}`); // --- 4. Multiple external events --- @@ -193,7 +193,7 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio await client.raiseOrchestrationEvent(multiId, "event1", "Hello"); await client.raiseOrchestrationEvent(multiId, "event2", "World"); - const multiState = await client.waitForOrchestrationCompletion(multiId, true, 30); + const multiState = await client.waitForOrchestrationCompletion(multiId, true, 60); console.log(`Result: ${multiState?.serializedOutput}`); console.log("\n=== All human-interaction demos completed successfully! ==="); diff --git a/examples/azure-managed/unit-testing/index.ts b/examples/azure-managed/unit-testing/index.ts index 7e8a925..f7c5b68 100644 --- a/examples/azure-managed/unit-testing/index.ts +++ b/examples/azure-managed/unit-testing/index.ts @@ -9,7 +9,7 @@ // 2. TestOrchestrationClient โ€” same API as TaskHubGrpcClient // 3. TestOrchestrationWorker โ€” same API as TaskHubGrpcWorker // 4. ReplaySafeLogger โ€” suppress duplicate logs during orchestration replay -// 5. Testing all patterns: sequence, fan-out/fan-in, timers, events, continue-as-new +// 5. Testing patterns: sequence, fan-out/fan-in, timers, events, terminate import { InMemoryOrchestrationBackend, @@ -74,15 +74,6 @@ const eventOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCont return `Received: ${data}`; }; -/** Continue-as-new: increments a counter until it reaches 3. */ -const counterOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, count: number): any { - if (count >= 3) { - return `Final count: ${count}`; - } - yield; // yield before continue-as-new - ctx.continueAsNew(count + 1); -}; - // --------------------------------------------------------------------------- // Test runner (lightweight, no framework needed) // --------------------------------------------------------------------------- @@ -195,21 +186,7 @@ function assert(condition: boolean, message: string): void { }), ); - // Test 5: Continue-as-new - results.push( - await runTest("Continue-as-new", async (_backend, client, worker) => { - worker.addOrchestrator(counterOrchestrator); - await worker.start(); - - const id = await client.scheduleNewOrchestration(counterOrchestrator, 0); - const state = await client.waitForOrchestrationCompletion(id, true, 10); - - assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should be completed"); - assert(state!.serializedOutput === '"Final count: 3"', `Got: ${state!.serializedOutput}`); - }), - ); - - // Test 6: Terminate + // Test 5: Terminate results.push( await runTest("Terminate", async (_backend, client, worker) => { worker.addOrchestrator(eventOrchestrator); @@ -225,27 +202,7 @@ function assert(condition: boolean, message: string): void { }), ); - // Test 7: Suspend / Resume - results.push( - await runTest("Suspend / Resume", async (_backend, client, worker) => { - worker.addOrchestrator(eventOrchestrator); - await worker.start(); - - const id = await client.scheduleNewOrchestration(eventOrchestrator); - await client.waitForOrchestrationStart(id); - - await client.suspendOrchestration(id); - let state = await client.getOrchestrationState(id); - assert(state!.runtimeStatus === OrchestrationStatus.SUSPENDED, "Should be suspended"); - - await client.resumeOrchestration(id); - await client.raiseOrchestrationEvent(id, "myEvent", "after-resume"); - state = await client.waitForOrchestrationCompletion(id, true, 10); - assert(state!.runtimeStatus === OrchestrationStatus.COMPLETED, "Should complete after resume"); - }), - ); - - // Test 8: NoOpLogger (verify it doesn't throw) + // Test 6: NoOpLogger (verify it doesn't throw) results.push( await runTest("NoOpLogger", async () => { const logger = new NoOpLogger(); From c29af5a26be0e0f07959267974e788c47ed54783 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:45:25 -0800 Subject: [PATCH 14/18] Refactor multiEventOrchestrator to reorder event handling steps and clarify workflow logging --- .../azure-managed/human-interaction/index.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/azure-managed/human-interaction/index.ts b/examples/azure-managed/human-interaction/index.ts index f2e6a44..1cf0634 100644 --- a/examples/azure-managed/human-interaction/index.ts +++ b/examples/azure-managed/human-interaction/index.ts @@ -163,15 +163,8 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio console.log(`Result: ${finalState?.serializedOutput}`); console.log(`Final custom status: ${finalState?.serializedCustomStatus}`); - // --- 2. Approval with timeout (no event sent) --- - console.log("\n=== 2. Approval Workflow (timeout โ€” no event sent) ==="); - const timeoutId = await client.scheduleNewOrchestration(approvalOrchestrator, 1000); - // Don't send any event โ€” let the timer expire - const timeoutState = await client.waitForOrchestrationCompletion(timeoutId, true, 60); - console.log(`Result: ${timeoutState?.serializedOutput}`); - - // --- 3. sendEvent (orchestration-to-orchestration) --- - console.log("\n=== 3. sendEvent (orchestration โ†’ orchestration) ==="); + // --- 2. sendEvent (orchestration-to-orchestration) --- + console.log("\n=== 2. sendEvent (orchestration โ†’ orchestration) ==="); const targetId = await client.scheduleNewOrchestration(approvalOrchestrator, 250); console.log(`Target orchestration: ${targetId}`); @@ -185,8 +178,8 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio const notifierState = await client.waitForOrchestrationCompletion(notifierId, true, 60); console.log(`Notifier result: ${notifierState?.serializedOutput}`); - // --- 4. Multiple external events --- - console.log("\n=== 4. Multiple External Events ==="); + // --- 3. Multiple external events --- + console.log("\n=== 3. Multiple External Events ==="); const multiId = await client.scheduleNewOrchestration(multiEventOrchestrator); await new Promise((r) => setTimeout(r, 2000)); @@ -196,6 +189,13 @@ const multiEventOrchestrator: TOrchestrator = async function* (ctx: Orchestratio const multiState = await client.waitForOrchestrationCompletion(multiId, true, 60); console.log(`Result: ${multiState?.serializedOutput}`); + // --- 4. Approval with timeout (no event sent โ€” timer wins the race) --- + console.log("\n=== 4. Approval Workflow (timeout โ€” no event sent) ==="); + const timeoutId = await client.scheduleNewOrchestration(approvalOrchestrator, 1000); + // Don't send any event โ€” let the 5-second timer expire and auto-reject + const timeoutState = await client.waitForOrchestrationCompletion(timeoutId, true, 60); + console.log(`Result: ${timeoutState?.serializedOutput}`); + console.log("\n=== All human-interaction demos completed successfully! ==="); } catch (error) { console.error("Error:", error); From 188ac0fb28427056bbc1502dcf2c1fea47e426b6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:55:30 -0800 Subject: [PATCH 15/18] Reduce timer durations in approval and notifier orchestrators for improved responsiveness --- examples/azure-managed/human-interaction/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/azure-managed/human-interaction/index.ts b/examples/azure-managed/human-interaction/index.ts index 1cf0634..e224bb6 100644 --- a/examples/azure-managed/human-interaction/index.ts +++ b/examples/azure-managed/human-interaction/index.ts @@ -63,7 +63,7 @@ const approvalOrchestrator: TOrchestrator = async function* (ctx: OrchestrationC // Step 2: Race external event vs timer const approvalEvent = ctx.waitForExternalEvent<{ approved: boolean }>("approval"); - const timeout = ctx.createTimer(5_000); // 5 seconds + const timeout = ctx.createTimer(5); // 5 seconds const winner = yield whenAny([approvalEvent, timeout]); @@ -94,7 +94,7 @@ const notifierOrchestrator: TOrchestrator = async function* ( targetInstanceId: string, ): any { // Wait a moment before sending (simulate some processing) - yield ctx.createTimer(1_000); + yield ctx.createTimer(1); // Send approval event to the target orchestration ctx.sendEvent(targetInstanceId, "approval", { approved: true }); From 641300f3b1bde294483125bf5b41a12d57df0cdb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:27:37 -0800 Subject: [PATCH 16/18] Refactor orchestration lifecycle management sample to remove recursive termination and adjust step numbering for clarity --- .../lifecycle-management/index.ts | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/examples/azure-managed/lifecycle-management/index.ts b/examples/azure-managed/lifecycle-management/index.ts index f94a6eb..e9d8541 100644 --- a/examples/azure-managed/lifecycle-management/index.ts +++ b/examples/azure-managed/lifecycle-management/index.ts @@ -3,12 +3,11 @@ // This sample demonstrates orchestration lifecycle management: // 1. Terminate โ€” cancel a running orchestration (with output) -// 2. Terminate (recursive) โ€” cancel parent + child orchestrations -// 3. Suspend / Resume โ€” pause and unpause an orchestration -// 4. Continue-as-new โ€” restart an orchestration with new input -// 5. Restart โ€” re-run a completed orchestration -// 6. Purge โ€” delete orchestration history -// 7. Tags โ€” attach metadata to orchestrations +// 2. Suspend / Resume โ€” pause and unpause an orchestration +// 3. Continue-as-new โ€” restart an orchestration with new input +// 4. Restart โ€” re-run a completed orchestration +// 5. Purge โ€” delete orchestration history +// 6. Tags โ€” attach metadata to orchestrations import * as dotenv from "dotenv"; import * as path from "path"; @@ -24,7 +23,6 @@ import { ActivityContext, TOrchestrator, OrchestrationStatus, - terminateOptions, } from "@microsoft/durabletask-js"; // --------------------------------------------------------------------------- @@ -49,12 +47,6 @@ const longRunning: TOrchestrator = async function* (ctx: OrchestrationContext): return "Completed normally"; }; -/** Parent orchestration with a child (for recursive terminate demo). */ -const parentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { - const childResult: string = yield ctx.callSubOrchestrator(longRunning); - return `Parent got: ${childResult}`; -}; - /** * Continue-as-new orchestration โ€” processes a batch, then restarts with new input. * Stops after processing 3 batches total. @@ -102,7 +94,6 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon .connectionString(connectionString) .logger(logger) .addOrchestrator(longRunning) - .addOrchestrator(parentOrchestrator) .addOrchestrator(batchProcessor) .addOrchestrator(simpleOrchestrator) .addActivity(doWork) @@ -121,16 +112,8 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon console.log(`Status: ${OrchestrationStatus[termState!.runtimeStatus]}`); console.log(`Output: ${termState?.serializedOutput}`); - // --- 2. Terminate (recursive) --- - console.log("\n=== 2. Terminate (recursive โ€” parent + child) ==="); - const parentId = await client.scheduleNewOrchestration(parentOrchestrator); - await new Promise((r) => setTimeout(r, 3000)); // let child start - await client.terminateOrchestration(parentId, terminateOptions({ output: "Force stop", recursive: true })); - const parentState = await client.waitForOrchestrationCompletion(parentId, true, 15); - console.log(`Parent status: ${OrchestrationStatus[parentState!.runtimeStatus]}`); - - // --- 3. Suspend / Resume --- - console.log("\n=== 3. Suspend / Resume ==="); + // --- 2. Suspend / Resume --- + console.log("\n=== 2. Suspend / Resume ==="); const suspId = await client.scheduleNewOrchestration(longRunning); await client.waitForOrchestrationStart(suspId); @@ -147,15 +130,15 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon const suspFinal = await client.waitForOrchestrationCompletion(suspId, true, 15); console.log(`Final result: ${suspFinal?.serializedOutput}`); - // --- 4. Continue-as-new --- - console.log("\n=== 4. Continue-as-new ==="); + // --- 3. Continue-as-new --- + console.log("\n=== 3. Continue-as-new ==="); const canId = await client.scheduleNewOrchestration(batchProcessor, { batchNum: 0, processed: 0 }); const canState = await client.waitForOrchestrationCompletion(canId, true, 30); console.log(`Status: ${OrchestrationStatus[canState!.runtimeStatus]}`); console.log(`Result: ${canState?.serializedOutput}`); - // --- 5. Restart --- - console.log("\n=== 5. Restart Orchestration ==="); + // --- 4. Restart --- + console.log("\n=== 4. Restart Orchestration ==="); const origId = await client.scheduleNewOrchestration(simpleOrchestrator, "original-run"); await client.waitForOrchestrationCompletion(origId, true, 15); console.log(`Original completed: ${origId}`); @@ -165,8 +148,8 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon console.log(`Restarted as new ID: ${restartedId}`); console.log(`Result: ${restartState?.serializedOutput}`); - // --- 6. Purge --- - console.log("\n=== 6. Purge Orchestration ==="); + // --- 5. Purge --- + console.log("\n=== 5. Purge Orchestration ==="); const purgeId = await client.scheduleNewOrchestration(simpleOrchestrator, "to-be-purged"); await client.waitForOrchestrationCompletion(purgeId, true, 15); @@ -176,8 +159,8 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon const purgedState = await client.getOrchestrationState(purgeId); console.log(`State after purge: ${purgedState ?? "undefined (deleted)"}`); - // --- 7. Tags --- - console.log("\n=== 7. Orchestration Tags ==="); + // --- 6. Tags --- + console.log("\n=== 6. Orchestration Tags ==="); const tags = { environment: "staging", owner: "demo-user", priority: "high" }; const tagId = await client.scheduleNewOrchestration(simpleOrchestrator, "tagged-run", { tags }); const tagState = await client.waitForOrchestrationCompletion(tagId, true, 15); From a8d13ded6990b8ac7ad5b1646060ff46aa1df188 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:31:12 -0800 Subject: [PATCH 17/18] Refactor history event logging to use consistent property names for improved clarity --- examples/azure-managed/query-and-history/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/azure-managed/query-and-history/index.ts b/examples/azure-managed/query-and-history/index.ts index b82cfc0..0cb13b3 100644 --- a/examples/azure-managed/query-and-history/index.ts +++ b/examples/azure-managed/query-and-history/index.ts @@ -162,22 +162,21 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon const history: HistoryEvent[] = await client.getOrchestrationHistory(richId); console.log(` History for ${richId}: ${history.length} events`); for (const event of history) { - const eventTypeName = HistoryEventType[event.eventType] || `Unknown(${event.eventType})`; - console.log(` [${event.eventId}] ${eventTypeName} @ ${event.timestamp?.toISOString()}`); + console.log(` [${event.eventId}] ${event.type} @ ${event.timestamp?.toISOString()}`); } // --- 4. Typed history events --- console.log("\n=== 4. Typed History Event Inspection ==="); const executionStarted = history.find( - (e) => e.eventType === HistoryEventType.ExecutionStarted, + (e) => e.type === HistoryEventType.ExecutionStarted, ) as ExecutionStartedEvent | undefined; if (executionStarted) { console.log(` ExecutionStarted: name=${executionStarted.name}, input=${executionStarted.input}`); } const taskScheduled = history.filter( - (e) => e.eventType === HistoryEventType.TaskScheduled, + (e) => e.type === HistoryEventType.TaskScheduled, ) as TaskScheduledEvent[]; console.log(` TaskScheduled events: ${taskScheduled.length}`); for (const ts of taskScheduled) { @@ -185,12 +184,12 @@ const simpleOrchestrator: TOrchestrator = async function* (ctx: OrchestrationCon } const taskCompleted = history.filter( - (e) => e.eventType === HistoryEventType.TaskCompleted, + (e) => e.type === HistoryEventType.TaskCompleted, ) as TaskCompletedEvent[]; console.log(` TaskCompleted events: ${taskCompleted.length}`); const timerCreated = history.filter( - (e) => e.eventType === HistoryEventType.TimerCreated, + (e) => e.type === HistoryEventType.TimerCreated, ) as TimerCreatedEvent[]; console.log(` TimerCreated events: ${timerCreated.length}`); for (const tc of timerCreated) { From 4ef83f48eaefddc0dde42637267cc5e90713de9c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:51:13 -0800 Subject: [PATCH 18/18] Refactor retry handler naming for consistency and clarity in error handling sample --- .../lifecycle-management/index.ts | 1 + .../retry-and-error-handling/index.ts | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/azure-managed/lifecycle-management/index.ts b/examples/azure-managed/lifecycle-management/index.ts index e9d8541..6001257 100644 --- a/examples/azure-managed/lifecycle-management/index.ts +++ b/examples/azure-managed/lifecycle-management/index.ts @@ -23,6 +23,7 @@ import { ActivityContext, TOrchestrator, OrchestrationStatus, + terminateOptions, } from "@microsoft/durabletask-js"; // --------------------------------------------------------------------------- diff --git a/examples/azure-managed/retry-and-error-handling/index.ts b/examples/azure-managed/retry-and-error-handling/index.ts index 965159a..078568c 100644 --- a/examples/azure-managed/retry-and-error-handling/index.ts +++ b/examples/azure-managed/retry-and-error-handling/index.ts @@ -4,7 +4,7 @@ // This sample demonstrates retry and error handling in the Durable Task SDK: // 1. RetryPolicy โ€” declarative retry with exponential backoff // 2. handleFailure predicate โ€” selectively retry based on error type -// 3. AsyncRetryHandler โ€” imperative retry logic with custom delays +// 3. RetryHandler โ€” imperative retry logic with custom control // 4. Sub-orchestration retry โ€” retry an entire sub-orchestration // 5. raiseIfFailed() โ€” convenient error checking on orchestration state @@ -24,7 +24,7 @@ import { RetryPolicy, OrchestrationStatus, } from "@microsoft/durabletask-js"; -import type { AsyncRetryHandler, RetryContext, TaskRetryOptions } from "@microsoft/durabletask-js"; +import type { RetryHandler, RetryContext } from "@microsoft/durabletask-js"; // --------------------------------------------------------------------------- // Activities @@ -105,24 +105,28 @@ const handleFailureOrchestrator: TOrchestrator = async function* (ctx: Orchestra }; /** - * 3. AsyncRetryHandler โ€” custom imperative retry logic. - * Implements a custom delay strategy: 100ms, 200ms, 400ms, then gives up. + * 3. RetryHandler โ€” custom imperative retry logic. + * Uses a sync handler that retries up to maxAttempts times. + * Returns true for immediate retry, or false to stop. */ const customRetryHandlerOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { const maxAttempts = 4; - const customRetryHandler: AsyncRetryHandler = async (retryCtx: RetryContext) => { + const customRetryHandler: RetryHandler = (retryCtx: RetryContext) => { if (retryCtx.lastAttemptNumber >= maxAttempts) { return false; // give up } - // Exponential delay: 100ms, 200ms, 400ms - return 100 * Math.pow(2, retryCtx.lastAttemptNumber - 1); + // Only retry transient errors + if (!retryCtx.lastFailure.message?.includes("TransientError")) { + return false; + } + return true; // retry immediately }; const result: string = yield ctx.callActivity( unreliableActivity, { key: `custom-handler-${ctx.instanceId}`, failCount: 2 }, - { retry: customRetryHandler as TaskRetryOptions }, + { retry: customRetryHandler }, ); return result; @@ -206,8 +210,8 @@ const alwaysFailOrchestrator: TOrchestrator = async function* (ctx: Orchestratio console.log(`Status: ${OrchestrationStatus[state2!.runtimeStatus]}`); console.log(`Result: ${state2?.serializedOutput}`); - // --- 3. Custom AsyncRetryHandler --- - console.log("\n=== 3. Custom AsyncRetryHandler ==="); + // --- 3. Custom RetryHandler --- + console.log("\n=== 3. Custom RetryHandler ==="); const id3 = await client.scheduleNewOrchestration(customRetryHandlerOrchestrator); const state3 = await client.waitForOrchestrationCompletion(id3, true, 60); console.log(`Status: ${OrchestrationStatus[state3!.runtimeStatus]}`); @@ -233,7 +237,7 @@ const alwaysFailOrchestrator: TOrchestrator = async function* (ctx: Orchestratio } if (state5?.failureDetails) { console.log(`Failure type: ${state5.failureDetails.errorType}`); - console.log(`Failure message: ${state5.failureDetails.errorMessage?.substring(0, 80)}`); + console.log(`Failure message: ${state5.failureDetails.message?.substring(0, 80)}`); } console.log("\n=== All retry/error demos completed successfully! ===");