Skip to content

End a request's sandbox fibers before its database connection closes - #1799

Merged
RhysSullivan merged 1 commit into
mainfrom
fix/request-scope-pool-discipline
Aug 28, 2026
Merged

End a request's sandbox fibers before its database connection closes#1799
RhysSullivan merged 1 commit into
mainfrom
fix/request-scope-pool-discipline

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Problem

Reads and writes through the storage layer intermittently fail on the cloud executor plane with driver-level connection faults — the pool has already been closed, or the socket belongs to a different request. They land on ordinary operations (tool and plugin-storage reads, connection health writes), and the requests they happen inside otherwise succeed, so the failures are swallowed and the caller silently gets a degraded answer.

The cause is a lifetime mismatch. executeWithPause forks the sandbox as a daemon fiber on purpose, so a paused execution can outlive the caller that observed the pause. What that fiber closes over is the problem: its tool invoker is built from the request's executor, and the storage client captures its database handle at construction rather than resolving it per operation. The fiber therefore holds the connection that belongs to whichever scope built the engine.

On the HTTP executor plane that scope is the request. Its finalizer closes the connection as soon as the response is written, while the forked sandbox is still parked. The next query that fiber makes runs against a closed pool.

Fix

Give the engine an explicit end-of-life seam and use it where the engine is per-request:

  • ExecutionEngine.shutdown interrupts the engine's in-flight sandbox fibers and waits for them to finish unwinding. Awaiting matters — returning early would leave the same race open, just narrower.
  • The shared execution-stack middleware runs it with Effect.ensuring, so the fibers are gone before the request scope closes the connection.

Nothing is lost by ending them there. On a per-request engine a pause is already unreachable once the response is written: a resume lands on a different engine and replays the call instead of resolving the original fiber. The MCP session Durable Object does not go through this middleware — it builds its stack over a session-lifetime handle — so its pauses still survive between requests, which is the point of that plane.

shutdown is a required member rather than an optional one. Two production decorators rebuild the engine as a fresh object literal, and an optional member would have let them drop it silently, reopening the race with nothing to show for it. Making it required turned that into a compile error and surfaced both.

This does not touch the storage error classification added earlier. These faults should stop happening, not stop being reported.

Testing

  • bun run typecheck green across all packages; lint and format:check clean.
  • New regression coverage beside the existing pause/resume tests: a paused execution's sandbox fiber is instrumented so the test can observe whether it is still alive. It asserts the fiber is alive while the response is served, that shutdown ends it, and that shutdown does not block the request it runs in. Verified red before the fix — with the interrupt removed it fails on "shutdown interrupted the parked sandbox fiber: expected false to be true" — and green after. An earlier version of this test passed against the broken code because it only asserted bookkeeping; it was rewritten to observe the fiber itself.
  • Unit suites for every package touched: execution, analytics, mcp host, cloudflare host, cloud.
  • Cloud e2e: storage error report shape, connections credentials, execution metering, emit envelope, execution limits, browser resume page — all passing, including the scenarios covering execute, pause/approval and metering.

executeWithPause forks the sandbox as a daemon so a pause can outlive the
caller that observed it. That fiber closes over the executor, and the FumaDB
client captures its handle at construction rather than resolving it per
operation, so the fiber keeps a live reference to the connection the host
opened for the scope that built the engine.

On the HTTP executor plane that scope is the request, and its finalizer closes
the pool as soon as the response is written. A sandbox still parked at that
moment issues its next query against a closed pool.

Add ExecutionEngine.shutdown, which interrupts the engine's in-flight sandbox
fibers and waits for them to unwind, and run it from the shared execution-stack
middleware so it happens before the request scope tears the connection down.
Nothing is lost by ending it there: on a per-request engine the pause is
already unreachable once the response is written, since a resume lands on a
different engine and replays the call. The MCP session Durable Object does not
use this middleware and keeps its session-lifetime pauses.

shutdown is a required member so a decorator that rebuilds the engine object
cannot drop it silently.
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 0a47f3a Aug 28 2026, 03:52 AM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 0a47f3a Commit Preview URL

Branch Preview URL
Aug 28 2026, 03:52 AM

@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1799

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1799

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1799

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1799

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1799

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1799

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1799

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1799

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1799

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1799

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1799

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1799

executor

npm i https://pkg.pr.new/executor@1799

commit: 0a47f3a

@RhysSullivan
RhysSullivan marked this pull request as ready for review August 28, 2026 04:10
@RhysSullivan
RhysSullivan merged commit 6609fa6 into main Aug 28, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant