Skip to content

Thread an event-scoped DB connection through cron, plugin contexts, media providers, and sandbox runner #1622

Description

@ascorbic

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

  • Scheduled publishing, plugin cron, and system cleanup work on the Hyperdrive adapter on a warm isolate.
  • Plugin hooks that query the DB work on Hyperdrive on both request and cron paths.
  • No behavior change for D1 / Node SQLite (query-count snapshots + cron tests green).
  • Remove the "request path only" limitation note from the hyperdrive() JSDoc, adapter module header, and changeset.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions