Skip to content

Commit

Permalink
Correctly apply Durable Object migrations for namespaced scripts (#7011)
Browse files Browse the repository at this point in the history
  • Loading branch information
GregBrimble authored Oct 23, 2024
1 parent a2afcf1 commit cef32c8
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 60 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-moose-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

fix: Correctly apply Durable Object migrations for namespaced scripts
212 changes: 181 additions & 31 deletions packages/wrangler/src/__tests__/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6534,6 +6534,109 @@ addEventListener('fetch', event => {});`
`);
});
});

describe("dispatch namespaces", () => {
it("should deploy all migrations on first deploy", async () => {
writeWranglerToml({
durable_objects: {
bindings: [
{ name: "SOMENAME", class_name: "SomeClass" },
{ name: "SOMEOTHERNAME", class_name: "SomeOtherClass" },
],
},
migrations: [
{ tag: "v1", new_classes: ["SomeClass"] },
{ tag: "v2", new_classes: ["SomeOtherClass"] },
],
});
fs.writeFileSync(
"index.js",
`export class SomeClass{}; export class SomeOtherClass{}; export default {};`
);
mockSubDomainRequest();
mockServiceScriptData({
dispatchNamespace: "test-namespace",
}); // no scripts at all
mockUploadWorkerRequest({
expectedMigrations: {
new_tag: "v2",
steps: [
{ new_classes: ["SomeClass"] },
{ new_classes: ["SomeOtherClass"] },
],
},
useOldUploadApi: true,
expectedDispatchNamespace: "test-namespace",
});

await runWrangler(
"deploy index.js --dispatch-namespace test-namespace"
);
expect(std.out).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your worker has access to the following bindings:
- Durable Objects:
- SOMENAME: SomeClass
- SOMEOTHERNAME: SomeOtherClass
Uploaded test-name (TIMINGS)
Dispatch Namespace: test-namespace
Current Version ID: Galaxy-Class"
`);
});

it("should use a script's current migration tag when publishing migrations", async () => {
writeWranglerToml({
durable_objects: {
bindings: [
{ name: "SOMENAME", class_name: "SomeClass" },
{ name: "SOMEOTHERNAME", class_name: "SomeOtherClass" },
],
},
migrations: [
{ tag: "v1", new_classes: ["SomeClass"] },
{ tag: "v2", new_classes: ["SomeOtherClass"] },
],
});
fs.writeFileSync(
"index.js",
`export class SomeClass{}; export class SomeOtherClass{}; export default {};`
);
mockSubDomainRequest();
mockServiceScriptData({
script: { id: "test-name", migration_tag: "v1" },
dispatchNamespace: "test-namespace",
});
mockUploadWorkerRequest({
expectedMigrations: {
old_tag: "v1",
new_tag: "v2",
steps: [
{
new_classes: ["SomeOtherClass"],
},
],
},
useOldUploadApi: true,
expectedDispatchNamespace: "test-namespace",
});

await runWrangler(
"deploy index.js --dispatch-namespace test-namespace"
);
expect(std.out).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your worker has access to the following bindings:
- Durable Objects:
- SOMENAME: SomeClass
- SOMEOTHERNAME: SomeOtherClass
Uploaded test-name (TIMINGS)
Dispatch Namespace: test-namespace
Current Version ID: Galaxy-Class"
`);
});
});
});

describe("tail consumers", () => {
Expand Down Expand Up @@ -11817,13 +11920,14 @@ function mockServiceScriptData(options: {
script?: DurableScriptInfo;
scriptName?: string;
env?: string;
dispatchNamespace?: string;
}) {
const { script } = options;
if (options.env) {
if (options.dispatchNamespace) {
if (!script) {
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
"*/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName",
() => {
return HttpResponse.json({
success: false,
Expand All @@ -11844,11 +11948,11 @@ function mockServiceScriptData(options: {
}
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
"*/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName",
({ params }) => {
expect(params.accountId).toEqual("some-account-id");
expect(params.scriptName).toEqual(options.scriptName || "test-name");
expect(params.envName).toEqual(options.env);
expect(params.dispatchNamespace).toEqual(options.dispatchNamespace);
return HttpResponse.json({
success: true,
errors: [],
Expand All @@ -11860,44 +11964,90 @@ function mockServiceScriptData(options: {
)
);
} else {
if (!script) {
if (options.env) {
if (!script) {
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
() => {
return HttpResponse.json({
success: false,
errors: [
{
code: 10092,
message: "workers.api.error.environment_not_found",
},
],
messages: [],
result: null,
});
},
{ once: true }
)
);
return;
}
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName/environments/:envName",
({ params }) => {
expect(params.accountId).toEqual("some-account-id");
expect(params.scriptName).toEqual(
options.scriptName || "test-name"
);
expect(params.envName).toEqual(options.env);
return HttpResponse.json({
success: true,
errors: [],
messages: [],
result: { script },
});
},
{ once: true }
)
);
} else {
if (!script) {
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName",
() => {
return HttpResponse.json({
success: false,
errors: [
{
code: 10090,
message: "workers.api.error.service_not_found",
},
],
messages: [],
result: null,
});
},
{ once: true }
)
);
return;
}
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName",
() => {
({ params }) => {
expect(params.accountId).toEqual("some-account-id");
expect(params.scriptName).toEqual(
options.scriptName || "test-name"
);
return HttpResponse.json({
success: false,
errors: [
{
code: 10090,
message: "workers.api.error.service_not_found",
},
],
success: true,
errors: [],
messages: [],
result: null,
result: { default_environment: { script } },
});
},
{ once: true }
)
);
return;
}
msw.use(
http.get(
"*/accounts/:accountId/workers/services/:scriptName",
({ params }) => {
expect(params.accountId).toEqual("some-account-id");
expect(params.scriptName).toEqual(options.scriptName || "test-name");
return HttpResponse.json({
success: true,
errors: [],
messages: [],
result: { default_environment: { script } },
});
},
{ once: true }
)
);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
config,
legacyEnv: props.legacyEnv,
env: props.env,
dispatchNamespace: props.dispatchNamespace,
})
: undefined;

Expand Down
74 changes: 45 additions & 29 deletions packages/wrangler/src/durable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function getMigrationsToUpload(
config: Config;
legacyEnv: boolean | undefined;
env: string | undefined;
dispatchNamespace: string | undefined;
}
): Promise<CfWorkerInit["migrations"]> {
const { config, accountId } = props;
Expand All @@ -26,39 +27,42 @@ export async function getMigrationsToUpload(
// get current migration tag
type ScriptData = { id: string; migration_tag?: string };
let script: ScriptData | undefined;
if (!props.legacyEnv) {
if (props.dispatchNamespace) {
try {
if (props.env) {
const scriptData = await fetchResult<{
script: ScriptData;
}>(
`/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
);
script = scriptData.script;
} else {
const scriptData = await fetchResult<{
default_environment: {
script: ScriptData;
};
}>(`/accounts/${accountId}/workers/services/${scriptName}`);
script = scriptData.default_environment.script;
}
const scriptData = await fetchResult<{ script: ScriptData }>(
`/accounts/${accountId}/workers/dispatch/namespaces/${props.dispatchNamespace}/scripts/${scriptName}`
);
script = scriptData.script;
} catch (err) {
if (
![
10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
].includes((err as { code: number }).code)
) {
throw err;
}
// else it's a 404, no script found, and we can proceed
suppressNotFoundError(err);
}
} else {
const scripts = await fetchResult<ScriptData[]>(
`/accounts/${accountId}/workers/scripts`
);
script = scripts.find(({ id }) => id === scriptName);
if (!props.legacyEnv) {
try {
if (props.env) {
const scriptData = await fetchResult<{
script: ScriptData;
}>(
`/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
);
script = scriptData.script;
} else {
const scriptData = await fetchResult<{
default_environment: {
script: ScriptData;
};
}>(`/accounts/${accountId}/workers/services/${scriptName}`);
script = scriptData.default_environment.script;
}
} catch (err) {
suppressNotFoundError(err);
}
} else {
const scripts = await fetchResult<ScriptData[]>(
`/accounts/${accountId}/workers/scripts`
);
script = scripts.find(({ id }) => id === scriptName);
}
}

if (script?.migration_tag) {
Expand Down Expand Up @@ -100,3 +104,15 @@ export async function getMigrationsToUpload(
}
return migrations;
}

const suppressNotFoundError = (err: unknown) => {
if (
![
10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
].includes((err as { code: number }).code)
) {
throw err;
}
// else it's a 404, no script found, and we can proceed
};
1 change: 1 addition & 0 deletions packages/wrangler/src/versions/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
config,
legacyEnv: props.legacyEnv,
env: props.env,
dispatchNamespace: undefined,
})
: undefined;

Expand Down

0 comments on commit cef32c8

Please sign in to comment.