Skip to content

Commit 563439b

Browse files
authored
Fix multiple workflows and add proper engine persistance (#7286)
* fix: allow multiple workflow definitions * feat: make engine persist logs and status over multiple runs * chore: add persistance test and remove redundant env types * chore: change changeset and remove redundant test options
1 parent fa21312 commit 563439b

File tree

14 files changed

+436
-38
lines changed

14 files changed

+436
-38
lines changed

.changeset/cyan-geese-smell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cloudflare/workflows-shared": minor
3+
"miniflare": minor
4+
---
5+
6+
Add proper engine persistance in .wrangler and fix multiple workflows in miniflare
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "my-workflow-multiple",
3+
"private": true,
4+
"scripts": {
5+
"deploy": "wrangler deploy",
6+
"start": "wrangler dev",
7+
"test:ci": "vitest"
8+
},
9+
"devDependencies": {
10+
"@cloudflare/workers-types": "^4.20241106.0",
11+
"undici": "catalog:default",
12+
"wrangler": "workspace:*"
13+
},
14+
"volta": {
15+
"extends": "../../package.json"
16+
}
17+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
WorkerEntrypoint,
3+
WorkflowEntrypoint,
4+
WorkflowEvent,
5+
WorkflowStep,
6+
} from "cloudflare:workers";
7+
8+
type Params = {
9+
name: string;
10+
};
11+
12+
export class Demo extends WorkflowEntrypoint<{}, Params> {
13+
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
14+
const { timestamp, payload } = event;
15+
16+
await step.sleep("Wait", "1 second");
17+
18+
const result = await step.do("First step", async function () {
19+
return {
20+
output: "First step result",
21+
};
22+
});
23+
24+
await step.sleep("Wait", "1 second");
25+
26+
const result2 = await step.do("Second step", async function () {
27+
return {
28+
output: "workflow1",
29+
};
30+
});
31+
32+
return [result, result2, timestamp, payload, "workflow1"];
33+
}
34+
}
35+
36+
export class Demo2 extends WorkflowEntrypoint<{}, Params> {
37+
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
38+
const { timestamp, payload } = event;
39+
40+
await step.sleep("Wait", "1 second");
41+
42+
const result = await step.do("First step", async function () {
43+
return {
44+
output: "First step result",
45+
};
46+
});
47+
48+
await step.sleep("Wait", "1 second");
49+
50+
const result2 = await step.do("Second step", async function () {
51+
return {
52+
output: "workflow2",
53+
};
54+
});
55+
56+
return [result, result2, timestamp, payload, "workflow2"];
57+
}
58+
}
59+
60+
type Env = {
61+
WORKFLOW: Workflow;
62+
WORKFLOW2: Workflow;
63+
};
64+
65+
export default class extends WorkerEntrypoint<Env> {
66+
async fetch(req: Request) {
67+
const url = new URL(req.url);
68+
const id = url.searchParams.get("id");
69+
const workflowName = url.searchParams.get("workflowName");
70+
71+
if (url.pathname === "/favicon.ico") {
72+
return new Response(null, { status: 404 });
73+
}
74+
let workflowToUse =
75+
workflowName == "2" ? this.env.WORKFLOW2 : this.env.WORKFLOW;
76+
77+
let handle: WorkflowInstance;
78+
if (url.pathname === "/create") {
79+
if (id === null) {
80+
handle = await workflowToUse.create();
81+
} else {
82+
handle = await workflowToUse.create({ id });
83+
}
84+
} else {
85+
handle = await workflowToUse.get(id);
86+
}
87+
88+
return Response.json({ status: await handle.status(), id: handle.id });
89+
}
90+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { rm } from "fs/promises";
2+
import { resolve } from "path";
3+
import { fetch } from "undici";
4+
import { afterAll, beforeAll, describe, it, vi } from "vitest";
5+
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";
6+
7+
describe("Workflows", () => {
8+
let ip: string,
9+
port: number,
10+
stop: (() => Promise<unknown>) | undefined,
11+
getOutput: () => string;
12+
13+
beforeAll(async () => {
14+
// delete previous run contents because of persistence
15+
await rm(resolve(__dirname, "..") + "/.wrangler", {
16+
force: true,
17+
recursive: true,
18+
});
19+
({ ip, port, stop, getOutput } = await runWranglerDev(
20+
resolve(__dirname, ".."),
21+
["--port=0", "--inspector-port=0"]
22+
));
23+
});
24+
25+
afterAll(async () => {
26+
await stop?.();
27+
});
28+
29+
async function fetchJson(url: string) {
30+
const response = await fetch(url, {
31+
headers: {
32+
"MF-Disable-Pretty-Error": "1",
33+
},
34+
});
35+
const text = await response.text();
36+
37+
try {
38+
return JSON.parse(text);
39+
} catch (err) {
40+
throw new Error(`Couldn't parse JSON:\n\n${text}`);
41+
}
42+
}
43+
44+
it("creates two instances with same id in two different workflows", async ({
45+
expect,
46+
}) => {
47+
const createResult = {
48+
id: "test",
49+
status: {
50+
status: "running",
51+
output: [],
52+
},
53+
};
54+
55+
await Promise.all([
56+
expect(
57+
fetchJson(`http://${ip}:${port}/create?workflowName=1&id=test`)
58+
).resolves.toStrictEqual(createResult),
59+
expect(
60+
fetchJson(`http://${ip}:${port}/create?workflowName=2&id=test`)
61+
).resolves.toStrictEqual(createResult),
62+
]);
63+
64+
const firstResult = {
65+
id: "test",
66+
status: {
67+
status: "running",
68+
output: [{ output: "First step result" }],
69+
},
70+
};
71+
await Promise.all([
72+
vi.waitFor(
73+
async () => {
74+
await expect(
75+
fetchJson(`http://${ip}:${port}/status?workflowName=1&id=test`)
76+
).resolves.toStrictEqual(firstResult);
77+
},
78+
{ timeout: 5000 }
79+
),
80+
vi.waitFor(
81+
async () => {
82+
await expect(
83+
fetchJson(`http://${ip}:${port}/status?workflowName=2&id=test`)
84+
).resolves.toStrictEqual(firstResult);
85+
},
86+
{ timeout: 5000 }
87+
),
88+
]);
89+
90+
await Promise.all([
91+
await vi.waitFor(
92+
async () => {
93+
await expect(
94+
fetchJson(`http://${ip}:${port}/status?workflowName=1&id=test`)
95+
).resolves.toStrictEqual({
96+
id: "test",
97+
status: {
98+
status: "complete",
99+
output: [
100+
{ output: "First step result" },
101+
{ output: "workflow1" },
102+
],
103+
},
104+
});
105+
},
106+
{ timeout: 5000 }
107+
),
108+
await vi.waitFor(
109+
async () => {
110+
await expect(
111+
fetchJson(`http://${ip}:${port}/status?workflowName=2&id=test`)
112+
).resolves.toStrictEqual({
113+
id: "test",
114+
status: {
115+
status: "complete",
116+
output: [
117+
{ output: "First step result" },
118+
{ output: "workflow2" },
119+
],
120+
},
121+
});
122+
},
123+
{ timeout: 5000 }
124+
),
125+
]);
126+
});
127+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "@cloudflare/workers-tsconfig/tsconfig.json",
3+
"compilerOptions": {
4+
"types": ["node"]
5+
},
6+
"include": ["**/*.ts", "../../../node-types.d.ts"]
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "CommonJS",
5+
"lib": ["ES2020"],
6+
"types": ["@cloudflare/workers-types"],
7+
"moduleResolution": "node",
8+
"noEmit": true,
9+
"skipLibCheck": true
10+
},
11+
"include": ["**/*.ts"],
12+
"exclude": ["tests"]
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineProject, mergeConfig } from "vitest/config";
2+
import configShared from "../../vitest.shared";
3+
4+
export default mergeConfig(
5+
configShared,
6+
defineProject({
7+
test: {},
8+
})
9+
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#:schema node_modules/wrangler/config-schema.json
2+
name = "my-workflow-demo"
3+
main = "src/index.ts"
4+
compatibility_date = "2024-10-22"
5+
6+
[[workflows]]
7+
binding = "WORKFLOW"
8+
name = "my-workflow"
9+
class_name = "Demo"
10+
11+
[[workflows]]
12+
binding = "WORKFLOW2"
13+
name = "my-workflow-2"
14+
class_name = "Demo2"

fixtures/workflow/tests/index.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { rm } from "fs/promises";
12
import { resolve } from "path";
23
import { fetch } from "undici";
34
import { afterAll, beforeAll, describe, it, vi } from "vitest";
@@ -10,14 +11,14 @@ describe("Workflows", () => {
1011
getOutput: () => string;
1112

1213
beforeAll(async () => {
14+
// delete previous run contents because of persistence
15+
await rm(resolve(__dirname, "..") + "/.wrangler", {
16+
force: true,
17+
recursive: true,
18+
});
1319
({ ip, port, stop, getOutput } = await runWranglerDev(
1420
resolve(__dirname, ".."),
15-
[
16-
"--port=0",
17-
"--inspector-port=0",
18-
"--upstream-protocol=https",
19-
"--host=prod.example.org",
20-
]
21+
["--port=0", "--inspector-port=0"]
2122
));
2223
});
2324

fixtures/workflow/worker-configuration.d.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)