porsager / porsager/postgres

Cloudflare/workerd: sql.reserve() never resolves on a pool that has not yet opened a connection

Open
#1,203 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
8.7k
Forks
374
Avg merge
11d 16h
Merged PRs (30d)
1

Description

Summary

On the Cloudflare Workers build (cf/), sql.reserve() never resolves if the pool has not yet opened a connection. It does not reject or time out — the returned promise simply stays pending forever.

Once any query has run on the pool, reserve() works normally and keeps working, because releasing returns the connection to open.

This bites anything that reserves as its first act. kysely-postgres-js acquires connections with postgres.reserve(), so the very first Kysely query against a fresh client hangs indefinitely.

Version: postgres@3.4.9, workerd via @cloudflare/vitest-plugin (also reachable through wrangler dev).

Reproduction
import postgres from 'postgres'

// Cold pool — hangs forever.
const cold = postgres(connectionString, { max: 1, fetch_types: false })
await cold.reserve()          // never settles

// Same pool, after one query — fine.
const warm = postgres(connectionString, { max: 1, fetch_types: false })
await warm`SELECT 1`
await warm.reserve()          // resolves immediately

Observed with max: 1 and max: 2 alike, so it is not pool-size exhaustion.

Where it seems to come from

reserve() in cf/src/index.js:

const c = open.length
  ? open.shift()
  : await new Promise((resolve, reject) => {
      const query = { reserve: resolve, reject }
      queries.push(query)
      closed.length && connect(closed.shift(), query)
    })

On a cold pool open is empty, so it queues a waiter whose only chance of being driven is the closed.length && connect(...) on the same line. If nothing is sitting in closed at that moment — or if connect() does not carry the queued reserve waiter through to resolution on this runtime — nothing ever resolves it. The socket polyfill's connect() is async and dynamically imports cloudflare:sockets, so connection setup here does not make synchronous progress the way it does on Node, which may be the relevant difference.

I have not traced it far enough to propose a patch with confidence, so this is a report rather than a PR — happy to dig further if the diagnosis above looks like it is pointing at the right place.

Workaround

Run one trivial query before handing the pool to anything that reserves:

const sql = postgres(connectionString, { max: 1, fetch_types: false })
await sql`SELECT 1`

Related: #1202 (unhandled rejection on sql.end() in the same build).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in cf/src/index.js at the reserve() implementation and trace how the queued waiter is handled by connect(), including the socket polyfill path described in the report. Run the cold-pool reproduction with max 1 and max 2, then verify that reserve() settles on a fresh pool without requiring a preliminary query.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, postgres
Domain
databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.