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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

### New

- Expose readonly `OrchestrationContext.name` from execution-start history, preserving logical
aliases and case on initial execution and replay. `RuntimeOrchestrationContext` constructor
arguments are unchanged; manually constructed contexts have an empty name until initialized.
- Add an optional `reason` to client `suspendOrchestration()` and `resumeOrchestration()`,
forwarded unchanged to the service and recorded in in-memory test history. Empty strings
are preserved; omitted reasons remain absent.
Expand Down Expand Up @@ -57,6 +60,8 @@

### Breaking changes

- Custom `OrchestrationContext` subclasses and typed test doubles must implement the new
`name` getter/property. Orchestrators consuming SDK-provided contexts require no changes.
- Activity calls without an explicit version now inherit the parent's instance version, and
child calls inherit the worker's `versioning.defaultVersion`; explicit `""` selects unversioned.
Worker acceptance checks now also apply to activities; `Strict` with an empty worker version
Expand Down
2 changes: 2 additions & 0 deletions packages/azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### New

- Expose readonly `context.df.name`, forwarding the core logical orchestration name from history
and preserving aliases and case across replay. This does not add `InvocationContext.functionName`.
- Add optional orchestration version migration support to `context.df.continueAsNew()`.
- Added a `durable-functions/testing` entry point with `runOrchestrator`, which runs an orchestrator
to a terminal state against inline activity implementations on the in-memory backend and always
Expand Down
5 changes: 5 additions & 0 deletions packages/azure-functions-durable/src/orchestration-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export class DurableOrchestrationContext {
private readonly _input: unknown,
) {}

/** The recorded logical orchestration name, preserving aliases and case across replay. */
get name(): string {
return this._ctx.name;
}

/** The instance ID of the current orchestration. */
get instanceId(): string {
return this._ctx.instanceId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function createFakeCoreContext() {
debug: jest.fn(),
};
const ctx = {
name: "LogicalOrch",
instanceId: "instance-1",
isReplaying: true,
currentUtcDateTime: new Date("2026-01-02T03:04:05.000Z"),
Expand All @@ -59,6 +60,7 @@ describe("DurableOrchestrationContext", () => {
const { ctx } = createFakeCoreContext();
const df = new DurableOrchestrationContext(ctx, { city: "Tokyo" });

expect(df.name).toBe("LogicalOrch");
expect(df.instanceId).toBe("instance-1");
expect(df.isReplaying).toBe(true);
expect(df.currentUtcDateTime).toEqual(new Date("2026-01-02T03:04:05.000Z"));
Expand Down Expand Up @@ -408,9 +410,10 @@ describe("wrapOrchestrator end-to-end (real core executor)", () => {

// Classic v3 sync generator using context.df.*; wrapOrchestrator wraps it so the engine drives it.
const classic = function* (context: ClassicOrchestrationContext): Generator<Task<unknown>, string, unknown> {
const initialName = context.df.name;
const input = context.df.getInput<string>();
const result = (yield context.df.callActivity("echo", input)) as string;
return `classic-done:${result}`;
return `${initialName}:${context.df.name}:${result}`;
};
worker.addNamedOrchestrator("classicOrch", wrapOrchestrator(classic));

Expand All @@ -421,7 +424,7 @@ describe("wrapOrchestrator end-to-end (real core executor)", () => {
const state = await client.waitForOrchestrationCompletion(id, true, 10);

expect(state?.runtimeStatus).toBe(OrchestrationStatus.COMPLETED);
expect(state?.serializedOutput).toBe(JSON.stringify("classic-done:echo:IN"));
expect(state?.serializedOutput).toBe(JSON.stringify("classicOrch:classicOrch:echo:IN"));
} finally {
await worker.stop();
}
Expand Down
10 changes: 10 additions & 0 deletions packages/durabletask-js/src/task/context/orchestration-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import { OrchestrationEntityFeature } from "../../entities/orchestration-entity-
import { compareVersions } from "../../utils/versioning.util";

export abstract class OrchestrationContext {
/**
* Gets the logical name used to schedule the current orchestration.
*
* This is the name recorded in orchestration history, preserving aliases and case,
* not necessarily the JavaScript function name. It remains the same during replay.
*
* @returns {string} The logical name of the currently executing orchestration.
*/
abstract get name(): string;

/**
* The instance ID of the currently executing orchestration.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/durabletask-js/src/worker/orchestration-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ export class OrchestrationExecutor {
// Log orchestration start (EventId 600)
WorkerLogs.orchestrationStarted(this._logger, ctx._instanceId, this._orchestratorName);

// Set the version from the execution started event
// Set the name and version from the execution started event
ctx._name = executionStartedEvent?.getName() ?? "";
ctx._version = executionStartedEvent?.getVersion()?.getValue() ?? "";

// Extract parent instance info if this is a sub-orchestration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
_newGuidCounter: number;
_currentUtcDatetime: Date;
_instanceId: string;
_name: string;
_executionId: string = "";
_version: string;
_parent?: ParentOrchestrationInstance;
Expand Down Expand Up @@ -75,6 +76,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
this._newGuidCounter = 0;
this._currentUtcDatetime = new Date(1000, 0, 1);
this._instanceId = instanceId;
this._name = "";
this._version = "";
this._parent = undefined;
this._completionStatus = undefined;
Expand All @@ -87,6 +89,11 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
this._entityFeature = new RuntimeOrchestrationEntityFeature(this);
}

/** The recorded logical name, or an empty string before execution history initializes the context. */
get name(): string {
return this._name;
}

get instanceId(): string {
return this._instanceId;
}
Expand Down
54 changes: 54 additions & 0 deletions packages/durabletask-js/test/orchestration_executor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,61 @@ const testLogger = new NoOpLogger();

const TEST_INSTANCE_ID = "abc123";

describe("Orchestration Context", () => {
it("keeps manual construction compatible with an empty, getter-only name", () => {
const ctx = new RuntimeOrchestrationContext(TEST_INSTANCE_ID);
expect(ctx).toMatchObject({ instanceId: TEST_INSTANCE_ID, name: "", version: "" });
expect(Object.getOwnPropertyDescriptor(RuntimeOrchestrationContext.prototype, "name")).toMatchObject({
get: expect.any(Function),
set: undefined,
});
expect(Reflect.set(ctx, "name", "changed")).toBe(false);
});
});

describe("Orchestration Executor", () => {
it.each([undefined, "1.0.0"])(
"preserves each logical name on initial execution and replay with registration version %s",
async (registrationVersion) => {
async function* implementation(ctx: OrchestrationContext) {
const initialName = ctx.name;
ctx.setCustomStatus([initialName, ctx.isReplaying]);
yield ctx.callActivity("Work");
return [initialName, ctx.name, ctx.instanceId, ctx.version, ctx.isReplaying];
}
const registry = new Registry();
const names = ["LogicalFlow", "logicalFlow"];
for (const name of names) {
registry.addNamedOrchestrator(name, implementation, registrationVersion);
}

for (const name of names) {
const startEvents = [
newOrchestratorStartedEvent(),
newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, undefined, undefined, "1.0.0"),
];
const initial = await new OrchestrationExecutor(registry, testLogger, undefined, "worker-default").execute(
TEST_INSTANCE_ID,
[],
startEvents,
);
expect(initial.customStatus).toBe(JSON.stringify([name, false]));
expect(initial.actions).toHaveLength(1);
expect(initial.actions[0].getScheduletask()?.getName()).toBe("Work");

const replay = await new OrchestrationExecutor(registry, testLogger, undefined, "worker-default").execute(
TEST_INSTANCE_ID,
[...startEvents, newTaskScheduledEvent(1, "Work")],
[newTaskCompletedEvent(1, '"done"')],
);
expect(replay.customStatus).toBe(JSON.stringify([name, true]));
const completed = getAndValidateSingleCompleteOrchestrationAction(replay);
expect(completed?.getOrchestrationstatus()).toBe(pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED);
expect(completed?.getResult()?.getValue()).toBe(JSON.stringify([name, name, TEST_INSTANCE_ID, "1.0.0", false]));
}
},
);

it("should validate the orchestrator function input population", async () => {
const orchestrator: TOrchestrator = async (ctx: OrchestrationContext, input: any) => {
// return all orchestrator inputs back as the output
Expand Down
Loading