Skip to content

Commit cef32c8

Browse files
authored
Correctly apply Durable Object migrations for namespaced scripts (#7011)
1 parent a2afcf1 commit cef32c8

File tree

5 files changed

+233
-60
lines changed

5 files changed

+233
-60
lines changed

.changeset/gold-moose-dream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: Correctly apply Durable Object migrations for namespaced scripts

packages/wrangler/src/__tests__/deploy.test.ts

+181-31
Original file line numberDiff line numberDiff line change
@@ -6534,6 +6534,109 @@ addEventListener('fetch', event => {});`
65346534
`);
65356535
});
65366536
});
6537+
6538+
describe("dispatch namespaces", () => {
6539+
it("should deploy all migrations on first deploy", async () => {
6540+
writeWranglerToml({
6541+
durable_objects: {
6542+
bindings: [
6543+
{ name: "SOMENAME", class_name: "SomeClass" },
6544+
{ name: "SOMEOTHERNAME", class_name: "SomeOtherClass" },
6545+
],
6546+
},
6547+
migrations: [
6548+
{ tag: "v1", new_classes: ["SomeClass"] },
6549+
{ tag: "v2", new_classes: ["SomeOtherClass"] },
6550+
],
6551+
});
6552+
fs.writeFileSync(
6553+
"index.js",
6554+
`export class SomeClass{}; export class SomeOtherClass{}; export default {};`
6555+
);
6556+
mockSubDomainRequest();
6557+
mockServiceScriptData({
6558+
dispatchNamespace: "test-namespace",
6559+
}); // no scripts at all
6560+
mockUploadWorkerRequest({
6561+
expectedMigrations: {
6562+
new_tag: "v2",
6563+
steps: [
6564+
{ new_classes: ["SomeClass"] },
6565+
{ new_classes: ["SomeOtherClass"] },
6566+
],
6567+
},
6568+
useOldUploadApi: true,
6569+
expectedDispatchNamespace: "test-namespace",
6570+
});
6571+
6572+
await runWrangler(
6573+
"deploy index.js --dispatch-namespace test-namespace"
6574+
);
6575+
expect(std.out).toMatchInlineSnapshot(`
6576+
"Total Upload: xx KiB / gzip: xx KiB
6577+
Worker Startup Time: 100 ms
6578+
Your worker has access to the following bindings:
6579+
- Durable Objects:
6580+
- SOMENAME: SomeClass
6581+
- SOMEOTHERNAME: SomeOtherClass
6582+
Uploaded test-name (TIMINGS)
6583+
Dispatch Namespace: test-namespace
6584+
Current Version ID: Galaxy-Class"
6585+
`);
6586+
});
6587+
6588+
it("should use a script's current migration tag when publishing migrations", async () => {
6589+
writeWranglerToml({
6590+
durable_objects: {
6591+
bindings: [
6592+
{ name: "SOMENAME", class_name: "SomeClass" },
6593+
{ name: "SOMEOTHERNAME", class_name: "SomeOtherClass" },
6594+
],
6595+
},
6596+
migrations: [
6597+
{ tag: "v1", new_classes: ["SomeClass"] },
6598+
{ tag: "v2", new_classes: ["SomeOtherClass"] },
6599+
],
6600+
});
6601+
fs.writeFileSync(
6602+
"index.js",
6603+
`export class SomeClass{}; export class SomeOtherClass{}; export default {};`
6604+
);
6605+
mockSubDomainRequest();
6606+
mockServiceScriptData({
6607+
script: { id: "test-name", migration_tag: "v1" },
6608+
dispatchNamespace: "test-namespace",
6609+
});
6610+
mockUploadWorkerRequest({
6611+
expectedMigrations: {
6612+
old_tag: "v1",
6613+
new_tag: "v2",
6614+
steps: [
6615+
{
6616+
new_classes: ["SomeOtherClass"],
6617+
},
6618+
],
6619+
},
6620+
useOldUploadApi: true,
6621+
expectedDispatchNamespace: "test-namespace",
6622+
});
6623+
6624+
await runWrangler(
6625+
"deploy index.js --dispatch-namespace test-namespace"
6626+
);
6627+
expect(std.out).toMatchInlineSnapshot(`
6628+
"Total Upload: xx KiB / gzip: xx KiB
6629+
Worker Startup Time: 100 ms
6630+
Your worker has access to the following bindings:
6631+
- Durable Objects:
6632+
- SOMENAME: SomeClass
6633+
- SOMEOTHERNAME: SomeOtherClass
6634+
Uploaded test-name (TIMINGS)
6635+
Dispatch Namespace: test-namespace
6636+
Current Version ID: Galaxy-Class"
6637+
`);
6638+
});
6639+
});
65376640
});
65386641

65396642
describe("tail consumers", () => {
@@ -11817,13 +11920,14 @@ function mockServiceScriptData(options: {
1181711920
script?: DurableScriptInfo;
1181811921
scriptName?: string;
1181911922
env?: string;
11923+
dispatchNamespace?: string;
1182011924
}) {
1182111925
const { script } = options;
11822-
if (options.env) {
11926+
if (options.dispatchNamespace) {
1182311927
if (!script) {
1182411928
msw.use(
1182511929
http.get(
11826-
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
11930+
"*/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName",
1182711931
() => {
1182811932
return HttpResponse.json({
1182911933
success: false,
@@ -11844,11 +11948,11 @@ function mockServiceScriptData(options: {
1184411948
}
1184511949
msw.use(
1184611950
http.get(
11847-
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
11951+
"*/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName",
1184811952
({ params }) => {
1184911953
expect(params.accountId).toEqual("some-account-id");
1185011954
expect(params.scriptName).toEqual(options.scriptName || "test-name");
11851-
expect(params.envName).toEqual(options.env);
11955+
expect(params.dispatchNamespace).toEqual(options.dispatchNamespace);
1185211956
return HttpResponse.json({
1185311957
success: true,
1185411958
errors: [],
@@ -11860,44 +11964,90 @@ function mockServiceScriptData(options: {
1186011964
)
1186111965
);
1186211966
} else {
11863-
if (!script) {
11967+
if (options.env) {
11968+
if (!script) {
11969+
msw.use(
11970+
http.get(
11971+
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
11972+
() => {
11973+
return HttpResponse.json({
11974+
success: false,
11975+
errors: [
11976+
{
11977+
code: 10092,
11978+
message: "workers.api.error.environment_not_found",
11979+
},
11980+
],
11981+
messages: [],
11982+
result: null,
11983+
});
11984+
},
11985+
{ once: true }
11986+
)
11987+
);
11988+
return;
11989+
}
11990+
msw.use(
11991+
http.get(
11992+
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
11993+
({ params }) => {
11994+
expect(params.accountId).toEqual("some-account-id");
11995+
expect(params.scriptName).toEqual(
11996+
options.scriptName || "test-name"
11997+
);
11998+
expect(params.envName).toEqual(options.env);
11999+
return HttpResponse.json({
12000+
success: true,
12001+
errors: [],
12002+
messages: [],
12003+
result: { script },
12004+
});
12005+
},
12006+
{ once: true }
12007+
)
12008+
);
12009+
} else {
12010+
if (!script) {
12011+
msw.use(
12012+
http.get(
12013+
"*/accounts/:accountId/workers/services/:scriptName",
12014+
() => {
12015+
return HttpResponse.json({
12016+
success: false,
12017+
errors: [
12018+
{
12019+
code: 10090,
12020+
message: "workers.api.error.service_not_found",
12021+
},
12022+
],
12023+
messages: [],
12024+
result: null,
12025+
});
12026+
},
12027+
{ once: true }
12028+
)
12029+
);
12030+
return;
12031+
}
1186412032
msw.use(
1186512033
http.get(
1186612034
"*/accounts/:accountId/workers/services/:scriptName",
11867-
() => {
12035+
({ params }) => {
12036+
expect(params.accountId).toEqual("some-account-id");
12037+
expect(params.scriptName).toEqual(
12038+
options.scriptName || "test-name"
12039+
);
1186812040
return HttpResponse.json({
11869-
success: false,
11870-
errors: [
11871-
{
11872-
code: 10090,
11873-
message: "workers.api.error.service_not_found",
11874-
},
11875-
],
12041+
success: true,
12042+
errors: [],
1187612043
messages: [],
11877-
result: null,
12044+
result: { default_environment: { script } },
1187812045
});
1187912046
},
1188012047
{ once: true }
1188112048
)
1188212049
);
11883-
return;
1188412050
}
11885-
msw.use(
11886-
http.get(
11887-
"*/accounts/:accountId/workers/services/:scriptName",
11888-
({ params }) => {
11889-
expect(params.accountId).toEqual("some-account-id");
11890-
expect(params.scriptName).toEqual(options.scriptName || "test-name");
11891-
return HttpResponse.json({
11892-
success: true,
11893-
errors: [],
11894-
messages: [],
11895-
result: { default_environment: { script } },
11896-
});
11897-
},
11898-
{ once: true }
11899-
)
11900-
);
1190112051
}
1190212052
}
1190312053

packages/wrangler/src/deploy/deploy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
632632
config,
633633
legacyEnv: props.legacyEnv,
634634
env: props.env,
635+
dispatchNamespace: props.dispatchNamespace,
635636
})
636637
: undefined;
637638

packages/wrangler/src/durable.ts

+45-29
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function getMigrationsToUpload(
1515
config: Config;
1616
legacyEnv: boolean | undefined;
1717
env: string | undefined;
18+
dispatchNamespace: string | undefined;
1819
}
1920
): Promise<CfWorkerInit["migrations"]> {
2021
const { config, accountId } = props;
@@ -26,39 +27,42 @@ export async function getMigrationsToUpload(
2627
// get current migration tag
2728
type ScriptData = { id: string; migration_tag?: string };
2829
let script: ScriptData | undefined;
29-
if (!props.legacyEnv) {
30+
if (props.dispatchNamespace) {
3031
try {
31-
if (props.env) {
32-
const scriptData = await fetchResult<{
33-
script: ScriptData;
34-
}>(
35-
`/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
36-
);
37-
script = scriptData.script;
38-
} else {
39-
const scriptData = await fetchResult<{
40-
default_environment: {
41-
script: ScriptData;
42-
};
43-
}>(`/accounts/${accountId}/workers/services/${scriptName}`);
44-
script = scriptData.default_environment.script;
45-
}
32+
const scriptData = await fetchResult<{ script: ScriptData }>(
33+
`/accounts/${accountId}/workers/dispatch/namespaces/${props.dispatchNamespace}/scripts/${scriptName}`
34+
);
35+
script = scriptData.script;
4636
} catch (err) {
47-
if (
48-
![
49-
10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
50-
10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
51-
].includes((err as { code: number }).code)
52-
) {
53-
throw err;
54-
}
55-
// else it's a 404, no script found, and we can proceed
37+
suppressNotFoundError(err);
5638
}
5739
} else {
58-
const scripts = await fetchResult<ScriptData[]>(
59-
`/accounts/${accountId}/workers/scripts`
60-
);
61-
script = scripts.find(({ id }) => id === scriptName);
40+
if (!props.legacyEnv) {
41+
try {
42+
if (props.env) {
43+
const scriptData = await fetchResult<{
44+
script: ScriptData;
45+
}>(
46+
`/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
47+
);
48+
script = scriptData.script;
49+
} else {
50+
const scriptData = await fetchResult<{
51+
default_environment: {
52+
script: ScriptData;
53+
};
54+
}>(`/accounts/${accountId}/workers/services/${scriptName}`);
55+
script = scriptData.default_environment.script;
56+
}
57+
} catch (err) {
58+
suppressNotFoundError(err);
59+
}
60+
} else {
61+
const scripts = await fetchResult<ScriptData[]>(
62+
`/accounts/${accountId}/workers/scripts`
63+
);
64+
script = scripts.find(({ id }) => id === scriptName);
65+
}
6266
}
6367

6468
if (script?.migration_tag) {
@@ -100,3 +104,15 @@ export async function getMigrationsToUpload(
100104
}
101105
return migrations;
102106
}
107+
108+
const suppressNotFoundError = (err: unknown) => {
109+
if (
110+
![
111+
10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
112+
10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
113+
].includes((err as { code: number }).code)
114+
) {
115+
throw err;
116+
}
117+
// else it's a 404, no script found, and we can proceed
118+
};

packages/wrangler/src/versions/upload.ts

+1
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
348348
config,
349349
legacyEnv: props.legacyEnv,
350350
env: props.env,
351+
dispatchNamespace: undefined,
351352
})
352353
: undefined;
353354

0 commit comments

Comments
 (0)