跳转到内容
搜索文档

Workflows 设计准则

最后更新 查看 MarkdownAgent 设置

Workflow 包含一个或多个步骤。每个步骤是 Workflow 中自包含、可单独重试的组件。步骤可以发出(可选的)状态,使 Workflow 能够持久化并从该步骤继续执行,即使 Workflow 因网络或基础设施问题而失败。

这是一本关于如何构建更具 resilient 且正确的 Workflows 的小型指南。

确保 API/绑定(binding)调用具有幂等性

由于步骤可能被重试多次,你的步骤(理想情况下)应具有幂等性。作为背景,幂等性是一种逻辑属性,即操作(在此情况下为步骤)可以多次应用,而不会改变初始应用之外的结果。

例如,假设你有一个向客户收费的 Workflow,你绝对不希望意外重复收费。在收费之前,你应该检查他们是否已被收费:

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		const customer_id = 123456;
		// ✅ Good: Non-idempotent API/Binding calls are always done **after** checking if the operation is
		// still needed.
		await step.do(
			`charge ${customer_id} for its monthly subscription`,
			async () => {
				// API call to check if customer was already charged
				const subscription = await fetch(
					`https://payment.processor/subscriptions/${customer_id}`,
				).then((res) => res.json());

				// return early if the customer was already charged, this can happen if the destination service dies
				// in the middle of the request but still commits it, or if the Workflows Engine restarts.
				if (subscription.charged) {
					return;
				}

				// non-idempotent call, this operation can fail and retry but still commit in the payment
				// processor - which means that, on retry, it would mischarge the customer again if the above checks
				// were not in place.
				return await fetch(
					`https://payment.processor/subscriptions/${customer_id}`,
					{
						method: "POST",
						body: JSON.stringify({ amount: 10.0 }),
					},
				);
			},
		);
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		const customer_id = 123456;
		// ✅ Good: Non-idempotent API/Binding calls are always done **after** checking if the operation is
		// still needed.
		await step.do(
			`charge ${customer_id} for its monthly subscription`,
			async () => {
				// API call to check if customer was already charged
				const subscription = await fetch(
					`https://payment.processor/subscriptions/${customer_id}`,
				).then((res) => res.json());

				// return early if the customer was already charged, this can happen if the destination service dies
				// in the middle of the request but still commits it, or if the Workflows Engine restarts.
				if (subscription.charged) {
					return;
				}

				// non-idempotent call, this operation can fail and retry but still commit in the payment
				// processor - which means that, on retry, it would mischarge the customer again if the above checks
				// were not in place.
				return await fetch(
					`https://payment.processor/subscriptions/${customer_id}`,
					{
						method: "POST",
						body: JSON.stringify({ amount: 10.0 }),
					},
				);
			},
		);
	}
}

使步骤粒度更细

步骤应尽可能自包含。这使你的逻辑在第三方 API 失败、网络错误等情况下更具 durable 性。

你也可以将其视为事务或工作单元。

  • ✅ 尽量减少每个步骤中的 API/绑定调用次数(除非你需要多次调用来证明幂等性)。
index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// ✅ Good: Unrelated API/Binding calls are self-contained, so that in case one of them fails
		// it can retry them individually. It also has an extra advantage: you can control retry or
		// timeout policies for each granular step - you might not to want to overload http.cat in
		// case of it being down.
		const httpCat = await step.do("get cutest cat from KV", async () => {
			return await this.env.KV.get("cutest-http-cat");
		});

		const image = await step.do("fetch cat image from http.cat", async () => {
			return await fetch(`https://http.cat/${httpCat}`);
		});
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// ✅ Good: Unrelated API/Binding calls are self-contained, so that in case one of them fails
		// it can retry them individually. It also has an extra advantage: you can control retry or
		// timeout policies for each granular step - you might not to want to overload http.cat in
		// case of it being down.
		const httpCat = await step.do("get cutest cat from KV", async () => {
			return await this.env.KV.get("cutest-http-cat");
		});

		const image = await step.do("fetch cat image from http.cat", async () => {
			return await fetch(`https://http.cat/${httpCat}`);
		});
	}
}

否则,你的整个 Workflow 可能不如你想象的 durable,你可能会遇到一些未定义的行为。你可以通过遵循以下规则来避免这些问题:

  • 🔴 不要将所有逻辑封装在单个步骤中。
  • 🔴 不要在同一步骤中调用不同的服务(除非你需要这样做来证明幂等性)。
  • 🔴 不要在同一步骤中进行过多服务调用(除非你需要这样做来证明幂等性)。
  • 🔴 不要在单个步骤内进行过多 CPU 密集型工作——有时引擎可能需要重启,它会从该步骤的开头重新开始。
index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: you are calling two separate services from within the same step. This might cause
		// some extra calls to the first service in case the second one fails, and in some cases, makes
		// the step non-idempotent altogether
		const image = await step.do("get cutest cat from KV", async () => {
			const httpCat = await this.env.KV.get("cutest-http-cat");
			return fetch(`https://http.cat/${httpCat}`);
		});
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: you are calling two separate services from within the same step. This might cause
		// some extra calls to the first service in case the second one fails, and in some cases, makes
		// the step non-idempotent altogether
		const image = await step.do("get cutest cat from KV", async () => {
			const httpCat = await this.env.KV.get("cutest-http-cat");
			return fetch(`https://http.cat/${httpCat}`);
		});
	}
}

不要依赖步骤之外的状态

Workflows 可能会休眠并丢失所有内存中的状态。当引擎检测到没有待处理工作且可以休眠直到需要唤醒(由于 sleep、重试或事件)时,就会发生这种情况。

这意味着你不应在步骤之外存储状态:

index.jsjs
function getRandomInt(min, max) {
	const minCeiled = Math.ceil(min);
	const maxFloored = Math.floor(max);
	return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: `imageList` will be not persisted across engine's lifetimes. Which means that after hibernation,
		// `imageList` will be empty again, even though the following two steps have already ran.
		const imageList = [];

		await step.do("get first cutest cat from KV", async () => {
			const httpCat = await this.env.KV.get("cutest-http-cat-1");

			imageList.push(httpCat);
		});

		await step.do("get second cutest cat from KV", async () => {
			const httpCat = await this.env.KV.get("cutest-http-cat-2");

			imageList.push(httpCat);
		});

		// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
		await step.sleep("💤💤💤💤", "3 hours");

		// When this runs, it will be on the second engine lifetime - which means `imageList` will be empty.
		await step.do(
			"choose a random cat from the list and download it",
			async () => {
				const randomCat = imageList.at(getRandomInt(0, imageList.length));
				// this will fail since `randomCat` is undefined because `imageList` is empty
				return await fetch(`https://http.cat/${randomCat}`);
			},
		);
	}
}
index.tsts
function getRandomInt(min, max) {
	const minCeiled = Math.ceil(min);
	const maxFloored = Math.floor(max);
	return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: `imageList` will be not persisted across engine's lifetimes. Which means that after hibernation,
		// `imageList` will be empty again, even though the following two steps have already ran.
		const imageList: string[] = [];

		await step.do("get first cutest cat from KV", async () => {
			const httpCat = await this.env.KV.get("cutest-http-cat-1");

			imageList.push(httpCat);
		});

		await step.do("get second cutest cat from KV", async () => {
			const httpCat = await this.env.KV.get("cutest-http-cat-2");

			imageList.push(httpCat);
		});

		// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
		await step.sleep("💤💤💤💤", "3 hours");

		// When this runs, it will be on the second engine lifetime - which means `imageList` will be empty.
		await step.do(
			"choose a random cat from the list and download it",
			async () => {
				const randomCat = imageList.at(getRandomInt(0, imageList.length));
				// this will fail since `randomCat` is undefined because `imageList` is empty
				return await fetch(`https://http.cat/${randomCat}`);
			},
		);
	}
}

相反,你应该构建完全由 step.do 返回值组成的顶层状态:

index.jsjs
function getRandomInt(min, max) {
	const minCeiled = Math.ceil(min);
	const maxFloored = Math.floor(max);
	return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// ✅ Good: imageList state is exclusively comprised of step returns - this means that in the event of
		// multiple engine lifetimes, imageList will be built accordingly
		const imageList = await Promise.all([
			step.do("get first cutest cat from KV", async () => {
				return await this.env.KV.get("cutest-http-cat-1");
			}),

			step.do("get second cutest cat from KV", async () => {
				return await this.env.KV.get("cutest-http-cat-2");
			}),
		]);

		// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
		await step.sleep("💤💤💤💤", "3 hours");

		// When this runs, it will be on the second engine lifetime - but this time, imageList will contain
		// the two most cutest cats
		await step.do(
			"choose a random cat from the list and download it",
			async () => {
				const randomCat = imageList.at(getRandomInt(0, imageList.length));
				// this will eventually succeed since `randomCat` is defined
				return await fetch(`https://http.cat/${randomCat}`);
			},
		);
	}
}
index.tsts
function getRandomInt(min, max) {
	const minCeiled = Math.ceil(min);
	const maxFloored = Math.floor(max);
	return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// ✅ Good: imageList state is exclusively comprised of step returns - this means that in the event of
		// multiple engine lifetimes, imageList will be built accordingly
		const imageList: string[] = await Promise.all([
			step.do("get first cutest cat from KV", async () => {
				return await this.env.KV.get("cutest-http-cat-1");
			}),

			step.do("get second cutest cat from KV", async () => {
				return await this.env.KV.get("cutest-http-cat-2");
			}),
		]);

		// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
		await step.sleep("💤💤💤💤", "3 hours");

		// When this runs, it will be on the second engine lifetime - but this time, imageList will contain
		// the two most cutest cats
		await step.do(
			"choose a random cat from the list and download it",
			async () => {
				const randomCat = imageList.at(getRandomInt(0, imageList.length));
				// this will eventually succeed since `randomCat` is defined
				return await fetch(`https://http.cat/${randomCat}`);
			},
		);
	}
}

避免在 step.do 之外产生副作用

不建议在步骤之外编写具有任何副作用的代码,除非你希望它重复执行,因为 Workflow 引擎可能在实例运行时重启。如果引擎重启,步骤逻辑将被保留,但步骤之外的逻辑可能会重复执行。

例如,workflow 步骤之外的 console.log() 可能在引擎重启时导致日志打印两次。

但是,涉及不可序列化资源(如数据库连接)的逻辑应在步骤之外执行。由于 Workflows 实例生命周期的特性,step.do 之外的操作可能会重复执行多次。

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: creating instances outside of steps
		// This might get called more than once creating more instances than expected
		const badInstance = await this.env.ANOTHER_WORKFLOW.create();

		// 🔴 Bad: using non-deterministic functions outside of steps
		// this will produce different results if the instance has to restart, different runs of the same instance
		// might go through different paths
		const badRandom = Math.random();

		if (badRandom > 0) {
			// do some stuff
		}

		// ⚠️ Warning: This log may happen many times
		console.log("This might be logged more than once");

		await step.do("do some stuff and have a log for when it runs", async () => {
			// do some stuff

			// this log will only appear once
			console.log("successfully did stuff");
		});

		// ✅ Good: wrap non-deterministic function in a step
		// after running successfully will not run again
		const goodRandom = await step.do("create a random number", async () => {
			return Math.random();
		});

		// ✅ Good: calls that have no side effects can be done outside of steps
		// For Hyperdrive, create the connection inside each step instead of here.
		const db = createDBConnection(this.env.DB_URL, this.env.DB_TOKEN);

		// ✅ Good: run functions with side effects inside of a step
		// after running successfully will not run again
		const goodInstance = await step.do(
			"good step that returns state",
			async () => {
				const instance = await this.env.ANOTHER_WORKFLOW.create();

				return instance;
			},
		);
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: creating instances outside of steps
		// This might get called more than once creating more instances than expected
		const badInstance = await this.env.ANOTHER_WORKFLOW.create();

		// 🔴 Bad: using non-deterministic functions outside of steps
		// this will produce different results if the instance has to restart, different runs of the same instance
		// might go through different paths
		const badRandom = Math.random();

		if (badRandom > 0) {
			// do some stuff
		}

		// ⚠️ Warning: This log may happen many times
		console.log("This might be logged more than once");

		await step.do("do some stuff and have a log for when it runs", async () => {
			// do some stuff

			// this log will only appear once
			console.log("successfully did stuff");
		});

		// ✅ Good: wrap non-deterministic function in a step
		// after running successfully will not run again
		const goodRandom = await step.do("create a random number", async () => {
			return Math.random();
		});

		// ✅ Good: calls that have no side effects can be done outside of steps
		// For Hyperdrive, create the connection inside each step instead of here.
		const db = createDBConnection(this.env.DB_URL, this.env.DB_TOKEN);

		// ✅ Good: run functions with side effects inside of a step
		// after running successfully will not run again
		const goodInstance = await step.do(
			"good step that returns state",
			async () => {
				const instance = await this.env.ANOTHER_WORKFLOW.create();

				return instance;
			},
		);
	}
}

不要修改传入的事件

传递给 Workflow run 方法的 event 是不可变的:你对事件所做的更改不会在步骤和/或 Workflow 重启之间持久化。

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: Mutating the event
		// This will not be persisted across steps and `event.payload` will
		// take on its original value.
		await step.do("bad step that mutates the incoming event", async () => {
			let userData = await this.env.KV.get(event.payload.user);
			event.payload = userData;
		});

		// ✅ Good: persist data by returning it as state from your step
		// Use that state in subsequent steps
		let userData = await step.do("good step that returns state", async () => {
			return await this.env.KV.get(event.payload.user);
		});

		let someOtherData = await step.do(
			"following step that uses that state",
			async () => {
				// Access to userData here
				// Will always be the same if this step is retried
			},
		);
	}
}
index.tsts
interface MyEvent {
	user: string;
	data: string;
}

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<MyEvent>, step: WorkflowStep) {
		// 🔴 Bad: Mutating the event
		// This will not be persisted across steps and `event.payload` will
		// take on its original value.
		await step.do("bad step that mutates the incoming event", async () => {
			let userData = await this.env.KV.get(event.payload.user);
			event.payload = userData;
		});

		// ✅ Good: persist data by returning it as state from your step
		// Use that state in subsequent steps
		let userData = await step.do("good step that returns state", async () => {
			return await this.env.KV.get(event.payload.user);
		});

		let someOtherData = await step.do(
			"following step that uses that state",
			async () => {
				// Access to userData here
				// Will always be the same if this step is retried
			},
		);
	}
}

确定性地命名步骤

步骤应确定性地命名(即不使用当前日期/时间、随机性等)。这确保其状态被缓存,并防止步骤不必要地重新运行。步骤名称在 Workflow 中充当「缓存键」。

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: Naming the step non-deterministically prevents it from being cached
		// This will cause the step to be re-run if subsequent steps fail.
		await step.do(`step #1 running at: ${Date.now()}`, async () => {
			let userData = await this.env.KV.get(event.payload.user);
			// Do not mutate event.payload
			event.payload = userData;
		});

		// ✅ Good: give steps a deterministic name.
		// Return dynamic values in your state, or log them instead.
		let state = await step.do("fetch user data from KV", async () => {
			let userData = await this.env.KV.get(event.payload.user);
			console.log(`fetched at ${Date.now()}`);
			return userData;
		});

		// ✅ Good: steps that are dynamically named are constructed in a deterministic way.
		// In this case, `catList` is a step output, which is stable, and `catList` is
		// traversed in a deterministic fashion (no shuffles or random accesses) so,
		// it's fine to dynamically name steps (e.g: create a step per list entry).
		let catList = await step.do("get cat list from KV", async () => {
			return await this.env.KV.get("cat-list");
		});

		for (const cat of catList) {
			await step.do(`get cat: ${cat}`, async () => {
				return await this.env.KV.get(cat);
			});
		}
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: Naming the step non-deterministically prevents it from being cached
		// This will cause the step to be re-run if subsequent steps fail.
		await step.do(`step #1 running at: ${Date.now()}`, async () => {
			let userData = await this.env.KV.get(event.payload.user);
			// Do not mutate event.payload
			event.payload = userData;
		});

		// ✅ Good: give steps a deterministic name.
		// Return dynamic values in your state, or log them instead.
		let state = await step.do("fetch user data from KV", async () => {
			let userData = await this.env.KV.get(event.payload.user);
			console.log(`fetched at ${Date.now()}`);
			return userData;
		});

		// ✅ Good: steps that are dynamically named are constructed in a deterministic way.
		// In this case, `catList` is a step output, which is stable, and `catList` is
		// traversed in a deterministic fashion (no shuffles or random accesses) so,
		// it's fine to dynamically name steps (e.g: create a step per list entry).
		let catList = await step.do("get cat list from KV", async () => {
			return await this.env.KV.get("cat-list");
		});

		for (const cat of catList) {
			await step.do(`get cat: ${cat}`, async () => {
				return await this.env.KV.get(cat);
			});
		}
	}
}

谨慎使用 Promise.race()Promise.any()

Workflows 允许在 Promise.race()Promise.any() 方法内使用步骤来实现并发步骤执行。但是,必须考虑一些事项。

由于 Workflows 实例生命周期的特性,并且 Promise 内的步骤将运行直到完成,第一次通过时返回的步骤可能不是实际缓存的步骤,因为步骤按名称缓存

index.jsjs
// helper sleep method
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: The `Promise.race` is not surrounded by a `step.do`, which may cause undeterministic caching behavior.
		const race_return = await Promise.race([
			step.do("Promise first race", async () => {
				await sleep(1000);
				return "first";
			}),
			step.do("Promise second race", async () => {
				return "second";
			}),
		]);

		await step.sleep("Sleep step", "2 hours");

		return await step.do("Another step", async () => {
			// This step will return `first`, even though the `Promise.race` first returned `second`.
			return race_return;
		});
	}
}
index.tsts
// helper sleep method
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: The `Promise.race` is not surrounded by a `step.do`, which may cause undeterministic caching behavior.
		const race_return = await Promise.race([
			step.do("Promise first race", async () => {
				await sleep(1000);
				return "first";
			}),
			step.do("Promise second race", async () => {
				return "second";
			}),
		]);

		await step.sleep("Sleep step", "2 hours");

		return await step.do("Another step", async () => {
			// This step will return `first`, even though the `Promise.race` first returned `second`.
			return race_return;
		});
	}
}

为确保一致性,我们建议将 Promise.race()Promise.any() 包裹在 step.do() 内,这将确保多次通过时的缓存一致性。

index.jsjs
// helper sleep method
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// ✅ Good: The `Promise.race` is surrounded by a `step.do`, ensuring deterministic caching behavior.
		const race_return = await step.do("Promise step", async () => {
			return await Promise.race([
				step.do("Promise first race", async () => {
					await sleep(1000);
					return "first";
				}),
				step.do("Promise second race", async () => {
					return "second";
				}),
			]);
		});

		await step.sleep("Sleep step", "2 hours");

		return await step.do("Another step", async () => {
			// This step will return `second` because the `Promise.race` was surround by the `step.do` method.
			return race_return;
		});
	}
}
index.tsts
// helper sleep method
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// ✅ Good: The `Promise.race` is surrounded by a `step.do`, ensuring deterministic caching behavior.
		const race_return = await step.do("Promise step", async () => {
			return await Promise.race([
				step.do("Promise first race", async () => {
					await sleep(1000);
					return "first";
				}),
				step.do("Promise second race", async () => {
					return "second";
				}),
			]);
		});

		await step.sleep("Sleep step", "2 hours");

		return await step.do("Another step", async () => {
			// This step will return `second` because the `Promise.race` was surround by the `step.do` method.
			return race_return;
		});
	}
}

实例 ID 是唯一的

Workflow 实例 ID 在每个 Workflow 中是唯一的。ID 是唯一标识符,将特定实例的日志、指标、状态和运行状态关联起来,即使在完成后也是如此。允许 ID 重用将难以理解 Workflow 实例 ID 是指昨天、上周还是今天运行的实例。

如果你希望为同一用户 ID 运行多个具有不同输入参数的不同 Workflow 实例,这也将带来问题,因为你将需要立即确定新的 ID 映射。

如果你需要将多个实例与系统中的特定用户、商户或其他「客户」ID 关联,请考虑使用复合 ID 或使用随机生成的 ID 并将映射存储在 D1 等数据库中。

index.jsjs
// This is in the same file as your Workflow definition
export default {
	async fetch(req, env) {
		// 🔴 Bad: Use an ID that isn't unique across future Workflow invocations
		let userId = getUserId(req); // Returns the userId
		let badInstance = await env.MY_WORKFLOW.create({
			id: userId,
			params: payload,
		});

		// ✅ Good: use an ID that is unique
		// e.g. a transaction ID, order ID, or task ID are good options
		let instanceId = getTransactionId(); // e.g. assuming transaction IDs are unique
		// or: compose a composite ID and store it in your database
		// so that you can track all instances associated with a specific user or merchant.
		instanceId = `${getUserId(req)}-${crypto.randomUUID().slice(0, 6)}`;
		let { result } = await addNewInstanceToDB(userId, instanceId);
		let goodInstance = await env.MY_WORKFLOW.create({
			id: instanceId,
			params: payload,
		});

		return Response.json({
			id: goodInstance.id,
			details: await goodInstance.status(),
		});
	},
};
index.tsts
// This is in the same file as your Workflow definition
export default {
	async fetch(req: Request, env: Env): Promise<Response> {
		// 🔴 Bad: Use an ID that isn't unique across future Workflow invocations
		let userId = getUserId(req); // Returns the userId
		let badInstance = await env.MY_WORKFLOW.create({
			id: userId,
			params: payload,
		});

		// ✅ Good: use an ID that is unique
		// e.g. a transaction ID, order ID, or task ID are good options
		let instanceId = getTransactionId(); // e.g. assuming transaction IDs are unique
		// or: compose a composite ID and store it in your database
		// so that you can track all instances associated with a specific user or merchant.
		instanceId = `${getUserId(req)}-${crypto.randomUUID().slice(0, 6)}`;
		let { result } = await addNewInstanceToDB(userId, instanceId);
		let goodInstance = await env.MY_WORKFLOW.create({
			id: instanceId,
			params: payload,
		});

		return Response.json({
			id: goodInstance.id,
			details: await goodInstance.status(),
		});
	},
};

对步骤使用 await

调用 step.dostep.sleep 时,使用 await 以避免在 Workflow 代码中引入 bug 和竞态条件。

如果不调用 await step.doawait step.sleep,你会创建一个悬空 Promise。当 Promise 被创建但未正确 await 时会发生这种情况,导致潜在的 bug 和竞态条件。

当你不使用 await 关键字或未链式调用 .then() 方法来处理 Promise 结果时,就会发生这种情况。例如,调用 fetch(GITHUB_URL) 而不等待其响应将导致后续代码立即执行,无论 fetch 是否完成。这可能导致过早日志记录、异常被吞没(且不终止 Workflow)以及丢失返回值(状态)等问题。

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns.
		const badIssues = step.do(`fetch issues from GitHub`, async () => {
			// The step will return before this call is done
			let issues = await getIssues(event.payload.repoName);
			return issues;
		});

		// ✅ Good: The step is correctly await'ed.
		const goodIssues = await step.do(`fetch issues from GitHub`, async () => {
			let issues = await getIssues(event.payload.repoName);
			return issues;
		});

		// Rest of your Workflow goes here!
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns.
		const badIssues = step.do(`fetch issues from GitHub`, async () => {
			// The step will return before this call is done
			let issues = await getIssues(event.payload.repoName);
			return issues;
		});

		// ✅ Good: The step is correctly await'ed.
		const goodIssues = await step.do(`fetch issues from GitHub`, async () => {
			let issues = await getIssues(event.payload.repoName);
			return issues;
		});

		// Rest of your Workflow goes here!
	}
}

谨慎使用条件逻辑

你可以在步骤之外使用 if 语句、循环和其他控制流。但是,条件必须基于确定性值——来自 event.payload 的值或先前步骤的返回值。步骤之外基于非确定性条件(如 Math.random()Date.now())的条件在 Workflow 重启时可能导致意外行为。

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		const config = await step.do("fetch config", async () => {
			return await this.env.KV.get("feature-flags", { type: "json" });
		});

		// ✅ Good: Condition based on step output (deterministic)
		if (config.enableEmailNotifications) {
			await step.do("send email", async () => {
				// Send email logic
			});
		}

		// ✅ Good: Condition based on event payload (deterministic)
		if (event.payload.userType === "premium") {
			await step.do("premium processing", async () => {
				// Premium-only logic
			});
		}

		// 🔴 Bad: Condition based on non-deterministic value outside a step
		// This could behave differently if the Workflow restarts
		if (Math.random() > 0.5) {
			await step.do("maybe do something", async () => {});
		}

		// ✅ Good: Wrap non-deterministic values in a step
		const shouldProcess = await step.do("decide randomly", async () => {
			return Math.random() > 0.5;
		});
		if (shouldProcess) {
			await step.do("conditionally do something", async () => {});
		}
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		const config = await step.do("fetch config", async () => {
			return await this.env.KV.get("feature-flags", { type: "json" });
		});

		// ✅ Good: Condition based on step output (deterministic)
		if (config.enableEmailNotifications) {
			await step.do("send email", async () => {
				// Send email logic
			});
		}

		// ✅ Good: Condition based on event payload (deterministic)
		if (event.payload.userType === "premium") {
			await step.do("premium processing", async () => {
				// Premium-only logic
			});
		}

		// 🔴 Bad: Condition based on non-deterministic value outside a step
		// This could behave differently if the Workflow restarts
		if (Math.random() > 0.5) {
			await step.do("maybe do something", async () => {});
		}

		// ✅ Good: Wrap non-deterministic values in a step
		const shouldProcess = await step.do("decide randomly", async () => {
			return Math.random() > 0.5;
		});
		if (shouldProcess) {
			await step.do("conditionally do something", async () => {});
		}
	}
}

批量创建多个 Workflow 调用

创建多个 Workflow 实例时,使用 createBatch 方法将调用批量处理。这允许你在单个请求中创建多个 Workflow 实例,从而减少向 Workflows API 发出的请求数量。但是,批次中的每个单独实例仍将计入创建速率限制。与 create 不同,createBatch 是幂等的:如果具有相同 ID 的现有实例仍在其保留限制内,它将被跳过并从返回数组中排除。

index.jsjs
export default {
	async fetch(req, env) {
		let instances = [
			{ id: "user1", params: { name: "John" } },
			{ id: "user2", params: { name: "Jane" } },
			{ id: "user3", params: { name: "Alice" } },
			{ id: "user4", params: { name: "Bob" } },
		];

		// 🔴 Bad: Create them one by one, which is more likely to hit creation rate limits.
		for (let instance of instances) {
			await env.MY_WORKFLOW.create({
				id: instance.id,
				params: instance.params,
			});
		}

		// ✅ Good: Batch calls together
		// This improves throughput.
		let createdInstances = await env.MY_WORKFLOW.createBatch(instances);
		return Response.json({ instances: createdInstances });
	},
};
index.tsts
export default {
	async fetch(req: Request, env: Env): Promise<Response> {
		let instances = [
			{ id: "user1", params: { name: "John" } },
			{ id: "user2", params: { name: "Jane" } },
			{ id: "user3", params: { name: "Alice" } },
			{ id: "user4", params: { name: "Bob" } },
		];

		// 🔴 Bad: Create them one by one, which is more likely to hit creation rate limits.
		for (let instance of instances) {
			await env.MY_WORKFLOW.create({
				id: instance.id,
				params: instance.params,
			});
		}

		// ✅ Good: Batch calls together
		// This improves throughput.
		let createdInstances = await env.MY_WORKFLOW.createBatch(instances);
		return Response.json({ instances: createdInstances });
	},
};

将超时限制在 30 分钟或更短

设置 WorkflowStep 超时 时,确保其持续时间为 30 分钟或更短。如果你的用例需要超过 30 分钟的超时,请考虑改用 step.waitForEvent()

将非流式步骤返回值保持在 1 MiB 以下

非流式 step.do() 返回值最多可持久化 1 MiB(2^20 字节)。如果你的步骤返回超过此限制的结构化数据,步骤将失败。这在获取大型 API 响应或处理大型文件时很常见。

在 JavaScript Workflows 中,ReadableStream<Uint8Array> 是用于较大二进制输出的支持的可序列化返回类型。持久化此类输出时,你应该:

  • 从步骤回调返回新流。

  • 将单个块保持在 16 MB 以下。

  • 不要返回已锁定的流或已被读取的流。

  • 仅依赖从步骤返回的流。

请注意,流式输出仍计入 Workflow 实例存储限制。

如果这些存储限制仍不能满足你的需求,请考虑将步骤输出外部存储(例如在 R2 中)并保存对其的引用。

index.jsjs
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event, step) {
		// 🔴 Bad: Returning a large response that may exceed 1 MiB
		const largeData = await step.do("fetch large dataset", async () => {
			const response = await fetch("https://api.example.com/large-dataset");
			return await response.json(); // Could exceed 1 MiB
		});

		// ✅ Good: Store large structured data externally and return a reference
		const dataRef = await step.do("fetch and store large dataset", async () => {
			const response = await fetch("https://api.example.com/large-dataset");
			const data = await response.json();
			// Store in R2 and return a reference
			await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
			return { key: "dataset-123" };
		});

		// Retrieve the data in a later step when needed
		const data = await step.do("process dataset", async () => {
			const stored = await this.env.MY_BUCKET.get(dataRef.key);
			return processData(await stored.json());
		});
	}
}
index.tsts
export class MyWorkflow extends WorkflowEntrypoint {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		// 🔴 Bad: Returning a large response that may exceed 1 MiB
		const largeData = await step.do("fetch large dataset", async () => {
			const response = await fetch("https://api.example.com/large-dataset");
			return await response.json(); // Could exceed 1 MiB
		});

		// ✅ Good: Store large structured data externally and return a reference
		const dataRef = await step.do("fetch and store large dataset", async () => {
			const response = await fetch("https://api.example.com/large-dataset");
			const data = await response.json();
			// Store in R2 and return a reference
			await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
			return { key: "dataset-123" };
		});

		// Retrieve the data in a later step when needed
		const data = await step.do("process dataset", async () => {
			const stored = await this.env.MY_BUCKET.get(dataRef.key);
			return processData(await stored.json());
		});
	}
}

相关资源

  • Workers 最佳实践:适用于触发 Workflows 的 Workers 的请求处理、可观测性和安全性的代码模式。
  • Durable Objects 设计准则:有状态、协调应用的最佳实践——在将 Durable Objects 与 Workflows 结合使用时很有用。

这篇文档对您有帮助吗?