Skip to content

Commit 8280086

Browse files
jqmmescjol
andauthored
[wrangler] Add Durable Objects hibernation timeout option (#15658)
Co-authored-by: Christopher Little-Savage <clittle-savage@cloudflare.com>
1 parent 45f2ff1 commit 8280086

27 files changed

Lines changed: 954 additions & 59 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"wrangler": minor
3+
"@cloudflare/deploy-helpers": minor
4+
"@cloudflare/workers-utils": minor
5+
---
6+
7+
Add Durable Objects code update strategies to Worker deployments
8+
9+
Use `--durable-objects-code-update-mode immediate` with `wrangler deploy`, `wrangler versions deploy`, and `wrangler rollback` to update code without waiting for active instances to hibernate. Use `--durable-objects-code-update-mode deferred 30s` to set a maximum delay, or configure `durable_objects.code_update_strategy` with `mode` and `max_delay`. When unset, the strategy defaults to deferred with a 5-minute maximum delay; delays cannot exceed 24 hours and must use millisecond precision.

‎packages/deploy-helpers/src/deploy/deploy.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ async function deployWorker(
430430
props.containers.source === undefined &&
431431
// Rollout skip can recover Container metadata absent from local config.
432432
containerMetadata === undefined;
433+
if (!canUseNewVersionsDeploymentsApi) {
434+
worker.code_update_strategy = props.durableObjectsCodeUpdateStrategy;
435+
}
433436

434437
let workerBundle: FormData;
435438
const dockerPath = getDockerPath();
@@ -527,13 +530,20 @@ async function deployWorker(
527530
// Deploy new version to 100%
528531
const versionMap = new Map<VersionId, Percentage>();
529532
versionMap.set(versionResult.id, 100);
533+
const unsafeMetadata = config.unsafe?.metadata;
534+
const codeUpdateStrategy =
535+
unsafeMetadata !== undefined &&
536+
"code_update_strategy" in unsafeMetadata
537+
? unsafeMetadata.code_update_strategy
538+
: props.durableObjectsCodeUpdateStrategy;
530539
await createDeployment(
531540
config,
532541
accountId,
533542
scriptName,
534543
versionMap,
535544
props.message,
536-
undefined
545+
undefined,
546+
codeUpdateStrategy
537547
);
538548

539549
// Update service and environment tags when using environments

‎packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function createWorkerUploadForm(
6868
main,
6969
sourceMaps,
7070
migrations,
71+
code_update_strategy,
7172
exports: configuredExports,
7273
compatibility_date,
7374
compatibility_flags,
@@ -882,6 +883,7 @@ export function createWorkerUploadForm(
882883
compatibility_flags,
883884
}),
884885
...(migrations && { migrations }),
886+
...(code_update_strategy && { code_update_strategy }),
885887
...(configuredExports &&
886888
Object.keys(configuredExports).length > 0 && {
887889
exports: configuredExports,
@@ -911,7 +913,6 @@ export function createWorkerUploadForm(
911913
metadata[key] = options.unsafe.metadata[key];
912914
}
913915
}
914-
915916
formData.set("metadata", JSON.stringify(metadata));
916917

917918
if (main.type === "commonjs" && modules && modules.length > 0) {

‎packages/deploy-helpers/src/deploy/helpers/versions-api.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,11 @@ export async function createDeployment(
135135
workerName: string,
136136
versionTraffic: Map<VersionId, Percentage>,
137137
message: string | undefined,
138-
force: boolean | undefined
138+
force: boolean | undefined,
139+
codeUpdateStrategy?: unknown
139140
) {
141+
// The generated Cloudflare SDK does not expose code_update_strategy yet.
142+
// Keep this request here until the updated deployment schema reaches the SDK.
140143
return await fetchResult<{ id: string }>(
141144
complianceConfig,
142145
`/accounts/${accountId}/workers/scripts/${workerName}/deployments${force ? "?force=true" : ""}`,
@@ -152,6 +155,7 @@ export async function createDeployment(
152155
annotations: {
153156
"workers/message": message,
154157
},
158+
code_update_strategy: codeUpdateStrategy,
155159
}),
156160
}
157161
);

‎packages/deploy-helpers/src/shared/types.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
Route,
1919
Entry,
2020
ContainerApp,
21+
DurableObjectCodeUpdateStrategy,
2122
} from "@cloudflare/workers-utils";
2223

2324
/** API, logging, and prompt implementations supplied by the consumer. */
@@ -154,6 +155,8 @@ export type DeployProps = SharedDeployVersionsProps & {
154155
oldAssetTtl: number | undefined;
155156
/** From --containers-rollout arg. Deploy-only. */
156157
containersRollout: "immediate" | "gradual" | "none" | undefined;
158+
/** Controls how Durable Object code updates are applied. */
159+
durableObjectsCodeUpdateStrategy?: DurableObjectCodeUpdateStrategy;
157160
/**
158161
* When true, an existing Worker with the same name aborts the deploy instead
159162
* of updating it, because this run cannot confirm the local project owns the

‎packages/workers-utils/src/config/environment.ts‎

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,29 @@ export type DurableObjectBindings = {
933933
environment?: string;
934934
}[];
935935

936+
export type DurableObjectCodeUpdateStrategy = {
937+
/** How Durable Object code updates should be applied. */
938+
mode: "immediate" | "deferred";
939+
/**
940+
* Maximum time, in seconds, to wait for Durable Objects to hibernate.
941+
* Defaults to 300 (5 minutes) and cannot exceed 86400 (24 hours).
942+
* @minimum 0
943+
* @maximum 86400
944+
* @multipleOf 0.001
945+
* @default 300
946+
*/
947+
max_delay?: number;
948+
};
949+
950+
export type DurableObjectsConfig = {
951+
bindings: DurableObjectBindings;
952+
code_update_strategy?: DurableObjectCodeUpdateStrategy;
953+
};
954+
955+
export type RawDurableObjectsConfig = Omit<DurableObjectsConfig, "bindings"> & {
956+
bindings?: DurableObjectBindings;
957+
};
958+
936959
export const ARTIFACTS_EVENT_TYPES = [
937960
"cf.artifacts.repo.created",
938961
"cf.artifacts.repo.deleted",
@@ -1064,7 +1087,7 @@ export interface EnvironmentNonInheritable {
10641087
};
10651088

10661089
/**
1067-
* A list of durable objects that your Worker should be bound to.
1090+
* Durable Object bindings and code update strategy for your Worker.
10681091
*
10691092
* For more information about Durable Objects, see the documentation at
10701093
* https://developers.cloudflare.com/workers/learning/using-durable-objects
@@ -1077,9 +1100,7 @@ export interface EnvironmentNonInheritable {
10771100
* @default {bindings:[]}
10781101
* @nonInheritable
10791102
*/
1080-
durable_objects: {
1081-
bindings: DurableObjectBindings;
1082-
};
1103+
durable_objects: DurableObjectsConfig;
10831104

10841105
/**
10851106
* A list of workflows that your Worker should be bound to.
@@ -1862,7 +1883,9 @@ export interface EnvironmentNonInheritable {
18621883
* All the properties are optional, and will be replaced with defaults in the configuration that
18631884
* is used in the rest of the codebase.
18641885
*/
1865-
export type RawEnvironment = Partial<Environment>;
1886+
export type RawEnvironment = Partial<Omit<Environment, "durable_objects">> & {
1887+
durable_objects?: RawDurableObjectsConfig;
1888+
};
18661889

18671890
/**
18681891
* A bundling resolver rule, defining the modules type for paths that match the specified globs.
@@ -2033,10 +2056,12 @@ export type ContainerEngine =
20332056
*/
20342057
export interface PreviewsConfig
20352058
extends
2036-
Partial<EnvironmentNonInheritable>,
2059+
Partial<Omit<EnvironmentNonInheritable, "durable_objects">>,
20372060
Partial<
20382061
Pick<
20392062
EnvironmentInheritable,
20402063
"logpush" | "observability" | "limits" | "placement" | "cache"
20412064
>
2042-
> {}
2065+
> {
2066+
durable_objects?: { bindings: DurableObjectBindings };
2067+
}

‎packages/workers-utils/src/config/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type {
3333
CacheOptions,
3434
ConfiguredExport,
3535
ConfigModuleRuleType,
36+
DurableObjectCodeUpdateStrategy,
3637
Environment,
3738
PreviewsConfig,
3839
RawEnvironment,

‎packages/workers-utils/src/config/validation.ts‎

Lines changed: 147 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,18 +1695,22 @@ function normalizeAndValidateEnvironment(
16951695
validateDefines(envName),
16961696
{}
16971697
),
1698-
durable_objects: notInheritable(
1699-
diagnostics,
1700-
topLevelEnv,
1701-
rawConfig,
1702-
rawEnv,
1703-
envName,
1704-
"durable_objects",
1705-
validateBindingsProperty(envName, validateDurableObjectBinding),
1706-
{
1707-
bindings: [],
1708-
}
1709-
),
1698+
durable_objects: (() => {
1699+
const durableObjects = notInheritable(
1700+
diagnostics,
1701+
topLevelEnv,
1702+
rawConfig,
1703+
rawEnv,
1704+
envName,
1705+
"durable_objects",
1706+
validateDurableObjectsProperty(envName, true),
1707+
{ bindings: [] }
1708+
);
1709+
return {
1710+
...durableObjects,
1711+
bindings: durableObjects.bindings ?? [],
1712+
};
1713+
})(),
17101714
workflows: notInheritable(
17111715
diagnostics,
17121716
topLevelEnv,
@@ -2719,6 +2723,136 @@ const validateBindingsProperty =
27192723
return isValid;
27202724
};
27212725

2726+
const DURABLE_OBJECTS_CODE_UPDATE_MAX_DELAY_SECONDS = 24 * 60 * 60;
2727+
// Absorbs float64 error in the millisecond precision check. Near the 24-hour
2728+
// maximum the error reaches ~1e-8 ms, so a tighter bound would reject valid
2729+
// values such as 65536.001.
2730+
const MILLISECOND_PRECISION_TOLERANCE = 1e-6;
2731+
2732+
const validateDurableObjectsProperty =
2733+
(
2734+
envName: string,
2735+
allowMissingBindings = false,
2736+
allowCodeUpdateStrategy = true
2737+
): ValidatorFn =>
2738+
(diagnostics, field, value, config) => {
2739+
const fieldPath =
2740+
config === undefined ? `${field}` : `env.${envName}.${field}`;
2741+
2742+
if (value === undefined) {
2743+
return true;
2744+
}
2745+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
2746+
diagnostics.errors.push(
2747+
`The field "${fieldPath}" should be an object but got ${JSON.stringify(value)}.`
2748+
);
2749+
return false;
2750+
}
2751+
2752+
validateAdditionalProperties(
2753+
diagnostics,
2754+
fieldPath,
2755+
Object.keys(value),
2756+
allowCodeUpdateStrategy
2757+
? ["bindings", "code_update_strategy"]
2758+
: ["bindings"]
2759+
);
2760+
2761+
const bindingsContainer =
2762+
allowMissingBindings && !hasProperty(value, "bindings")
2763+
? { ...value, bindings: [] }
2764+
: value;
2765+
let isValid = validateBindingsProperty(
2766+
envName,
2767+
validateDurableObjectBinding
2768+
)(diagnostics, field, bindingsContainer, config);
2769+
if (!allowCodeUpdateStrategy) {
2770+
return isValid;
2771+
}
2772+
2773+
if (
2774+
!hasProperty(value, "code_update_strategy") ||
2775+
value.code_update_strategy === undefined
2776+
) {
2777+
return isValid;
2778+
}
2779+
2780+
const strategy = value.code_update_strategy;
2781+
const strategyPath = `${fieldPath}.code_update_strategy`;
2782+
if (
2783+
typeof strategy !== "object" ||
2784+
strategy === null ||
2785+
Array.isArray(strategy)
2786+
) {
2787+
diagnostics.errors.push(
2788+
`The field "${strategyPath}" should be an object but got ${JSON.stringify(strategy)}.`
2789+
);
2790+
return false;
2791+
}
2792+
2793+
validateAdditionalProperties(
2794+
diagnostics,
2795+
strategyPath,
2796+
Object.keys(strategy),
2797+
["mode", "max_delay"]
2798+
);
2799+
isValid =
2800+
validateRequiredProperty(
2801+
diagnostics,
2802+
strategyPath,
2803+
"mode",
2804+
hasProperty(strategy, "mode") ? strategy.mode : undefined,
2805+
"string",
2806+
["immediate", "deferred"]
2807+
) && isValid;
2808+
2809+
const maxDelay = hasProperty(strategy, "max_delay")
2810+
? strategy.max_delay
2811+
: undefined;
2812+
const maxDelayHasValidType = validateOptionalProperty(
2813+
diagnostics,
2814+
strategyPath,
2815+
"max_delay",
2816+
maxDelay,
2817+
"number"
2818+
);
2819+
isValid = maxDelayHasValidType && isValid;
2820+
if (
2821+
maxDelayHasValidType &&
2822+
typeof maxDelay === "number" &&
2823+
(!Number.isFinite(maxDelay) ||
2824+
maxDelay < 0 ||
2825+
maxDelay > DURABLE_OBJECTS_CODE_UPDATE_MAX_DELAY_SECONDS)
2826+
) {
2827+
diagnostics.errors.push(
2828+
`Expected "${strategyPath}.max_delay" to be between 0 and ${DURABLE_OBJECTS_CODE_UPDATE_MAX_DELAY_SECONDS} seconds but got ${JSON.stringify(maxDelay)}.`
2829+
);
2830+
isValid = false;
2831+
}
2832+
if (
2833+
maxDelayHasValidType &&
2834+
typeof maxDelay === "number" &&
2835+
Number.isFinite(maxDelay) &&
2836+
maxDelay >= 0 &&
2837+
maxDelay <= DURABLE_OBJECTS_CODE_UPDATE_MAX_DELAY_SECONDS
2838+
) {
2839+
const milliseconds = maxDelay * 1000;
2840+
const roundedMilliseconds = Math.round(milliseconds);
2841+
if (
2842+
(maxDelay > 0 && roundedMilliseconds === 0) ||
2843+
Math.abs(milliseconds - roundedMilliseconds) >
2844+
MILLISECOND_PRECISION_TOLERANCE
2845+
) {
2846+
diagnostics.errors.push(
2847+
`Expected "${strategyPath}.max_delay" to use millisecond precision but got ${JSON.stringify(maxDelay)}.`
2848+
);
2849+
isValid = false;
2850+
}
2851+
}
2852+
2853+
return isValid;
2854+
};
2855+
27222856
const validateUnsafeSettings =
27232857
(envName: string): ValidatorFn =>
27242858
(diagnostics, field, value, config) => {
@@ -6415,7 +6549,7 @@ const validatePreviewsConfig =
64156549
);
64166550

64176551
isValid =
6418-
validateBindingsProperty(envName, validateDurableObjectBinding)(
6552+
validateDurableObjectsProperty(envName, false, false)(
64196553
diagnostics,
64206554
`${field}.durable_objects`,
64216555
previews.durable_objects,

‎packages/workers-utils/src/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
CustomDomainRoute,
55
ContainerApp,
66
ContainerEngine,
7+
DurableObjectCodeUpdateStrategy,
78
Exports,
89
DurableObjectMigration,
910
Observability,
@@ -271,6 +272,7 @@ type WorkerMetadataPut = {
271272
compatibility_flags?: string[];
272273
usage_model?: "bundled" | "unbound";
273274
migrations?: CfDurableObjectMigrations;
275+
code_update_strategy?: DurableObjectCodeUpdateStrategy;
274276
exports?: CfExports;
275277
capnp_schema?: string;
276278
bindings: WorkerMetadataBinding[];

‎packages/workers-utils/src/worker.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
CacheOptions,
3+
DurableObjectCodeUpdateStrategy,
34
Exports,
45
LocalS3Credentials,
56
Observability,
@@ -504,6 +505,7 @@ export interface CfWorkerInit {
504505
| undefined;
505506

506507
migrations: CfDurableObjectMigrations | undefined;
508+
code_update_strategy?: DurableObjectCodeUpdateStrategy;
507509
/**
508510
* Declarative exports configuration. Durable Object entries are sent instead
509511
* of `migrations`.

0 commit comments

Comments
 (0)