Skip to content

Commit fac8d00

Browse files
mjameswhclaude
andauthored
Allow WorkerDeploymentOptions without defaultVersioningBehavior when useWorkerVersioning is false (#1963)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ecf2d49 commit fac8d00

5 files changed

Lines changed: 130 additions & 31 deletions

File tree

packages/core-bridge/src/worker.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ mod config {
550550
pub struct WorkerDeploymentOptions {
551551
version: WorkerDeploymentVersion,
552552
use_worker_versioning: bool,
553-
default_versioning_behavior: VersioningBehavior,
553+
default_versioning_behavior: Option<VersioningBehavior>,
554554
}
555555

556556
#[derive(TryFromJs)]
@@ -659,7 +659,7 @@ mod config {
659659
Self {
660660
version: val.version.into(),
661661
use_worker_versioning: val.use_worker_versioning,
662-
default_versioning_behavior: Some(val.default_versioning_behavior.into()),
662+
default_versioning_behavior: val.default_versioning_behavior.map(Into::into),
663663
}
664664
}
665665
}

packages/core-bridge/ts/native.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export type PollerBehavior =
257257
export type WorkerDeploymentOptions = {
258258
version: WorkerDeploymentVersion;
259259
useWorkerVersioning: boolean;
260-
defaultVersioningBehavior: VersioningBehavior;
260+
defaultVersioningBehavior: Option<VersioningBehavior>;
261261
};
262262

263263
export type WorkerDeploymentVersion = {

packages/test/src/test-worker-deployment-versioning.ts

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
*
44
* @module
55
*/
6-
import assert from 'assert';
76
import { randomUUID } from 'crypto';
87
import asyncRetry from 'async-retry';
98
import { ExecutionContext } from 'ava';
109
import { Client } from '@temporalio/client';
1110
import { toCanonicalString, WorkerDeploymentVersion } from '@temporalio/common';
1211
import { temporal } from '@temporalio/proto';
12+
import { WorkerOptions } from '@temporalio/worker';
1313
import { Worker } from './helpers';
1414
import { Context, helpers, makeTestFunction } from './helpers-integration';
1515
import { unblockSignal, versionQuery } from './workflows';
1616

17+
type WorkerDeploymentOptions = NonNullable<WorkerOptions['workerDeploymentOptions']>;
18+
1719
const test = makeTestFunction({ workflowsPath: __filename });
1820

1921
test('Worker deployment based versioning', async (t) => {
@@ -94,7 +96,7 @@ test('Worker deployment based versioning', async (t) => {
9496
workflowId: 'deployment-versioning-v1-' + randomUUID(),
9597
});
9698
const state1 = await wf1.query(versionQuery);
97-
assert.equal(state1, 'v1');
99+
t.is(state1, 'v1');
98100

99101
// Wait for worker 2 to be visible and set as current version
100102
const describeResp2 = await waitUntilWorkerDeploymentVisible(client, w2DeploymentVersion);
@@ -106,7 +108,7 @@ test('Worker deployment based versioning', async (t) => {
106108
workflowId: 'deployment-versioning-v2-' + randomUUID(),
107109
});
108110
const state2 = await wf2.query(versionQuery);
109-
assert.equal(state2, 'v2');
111+
t.is(state2, 'v2');
110112

111113
// Wait for worker 3 to be visible and set as current version
112114
const describeResp3 = await waitUntilWorkerDeploymentVisible(client, w3DeploymentVersion);
@@ -118,7 +120,7 @@ test('Worker deployment based versioning', async (t) => {
118120
workflowId: 'deployment-versioning-v3-' + randomUUID(),
119121
});
120122
const state3 = await wf3.query(versionQuery);
121-
assert.equal(state3, 'v3');
123+
t.is(state3, 'v3');
122124

123125
// Signal all workflows to finish
124126
await wf1.signal(unblockSignal);
@@ -129,17 +131,16 @@ test('Worker deployment based versioning', async (t) => {
129131
const res2 = await wf2.result();
130132
const res3 = await wf3.result();
131133

132-
assert.equal(res1, 'version-v3');
133-
assert.equal(res2, 'version-v2');
134-
assert.equal(res3, 'version-v3');
134+
t.is(res1, 'version-v3');
135+
t.is(res2, 'version-v2');
136+
t.is(res3, 'version-v3');
135137

136138
worker1.shutdown();
137139
worker2.shutdown();
138140
worker3.shutdown();
139141
await worker1Promise;
140142
await worker2Promise;
141143
await worker3Promise;
142-
t.pass();
143144
});
144145

145146
test('Worker deployment based versioning with ramping', async (t) => {
@@ -205,7 +206,7 @@ test('Worker deployment based versioning with ramping', async (t) => {
205206
});
206207
await wf.signal(unblockSignal);
207208
const res = await wf.result();
208-
assert.equal(res, 'version-v2');
209+
t.is(res, 'version-v2');
209210
}
210211

211212
// Set ramp to 0, expecting workflows to run on v1
@@ -217,7 +218,7 @@ test('Worker deployment based versioning with ramping', async (t) => {
217218
});
218219
await wf.signal(unblockSignal);
219220
const res = await wf.result();
220-
assert.equal(res, 'version-v1');
221+
t.is(res, 'version-v1');
221222
}
222223

223224
// Set ramp to 50 and eventually verify workflows run on both versions
@@ -257,7 +258,7 @@ async function testWorkerDeploymentWithDynamicBehavior(
257258
expectedResult: string
258259
) {
259260
if (t.context.env.supportsTimeSkipping) {
260-
t.pass("Test Server doesn't support worker deployments");
261+
t.fail("Test Server doesn't support worker deployments");
261262
return;
262263
}
263264

@@ -295,7 +296,7 @@ async function testWorkerDeploymentWithDynamicBehavior(
295296
});
296297

297298
const result = await wf.result();
298-
assert.equal(result, expectedResult);
299+
t.is(result, expectedResult);
299300

300301
const history = await wf.fetchHistory();
301302
const hasPinnedVersioningBehavior = history.events!.some(
@@ -304,11 +305,10 @@ async function testWorkerDeploymentWithDynamicBehavior(
304305
event.workflowTaskCompletedEventAttributes.versioningBehavior ===
305306
temporal.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_PINNED
306307
);
307-
assert.ok(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior');
308+
t.true(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior');
308309

309310
worker.shutdown();
310311
await workerPromise;
311-
t.pass();
312312
}
313313

314314
test('Worker deployment with dynamic workflow static behavior', async (t) => {
@@ -362,11 +362,10 @@ test('Workflows can use default versioning behavior', async (t) => {
362362
event.workflowTaskCompletedEventAttributes.versioningBehavior ===
363363
temporal.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_PINNED
364364
);
365-
assert.ok(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior');
365+
t.true(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior');
366366

367367
worker.shutdown();
368368
await workerPromise;
369-
t.pass();
370369
});
371370

372371
test('Workflow versioningOverride overrides default versioning behavior', async (t) => {
@@ -406,7 +405,7 @@ test('Workflow versioningOverride overrides default versioning behavior', async
406405
},
407406
});
408407
const statePinned = await wfPinned.query(versionQuery);
409-
assert.equal(statePinned, 'v1');
408+
t.is(statePinned, 'v1');
410409

411410
await wfPinned.signal(unblockSignal);
412411

@@ -418,14 +417,13 @@ test('Workflow versioningOverride overrides default versioning behavior', async
418417
temporal.api.enums.v1.VersioningBehavior.VERSIONING_BEHAVIOR_PINNED ||
419418
event.workflowExecutionStartedEventAttributes?.versioningOverride?.pinned != null
420419
);
421-
assert.ok(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior');
420+
t.true(hasPinnedVersioningBehavior, 'Expected workflow to use pinned versioning behavior');
422421

423422
const resPinned = await wfPinned.result();
424-
assert.equal(resPinned, 'version-v1');
423+
t.is(resPinned, 'version-v1');
425424

426425
worker1.shutdown();
427426
await worker1Promise;
428-
t.pass();
429427
});
430428

431429
async function setRampingVersion(
@@ -474,3 +472,85 @@ async function setCurrentDeploymentVersion(
474472
conflictToken,
475473
});
476474
}
475+
476+
///////////////////////////////
477+
478+
/**
479+
* Type-level tests for WorkerDeploymentOptions.
480+
*
481+
* Ensures that:
482+
* - When useWorkerVersioning is true, defaultVersioningBehavior is required
483+
* - When useWorkerVersioning is false, defaultVersioningBehavior must be absent
484+
* - version is always required
485+
*/
486+
487+
test('Worker with deployment options and useWorkerVersioning false can run workflows', async (t) => {
488+
const taskQueue = 'versioning-off-with-build-id-' + randomUUID();
489+
const buildId = 'my-custom-build-id-1.0';
490+
const { client, nativeConnection } = t.context.env;
491+
492+
const worker = await Worker.create({
493+
workflowsPath: require.resolve('./workflows'),
494+
taskQueue,
495+
workerDeploymentOptions: {
496+
useWorkerVersioning: false,
497+
version: {
498+
buildId,
499+
deploymentName: 'deployment-' + randomUUID(),
500+
},
501+
},
502+
connection: nativeConnection,
503+
});
504+
505+
const handle = await client.workflow.start('successString', {
506+
taskQueue,
507+
workflowId: 'versioning-off-build-id-' + randomUUID(),
508+
});
509+
510+
await worker.runUntil(handle.result());
511+
t.is(await handle.result(), 'success');
512+
513+
const history = await handle.fetchHistory();
514+
const buildIdInHistory = history.events!.some(
515+
(event) => event.workflowTaskCompletedEventAttributes?.workerVersion?.buildId === buildId
516+
);
517+
t.true(buildIdInHistory, 'Expected build ID to appear in workflow history');
518+
});
519+
520+
test('WorkerDeploymentOptions with useWorkerVersioning true requires defaultVersioningBehavior', (t) => {
521+
const valid: WorkerDeploymentOptions = {
522+
version: { buildId: '1.0', deploymentName: 'my-deployment' },
523+
useWorkerVersioning: true,
524+
defaultVersioningBehavior: 'AUTO_UPGRADE',
525+
};
526+
527+
const validPinned: WorkerDeploymentOptions = {
528+
version: { buildId: '1.0', deploymentName: 'my-deployment' },
529+
useWorkerVersioning: true,
530+
defaultVersioningBehavior: 'PINNED',
531+
};
532+
533+
// @ts-expect-error defaultVersioningBehavior is required when useWorkerVersioning is true
534+
const missingBehavior: WorkerDeploymentOptions = {
535+
version: { buildId: '1.0', deploymentName: 'my-deployment' },
536+
useWorkerVersioning: true,
537+
};
538+
539+
t.pass();
540+
});
541+
542+
test('WorkerDeploymentOptions with useWorkerVersioning false does not allow defaultVersioningBehavior', (t) => {
543+
const _validNoVersioning: WorkerDeploymentOptions = {
544+
version: { buildId: '1.0', deploymentName: 'my-deployment' },
545+
useWorkerVersioning: false,
546+
};
547+
548+
const invalidWithBehavior: WorkerDeploymentOptions = {
549+
version: { buildId: '1.0', deploymentName: 'my-deployment' },
550+
useWorkerVersioning: false,
551+
// @ts-expect-error defaultVersioningBehavior must not be specified when useWorkerVersioning is false
552+
defaultVersioningBehavior: 'AUTO_UPGRADE',
553+
};
554+
555+
t.pass();
556+
});

packages/worker/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export { DataConverter, defaultPayloadConverter, State, Worker, WorkerStatus } f
3939
export {
4040
CompiledWorkerOptions,
4141
ReplayWorkerOptions,
42+
WorkerDeploymentOptions,
4243
WorkerOptions,
4344
WorkerPlugin,
4445
WorkflowBundle,

packages/worker/src/worker-options.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -641,11 +641,21 @@ export type WorkerDeploymentOptions = {
641641
useWorkerVersioning: boolean;
642642

643643
/**
644-
* The default versioning behavior to use for all workflows on this worker. Specifying a default
645-
* behavior is required.
644+
* The default versioning behavior to use for all workflows on this worker.
645+
*
646+
* Required if {@link useWorkerVersioning} is `true`; should be left unset otherwise.
646647
*/
647-
defaultVersioningBehavior: VersioningBehavior;
648-
};
648+
defaultVersioningBehavior?: VersioningBehavior | undefined;
649+
} & (
650+
| {
651+
useWorkerVersioning: true;
652+
defaultVersioningBehavior: VersioningBehavior;
653+
}
654+
| {
655+
useWorkerVersioning: false;
656+
defaultVersioningBehavior?: never;
657+
}
658+
);
649659

650660
// Replay Worker ///////////////////////////////////////////////////////////////////////////////////
651661

@@ -1126,21 +1136,29 @@ function toNativeDeploymentOptions(options?: WorkerDeploymentOptions): native.Wo
11261136
if (options === undefined) {
11271137
return null;
11281138
}
1139+
if (!options.useWorkerVersioning) {
1140+
return {
1141+
version: options.version,
1142+
useWorkerVersioning: false,
1143+
defaultVersioningBehavior: null,
1144+
};
1145+
}
1146+
const { defaultVersioningBehavior } = options;
11291147
let vb: native.VersioningBehavior;
1130-
switch (options.defaultVersioningBehavior) {
1148+
switch (defaultVersioningBehavior) {
11311149
case 'PINNED':
11321150
vb = { type: 'pinned' };
11331151
break;
11341152
case 'AUTO_UPGRADE':
11351153
vb = { type: 'auto-upgrade' };
11361154
break;
11371155
default:
1138-
options.defaultVersioningBehavior satisfies never;
1139-
throw new Error(`Unknown versioning behavior: ${options.defaultVersioningBehavior}`);
1156+
defaultVersioningBehavior satisfies never;
1157+
throw new Error(`Unknown versioning behavior: ${defaultVersioningBehavior}`);
11401158
}
11411159
return {
11421160
version: options.version,
1143-
useWorkerVersioning: options.useWorkerVersioning,
1161+
useWorkerVersioning: true,
11441162
defaultVersioningBehavior: vb,
11451163
};
11461164
}

0 commit comments

Comments
 (0)