Skip to content

Commit 0c76c49

Browse files
authored
Allow overriding orchestration version when starting orchestrations via APIs (#658)
1 parent 2ae9699 commit 0c76c49

File tree

11 files changed

+201
-24
lines changed

11 files changed

+201
-24
lines changed

samples-ts/functions/httpStart.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,25 @@ const httpStart: HttpHandler = async (
77
): Promise<HttpResponse> => {
88
const client = df.getClient(context);
99
const body: unknown = await request.json();
10-
const instanceId: string = await client.startNew(request.params.orchestratorName, {
11-
input: body,
12-
});
1310

14-
context.log(`Started orchestration with ID = '${instanceId}'.`);
11+
// Get optional version from query parameter
12+
const version = request.query.get("version");
13+
14+
let instanceId: string;
15+
if (version) {
16+
// Override the orchestration version
17+
instanceId = await client.startNew(request.params.orchestratorName, {
18+
input: body,
19+
version: version,
20+
});
21+
context.log(`Started orchestration with ID = '${instanceId}' and version = '${version}'.`);
22+
} else {
23+
// Use defaultVersion from host.json
24+
instanceId = await client.startNew(request.params.orchestratorName, {
25+
input: body,
26+
});
27+
context.log(`Started orchestration with ID = '${instanceId}'.`);
28+
}
1529

1630
return client.createCheckStatusResponse(request, instanceId);
1731
};

samples-ts/functions/orchestrationVersion.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@ const versionedOrchestrator: OrchestrationHandler = function* (context: Orchestr
1818
yield context.df.waitForExternalEvent("Continue");
1919
context.df.setCustomStatus("Continue event received");
2020

21-
// New sub-orchestrations will use the current defaultVersion specified in host.json
21+
// You can explicitly pass a version to sub-orchestrators
22+
const subOrchestratorWithVersionResult = yield context.df.callSubOrchestrator(
23+
"versionedSuborchestrator",
24+
undefined, // input
25+
undefined, // instanceId
26+
"0.9" // version override
27+
);
28+
29+
// Without specifying version, the sub-orchestrator will use the current defaultVersion
2230
const subOrchestratorResult = yield context.df.callSubOrchestrator("versionedSuborchestrator");
2331

2432
return [
2533
`Orchestration version: ${context.df.version}`,
26-
`Suborchestration version: ${subOrchestratorResult}`,
34+
`Suborchestration version (explicit): ${subOrchestratorWithVersionResult}`,
35+
`Suborchestration version (default): ${subOrchestratorResult}`,
2736
`Activity result: ${activityResult}`,
2837
];
2938
};

samples-ts/host.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"extensionBundle": {
1212
"id": "Microsoft.Azure.Functions.ExtensionBundle",
13-
"version": "[3.15.0, 4.0.0)"
13+
"version": "[4.26.0, 5.0.0)"
1414
},
1515
"extensions": {
1616
"durableTask": {

src/actions/CallSubOrchestratorAction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export class CallSubOrchestratorAction implements IAction {
1010
constructor(
1111
public readonly functionName: string,
1212
public readonly instanceId?: string,
13-
input?: unknown
13+
input?: unknown,
14+
public readonly version?: string
1415
) {
1516
this.input = input;
1617
Utils.throwIfEmpty(functionName, "functionName");

src/actions/CallSubOrchestratorWithRetryAction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export class CallSubOrchestratorWithRetryAction implements IAction {
1212
public readonly functionName: string,
1313
public readonly retryOptions: RetryOptions,
1414
input?: unknown,
15-
public readonly instanceId?: string
15+
public readonly instanceId?: string,
16+
public readonly version?: string
1617
) {
1718
this.input = input;
1819
Utils.throwIfEmpty(functionName, "functionName");

src/durableClient/DurableClient.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,16 +484,26 @@ export class DurableClient implements types.DurableClient {
484484
const instanceIdPath: string = options?.instanceId ? `/${options.instanceId}` : "";
485485
if (this.clientData.rpcBaseUrl) {
486486
// Fast local RPC path
487-
requestUrl = new URL(
487+
const urlObj = new URL(
488488
`orchestrators/${orchestratorFunctionName}${instanceIdPath}`,
489489
this.clientData.rpcBaseUrl
490-
).href;
490+
);
491+
if (options?.version) {
492+
urlObj.searchParams.append("version", options.version);
493+
}
494+
requestUrl = urlObj.href;
491495
} else {
492496
// Legacy app frontend path
493497
requestUrl = this.clientData.creationUrls.createNewInstancePostUri;
494498
requestUrl = requestUrl
495499
.replace(this.functionNamePlaceholder, orchestratorFunctionName)
496500
.replace(this.instanceIdPlaceholder, instanceIdPath);
501+
if (options?.version) {
502+
const separator = requestUrl.includes("?") ? "&" : "?";
503+
requestUrl = `${requestUrl}${separator}version=${encodeURIComponent(
504+
options.version
505+
)}`;
506+
}
497507
}
498508

499509
const headers = this.getDistributedTracingHeaders();

src/orchestrations/DurableOrchestrationContext.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,19 @@ export class DurableOrchestrationContext implements types.DurableOrchestrationCo
201201
this.taskOrchestratorExecutor.recordFireAndForgetAction(action);
202202
}
203203

204-
public callSubOrchestrator(name: string, input?: unknown, instanceId?: string): Task {
204+
public callSubOrchestrator(
205+
name: string,
206+
input?: unknown,
207+
instanceId?: string,
208+
version?: string
209+
): Task {
205210
if (!name) {
206211
throw new Error(
207212
"A sub-orchestration function name must be provided when attempting to create a suborchestration"
208213
);
209214
}
210215

211-
const newAction = new CallSubOrchestratorAction(name, instanceId, input);
216+
const newAction = new CallSubOrchestratorAction(name, instanceId, input, version);
212217
const task = new AtomicTask(false, newAction);
213218
return task;
214219
}
@@ -217,7 +222,8 @@ export class DurableOrchestrationContext implements types.DurableOrchestrationCo
217222
name: string,
218223
retryOptions: types.RetryOptions,
219224
input?: unknown,
220-
instanceId?: string
225+
instanceId?: string,
226+
version?: string
221227
): Task {
222228
if (!name) {
223229
throw new Error(
@@ -229,7 +235,8 @@ export class DurableOrchestrationContext implements types.DurableOrchestrationCo
229235
name,
230236
retryOptions,
231237
input,
232-
instanceId
238+
instanceId,
239+
version
233240
);
234241
const backingTask = new AtomicTask(false, newAction);
235242
const task = new RetryableTask(backingTask, retryOptions);

test/integration/orchestrator-spec.ts

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,45 @@ describe("Orchestrator", () => {
14881488
);
14891489
});
14901490

1491+
it("schedules a versioned suborchestrator function", async () => {
1492+
const orchestrator = TestOrchestrations.SayHelloWithVersionedSubOrchestrator;
1493+
const name = "World";
1494+
const id = uuidv1();
1495+
const childId = `${id}:0`;
1496+
const mockContext = new DummyOrchestrationContext();
1497+
const orchestrationInput = new DurableOrchestrationInput(
1498+
id,
1499+
TestHistories.GetOrchestratorStart(
1500+
"SayHelloWithVersionedSubOrchestrator",
1501+
moment.utc().toDate()
1502+
),
1503+
name
1504+
);
1505+
1506+
const result = await orchestrator(orchestrationInput, mockContext);
1507+
1508+
expect(result).to.be.deep.equal(
1509+
new OrchestratorState(
1510+
{
1511+
isDone: false,
1512+
output: undefined,
1513+
actions: [
1514+
[
1515+
new CallSubOrchestratorAction(
1516+
"SayHelloWithActivity",
1517+
childId,
1518+
name,
1519+
"v2"
1520+
),
1521+
],
1522+
],
1523+
schemaVersion: ReplaySchema.V1,
1524+
},
1525+
true
1526+
)
1527+
);
1528+
});
1529+
14911530
it("schedules a suborchestrator function with no instanceId", async () => {
14921531
const orchestrator = TestOrchestrations.SayHelloWithSubOrchestratorNoSubId;
14931532
const name = "World";
@@ -1661,8 +1700,18 @@ describe("Orchestrator", () => {
16611700
.to.be.an("object")
16621701
.that.deep.include({
16631702
isDone: false,
1703+
// Note: In error paths, actions may be serialized without undefined fields.
1704+
// Using a plain object literal avoids setting expectations on the presence
1705+
// of any property that would cause deep.include to fail.
16641706
actions: [
1665-
[new CallSubOrchestratorAction("SayHelloWithActivity", childId, name)],
1707+
[
1708+
{
1709+
actionType: ActionType.CallSubOrchestrator,
1710+
functionName: "SayHelloWithActivity",
1711+
instanceId: childId,
1712+
input: name,
1713+
},
1714+
],
16661715
],
16671716
});
16681717
expect(orchestrationState.error).to.include(expectedErr);
@@ -1789,6 +1838,47 @@ describe("Orchestrator", () => {
17891838
);
17901839
});
17911840

1841+
it("schedules a versioned suborchestrator function with retry", async () => {
1842+
const orchestrator = TestOrchestrations.SayHelloWithVersionedSubOrchestratorRetry;
1843+
const name = "World";
1844+
const id = uuidv1();
1845+
const childId = `${id}:0`;
1846+
const mockContext = new DummyOrchestrationContext();
1847+
const orchestrationInput = new DurableOrchestrationInput(
1848+
id,
1849+
TestHistories.GetOrchestratorStart(
1850+
"SayHelloWithVersionedSubOrchestratorRetry",
1851+
moment.utc().toDate(),
1852+
name
1853+
),
1854+
name
1855+
);
1856+
1857+
const result = await orchestrator(orchestrationInput, mockContext);
1858+
1859+
expect(result).to.be.deep.equal(
1860+
new OrchestratorState(
1861+
{
1862+
isDone: false,
1863+
output: undefined,
1864+
actions: [
1865+
[
1866+
new CallSubOrchestratorWithRetryAction(
1867+
"SayHelloInline",
1868+
new RetryOptions(10000, 2),
1869+
name,
1870+
childId,
1871+
"v2"
1872+
),
1873+
],
1874+
],
1875+
schemaVersion: ReplaySchema.V1,
1876+
},
1877+
true
1878+
)
1879+
);
1880+
});
1881+
17921882
it("retries a failed suborchestrator function if < max attempts", async () => {
17931883
const orchestrator = TestOrchestrations.SayHelloWithSubOrchestratorRetry;
17941884
const name = "World";
@@ -1864,14 +1954,18 @@ describe("Orchestrator", () => {
18641954
.to.be.an("object")
18651955
.that.deep.include({
18661956
isDone: false,
1957+
// Note: In error paths, actions may be serialized without undefined fields.
1958+
// Using a plain object literal avoids setting expectations on the presence
1959+
// of any property that would cause deep.include to fail.
18671960
actions: [
18681961
[
1869-
new CallSubOrchestratorWithRetryAction(
1870-
"SayHelloInline",
1871-
new RetryOptions(10000, 2),
1872-
name,
1873-
childId
1874-
),
1962+
{
1963+
actionType: ActionType.CallSubOrchestratorWithRetry,
1964+
functionName: "SayHelloInline",
1965+
retryOptions: new RetryOptions(10000, 2),
1966+
instanceId: childId,
1967+
input: name,
1968+
},
18751969
],
18761970
],
18771971
});

test/testobjects/TestOrchestrations.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,20 @@ export class TestOrchestrations {
278278
return output;
279279
});
280280

281+
public static SayHelloWithVersionedSubOrchestrator = createOrchestrator(function* (
282+
context: OrchestrationContext
283+
) {
284+
const input = context.df.getInput();
285+
const childId = context.df.instanceId + ":0";
286+
const output = yield context.df.callSubOrchestrator(
287+
"SayHelloWithActivity",
288+
input,
289+
childId,
290+
"v2"
291+
);
292+
return output;
293+
});
294+
281295
public static SayHelloWithSubOrchestratorNoSubId: any = createOrchestrator(function* (
282296
context: OrchestrationContext
283297
) {
@@ -314,6 +328,22 @@ export class TestOrchestrations {
314328
return output;
315329
});
316330

331+
public static SayHelloWithVersionedSubOrchestratorRetry: any = createOrchestrator(function* (
332+
context: any
333+
) {
334+
const input = context.df.getInput();
335+
const childId = context.df.instanceId + ":0";
336+
const retryOptions = new df.RetryOptions(10000, 2);
337+
const output = yield context.df.callSubOrchestratorWithRetry(
338+
"SayHelloInline",
339+
retryOptions,
340+
input,
341+
childId,
342+
"v2"
343+
);
344+
return output;
345+
});
346+
317347
public static SayHelloWithSubOrchestratorRetryFanout: any = createOrchestrator(function* (
318348
context: any
319349
) {

types/durableClient.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,12 @@ export interface StartNewOptions {
372372
* JSON-serializable input value for the orchestrator function.
373373
*/
374374
input?: unknown;
375+
376+
/**
377+
* The version to use for the new orchestration instance. If not specified,
378+
* the default version from host.json will be used.
379+
*/
380+
version?: string;
375381
}
376382

377383
/**

0 commit comments

Comments
 (0)