-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[workflows-shared] Emulate deterministic-ID uniqueness in the local Workflows binding #14847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aeaa8d4
e1aa95c
cbccc99
e46b446
5c73f0f
7f0e856
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "miniflare": patch | ||
| "wrangler": patch | ||
| --- | ||
|
|
||
| Emulate the deterministic-ID uniqueness contract in the local Workflows binding | ||
|
|
||
| The local Workflows binding now matches the documented production behavior for deterministic instance IDs: `create({ id })` with an ID that already exists throws and retains the existing instance, and `createBatch()` skips IDs that already exist — or repeat within the batch — excluding them from the result instead of creating duplicate executions. Previously both paths silently created duplicates, so code relying on deterministic IDs for idempotency (for example a Queue consumer creating one workflow per message) appeared to work locally while double-executing workflow bodies. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers"; | ||
| import { InstanceEvent, instanceStatusName } from "./instance"; | ||
| import { | ||
| duplicateInstanceError, | ||
| isDuplicateInstanceError, | ||
| isUserTriggeredPause, | ||
| isUserTriggeredRestart, | ||
| isUserTriggeredTerminate, | ||
|
|
@@ -141,16 +143,29 @@ export class WorkflowBinding extends WorkerEntrypoint<Env> { | |
| super(ctx, env); | ||
| } | ||
|
|
||
| public async create({ | ||
| id = crypto.randomUUID(), | ||
| params = {}, | ||
| }: WorkflowInstanceCreateOptions = {}): Promise<{ | ||
| async #instanceExists(id: string): Promise<boolean> { | ||
| const stub = this.env.ENGINE.get(this.env.ENGINE.idFromName(id)); | ||
| return await stub.hasInstance(); | ||
| } | ||
|
|
||
| public async create(options: WorkflowInstanceCreateOptions = {}): Promise<{ | ||
| id: string; | ||
| }> { | ||
| // Destructuring defaults apply only to absent fields: an explicit null | ||
| // id must reach the validation below rather than becoming a generated | ||
| // id. | ||
| const { id = crypto.randomUUID(), params = {} } = options; | ||
| if (!isValidWorkflowInstanceId(id)) { | ||
| throw new WorkflowError("Workflow instance has invalid id"); | ||
| } | ||
|
|
||
| // Deterministic (caller-provided) ids carry a documented uniqueness | ||
| // contract: creating an instance with an id that already exists throws | ||
| // and the existing instance is retained. | ||
| if (options.id !== undefined && (await this.#instanceExists(id))) { | ||
| throw duplicateInstanceError(id); | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| const stubId = this.env.ENGINE.idFromName(id); | ||
| const stub = this.env.ENGINE.get(stubId); | ||
| const introspectionSession = workflowIntrospectionSessions.get( | ||
|
|
@@ -232,12 +247,42 @@ export class WorkflowBinding extends WorkerEntrypoint<Env> { | |
| ); | ||
| } | ||
|
|
||
| return await Promise.all( | ||
| batch.map(async (val) => { | ||
| const res = await this.create(val); | ||
| return res; | ||
| }) | ||
| ); | ||
| // Reject malformed ids before anything is probed or created: probing | ||
| // an id constructs its engine Durable Object, which persists storage, | ||
| // and a bad batch must not be partially applied. | ||
| for (const options of batch) { | ||
| if (options.id !== undefined && !isValidWorkflowInstanceId(options.id)) { | ||
| throw new WorkflowError("Workflow instance has invalid id"); | ||
| } | ||
| } | ||
|
|
||
| // The documented batch contract is idempotent creation: ids that already | ||
| // exist, or that repeat within the batch, are skipped and excluded from | ||
| // the result rather than throwing, and instances are created in batch | ||
| // order. | ||
| const results: { id: string }[] = []; | ||
| const seenIds = new Set<string>(); | ||
| for (const options of batch) { | ||
| const id = options.id; | ||
| if (id !== undefined) { | ||
| if (seenIds.has(id) || (await this.#instanceExists(id))) { | ||
| continue; | ||
| } | ||
| seenIds.add(id); | ||
| } | ||
| try { | ||
| results.push(await this.create(options)); | ||
| } catch (e) { | ||
| // A concurrent create can claim the id between the existence | ||
| // check above and create(); the batch contract skips such ids | ||
| // rather than failing the batch. | ||
| if (id !== undefined && isDuplicateInstanceError(e)) { | ||
| continue; | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Comment on lines
+263
to
+284
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Creating a batch of workflows now checks every id twice and runs one at a time Each entry in a batch is checked for an existing instance twice — once in the batch loop and again inside the per-instance creation call ( Sequential loop with duplicated existence probe replaces the previous parallel Promise.allPreviously The within-batch de-duplication ( Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| return results; | ||
| } | ||
|
|
||
| public async unsafeGetBindingName(): Promise<string> { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.