diff --git a/src/content/docs/workflows/build/rules-of-workflows.mdx b/src/content/docs/workflows/build/rules-of-workflows.mdx index 145f5d830aee2fc..9af4259837fa67f 100644 --- a/src/content/docs/workflows/build/rules-of-workflows.mdx +++ b/src/content/docs/workflows/build/rules-of-workflows.mdx @@ -243,7 +243,7 @@ export class MyWorkflow extends WorkflowEntrypoint { ### Name steps deterministically -Steps should be named deterministically (ie, not using the current date/time, randomness, etc). This ensures that their state is cached, and prevents the step from being rerun unnecessarily. Step names act as the "cache key" in your Workflow. +Steps should be named deterministically (that is, not using the current date/time, randomness, etc). This ensures that their state is cached, and prevents the step from being rerun unnecessarily. Step names act as the "cache key" in your Workflow. ```ts @@ -283,6 +283,101 @@ export class MyWorkflow extends WorkflowEntrypoint { ``` +### Take care with `Promise.race()` and `Promise.any()` + +Workflows allows the usage steps within the `Promise.race()` or `Promise.any()` methods as a way to achieve concurrent steps execution. However, some considerations must be taken. + +Due to the nature of Workflows' instance lifecycle, and given that a step inside a Promise will run until it finishes, the step that is returned during the first passage may not be the actual cached step, as [steps are cached by their names](#name-steps-deterministically). + + +```ts + +// helper sleep method +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export class MyWorkflow extends WorkflowEntrypoint { + async run(event: WorkflowEvent, 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; + }, + ); + } +} +``` + + +To ensure consistency, we suggest to surround the `Promise.race()` or `Promise.any()` within a `step.do()`, as this will ensure caching consistency across multiple passages. + + +```ts + +// helper sleep method +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export class MyWorkflow extends WorkflowEntrypoint { + async run(event: WorkflowEvent, 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; + }, + ); + } +} +``` + + ### Instance IDs are unique Workflow [instance IDs](/workflows/build/workers-api/#workflowinstance) are unique per Workflow. The ID is the unique identifier that associates logs, metrics, state and status of a run to a specific an instance, even after completion. Allowing ID re-use would make it hard to understand if a Workflow instance ID referred to an instance that run yesterday, last week or today.