Follow-up from PR #1614 (Hyperdrive Postgres adapter).
Problem
Connection-backed database adapters (e.g. hyperdrive() / Postgres-over-Hyperdrive) cannot reuse the per-isolate singleton connection across event boundaries: a pg socket is bound to the request that opened it, and on a warm isolate workerd rejects reuse from a later event with Cannot perform I/O on behalf of a different request.
PR #1614 fixes the core read/write path by giving each request its own scoped connection (stashed in ALS; the runtime.db getter and getDb() prefer it). But several subsystems capture the runtime singleton db at construction and never consult the ALS-scoped connection:
| Site |
File |
cronExecutor = new CronExecutor(db, ...) |
packages/core/src/emdash-runtime.ts (~1265) |
PluginContextFactory({ db }) — backs every hook's content/media/users/cron access |
packages/core/src/plugins/context.ts (~892) |
MediaProviderContext = { db, storage } |
packages/core/src/emdash-runtime.ts (~1211) |
sandbox runner (createSandboxRunner({ db })) |
runtime init |
Consequences on Hyperdrive (warm isolate):
- Cron Triggers (
scheduled() -> scheduled publishing, plugin cron, system cleanup) query the singleton -> fail under the cross-request I/O guard.
- Plugin hooks that touch their context db hit the singleton -> fail on the request path too, not just cron.
- Media providers / sandboxed plugins that hold the singleton db are similarly affected.
Not a data-corruption risk (the work errors and is logged), but scheduled publishing and DB-querying plugins do not work on the Hyperdrive adapter. D1 and Node SQLite are unaffected (stateless across events).
Proposed fix
Make the captured singleton ALS-resolvable everywhere it is held, e.g. pass a resolver () => getRequestContext()?.db ?? singleton (mirroring the runtime.db getter) into PluginContextFactory, CronExecutor, media provider context, and the sandbox runner. Then:
- Give the cron path (
runScheduledTasks in packages/core/src/astro/middleware.ts) its own event-scoped connection via createRequestScopedDb, run the batch inside runWithContext({ db }), and close() it after — gated on the adapter actually being connection-backed (the scoped db exposes a close()), so D1/SQLite keep using the singleton unchanged.
This is core infrastructure shared by all adapters, so it needs dedicated cron/plugin-context regression tests (D1 + SQLite parity) to prove no behavior change for stateless bindings.
Acceptance
Follow-up from PR #1614 (Hyperdrive Postgres adapter).
Problem
Connection-backed database adapters (e.g.
hyperdrive()/ Postgres-over-Hyperdrive) cannot reuse the per-isolate singleton connection across event boundaries: a pg socket is bound to the request that opened it, and on a warm isolate workerd rejects reuse from a later event withCannot perform I/O on behalf of a different request.PR #1614 fixes the core read/write path by giving each request its own scoped connection (stashed in ALS; the
runtime.dbgetter andgetDb()prefer it). But several subsystems capture the runtime singletondbat construction and never consult the ALS-scoped connection:cronExecutor = new CronExecutor(db, ...)packages/core/src/emdash-runtime.ts(~1265)PluginContextFactory({ db })— backs every hook'scontent/media/users/cronaccesspackages/core/src/plugins/context.ts(~892)MediaProviderContext = { db, storage }packages/core/src/emdash-runtime.ts(~1211)createSandboxRunner({ db }))Consequences on Hyperdrive (warm isolate):
scheduled()-> scheduled publishing, plugin cron, system cleanup) query the singleton -> fail under the cross-request I/O guard.Not a data-corruption risk (the work errors and is logged), but scheduled publishing and DB-querying plugins do not work on the Hyperdrive adapter. D1 and Node SQLite are unaffected (stateless across events).
Proposed fix
Make the captured singleton ALS-resolvable everywhere it is held, e.g. pass a resolver
() => getRequestContext()?.db ?? singleton(mirroring theruntime.dbgetter) intoPluginContextFactory,CronExecutor, media provider context, and the sandbox runner. Then:runScheduledTasksinpackages/core/src/astro/middleware.ts) its own event-scoped connection viacreateRequestScopedDb, run the batch insiderunWithContext({ db }), andclose()it after — gated on the adapter actually being connection-backed (the scoped db exposes aclose()), so D1/SQLite keep using the singleton unchanged.This is core infrastructure shared by all adapters, so it needs dedicated cron/plugin-context regression tests (D1 + SQLite parity) to prove no behavior change for stateless bindings.
Acceptance
hyperdrive()JSDoc, adapter module header, and changeset.