Skip to content

[workflows] Dispose RPC results and introspection modifiers - #15708

Open
samstarling wants to merge 5 commits into
cloudflare:mainfrom
samstarling:sam/fix-workflow-rpc-disposal
Open

samstarling wants to merge 5 commits into
cloudflare:mainfrom
samstarling:sam/fix-workflow-rpc-disposal

Conversation

@samstarling

@samstarling samstarling commented Sep 18, 2026 •

Copy link
Copy Markdown

Local Workflow execution and introspection can leave RPC resources alive, producing undisposed-result warnings and requests cancelled after their execution context has ended. These changes affect local development and test tooling, not deployed production Workflows.

Dispose step callback results after copying their data, release the temporary modifier used during introspected instance creation, and release each introspector's modifier before aborting its instance. Disposal also handles an unused introspector, repeated calls and failed modifier acquisition. Acquisition errors still surface through modify(), and genuine disposer errors still propagate after aborting the instance. Live results retain typed-array offsets and backing buffers. Compaction remains limited to storage.

When a timeout wins the callback race, retain the callback promise and release its eventual result without delaying retries. Late objects are disposed, unused incoming streams are cancelled, and late rejection or cleanup failure does not replace the original timeout. Successful results remain owned by the existing persistence path.

The fixes live in workflows-shared, which is bundled by Miniflare and the Vitest plugin. The five original lifecycle regressions fail on the unpatched baseline and pass with the fix. Additional cases cover failed acquisition, disposer errors, serialisation boundaries and timeout cleanup. The timeout regressions hold the first callback open until its retry starts, checking late objects, late streams, callback rejection and cancellation failure. The three new cleanup regressions fail without the timeout fix. All 203 shared Workflow tests pass, along with type, lint and formatting checks.

Non-stream step results must satisfy the documented structured-cloneable contract. The existing Durable Object storage write already rejected ordinary function-valued properties. Cloning first also rejects functions attached to arrays instead of letting normalisation silently discard them. Tests cover both cases and confirm that prototype methods do not prevent class-instance data from being cloned. Streams continue through their separate persistence path. The timeout tests observe cancellation on the receiving stream: workerd does not reliably notify the producer across RPC. This change does not address the existing lower-level RPC stream-cancellation diagnostics.

Worked example

Imagine a Workflow containing a step such as this:

await step.do("load data", async () => ({ value: 42 }));

Without this change, a test like this could pass while still producing RPC lifecycle errors:

import { introspectWorkflowInstance } from "cloudflare:test";
import { env } from "cloudflare:workers";
import { it, expect } from "vitest";

it("completes the workflow", async () => {
  const id = crypto.randomUUID();

  await using instance = await introspectWorkflowInstance(
    env.MY_WORKFLOW,
    id,
  );

  await env.MY_WORKFLOW.create({ id });

  await expect(instance.waitForStatus("complete")).resolves.toBeUndefined();
});

Despite the test disposing its introspector with await using, the runtime could log this:

An RPC result was not disposed properly. One of the RPC calls you made expects you to call dispose() on the return value, but you didn't do so. You cannot rely on the garbage collector for this because it may take arbitrarily long before actually collecting unreachable objects.
uncaught exception; exception = workerd/io/io-context.c++:1593: failed: jsg.Error: The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response.

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this fixes resource cleanup behind existing APIs without changing their public usage.

@changeset-bot

changeset-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9c13670

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@cloudflare/workflows-shared Patch
miniflare Patch
@cloudflare/vitest-plugin Patch
wrangler Patch
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Sep 18, 2026
@samstarling
samstarling marked this pull request as ready for review September 18, 2026 10:56
@workers-devprod
workers-devprod requested review from a team and cjol and removed request for a team September 18, 2026 10:57
@workers-devprod

workers-devprod commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/workflows
  • ✅ @cloudflare/wrangler
Show detailed file reviewers
  • packages/workflows-shared/src/binding.ts: [@cloudflare/workflows]
  • packages/workflows-shared/src/context.ts: [@cloudflare/workflows]
  • packages/workflows-shared/src/introspection.ts: [@cloudflare/workflows]
  • packages/workflows-shared/tests/rpc-lifecycle.test.ts: [@cloudflare/workflows]

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15708

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15708

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15708

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15708

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@15708

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15708

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15708

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15708

miniflare

npm i https://pkg.pr.new/miniflare@15708

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15708

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15708

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15708

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15708

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15708

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15708

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15708

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15708

wrangler

npm i https://pkg.pr.new/wrangler@15708

commit: 9c13670

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

samstarling and others added 4 commits September 21, 2026 10:07
The cleanup for a callback abandoned by a step timeout runs from an
untracked promise. The engine is a Durable Object, which stays active
while there is ongoing work or pending I/O, so the cleanup is retained
without a waitUntil (a documented no-op for Durable Objects).

Add a regression test that releases the abandoned callback from a later
step, and record the lifetime reasoning at the call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@samstarling
samstarling force-pushed the sam/fix-workflow-rpc-disposal branch from 0a9c07a to d68cb6f Compare September 21, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

3 participants