db.ts: init().end() resolves before the connection pool is closed

Open Beginner friendly
#1,150 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
postgres, typescript
Domain
database

Research direction

Start in src/lib/db.ts around the init().end() implementation and compare its Sentry.startSpan handling with the other call sites. Use the reproduction in test/lib/db.ts, register it in test/index.test.ts, then run npm run db:run and npx vitest run test/index.test.ts -t 'in-flight queries'. Done means end() resolves only after the pool and in-flight query have finished closing.

Written by the indexing model from the issue text.

Description

bug

Bug report

  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

init().end() is declared end: () => Promise<void> (src/lib/db.ts:73) and is awaited at 65 call sites — 64 in src/server/routes/*, 1 in src/lib/generators.ts. It resolves before the connection pool is closed.

src/lib/db.ts:249-264, verbatim:

      async end() {
        Sentry.startSpan({ op: 'db', name: 'init.end' }, async () => {
          try {
            const _pool = pool
            pool = null
            // Gracefully wait for active connections to be idle, then close all
            // connections in the pool.
            if (_pool) {
              await _pool.end()
            }
          } catch (endError) {
            // Ignore any errors during cleanup just log them
            console.error('Failed ending connection pool', endError)
          }
        })
      },

The Sentry.startSpan(...) call on :250 is not returned, so the promise wrapping await _pool.end() is discarded and end() resolves immediately. The other three Sentry.startSpan call sites in this file — :31, :75, :117 — all return their result.

Introduced by bf2fad4 (14 Apr 2025, "wip: add additional details remove pii headers"), which moved the body of end() inside a span callback. Before that commit the method awaited the pool directly:

// bf2fad4^ — src/lib/db.ts:212
    async end() {
      try {
        const _pool = pool
        pool = null
        // Gracefully wait for active connections to be idle, then close all
        // connections in the pool.
        if (_pool) {
          await _pool.end()
        }
      } catch (endError) {
        // Ignore any errors during cleanup just log them
        console.error('Failed ending connection pool', endError)
      }
    },

To Reproduce

Save as test/lib/db.ts:

import { randomUUID } from 'node:crypto'
import { expect, test } from 'vitest'
import { init } from '../../src/lib/db'
import { pgMeta, TEST_CONNECTION_STRING } from './utils'
 
test('end() resolves only after in-flight queries have finished', async () => {
  const applicationName = `pg-meta-end-${randomUUID()}`
  const db = init({
    max: 1,
    connectionString: TEST_CONNECTION_STRING,
    application_name: applicationName,
  })
 
  const inFlight = db.query('select pg_sleep(1)').then(() => 'query' as const)
  const teardown = db.end().then(() => 'end' as const)
 
  expect(await Promise.race([inFlight, teardown])).toBe('query')
  await teardown
 
  const { data } = await pgMeta.query(
    `select 1 from pg_stat_activity where application_name = '${applicationName}'`
  )
  expect(data).toHaveLength(0)
 
  await inFlight
})

Register it in test/index.test.ts:

 import './lib/columns'
 import './lib/config'
+import './lib/db'
 import './lib/extensions'

Then:

npm run db:run
npx vitest run test/index.test.ts -t 'in-flight queries'

The query is started and not awaited, so the pool is busy; the two promises are then raced. If end() waits for the pool to drain, the query has to settle first. On f380cc5 it does not:

❯ test/index.test.ts (149 tests | 1 failed | 148 skipped) 14ms
  × end() resolves only after in-flight queries have finished 13ms
    → expected 'end' to be 'query' // Object.is equality
 
AssertionError: expected 'end' to be 'query' // Object.is equality
Expected: "query"
Received: "end"
 ❯ test/lib/db.ts:17:52

Adding return in front of Sentry.startSpan on src/lib/db.ts:250 makes it pass:

✓ test/index.test.ts (149 tests | 148 skipped) 1138ms
  ✓ end() resolves only after in-flight queries have finished 1136ms
Tests  1 passed | 148 skipped (149)

Expected behavior

await db.end() resolves after the pool has been closed, matching the declared Promise<void> return type and the comment at :254-255 ("Gracefully wait for active connections to be idle, then close all connections in the pool").

Where this shows up

  • The CRUD routes (the 64 sites above) are sequential — every pgMeta.* call in src/server/routes/ is awaited, and await pgMeta.end() follows it — so nothing is in flight when end() returns early.
  • GET /generators/{typescript,go,swift,python} reach end() through getGeneratorMetadata's finally (src/lib/generators.ts), which wraps introspect() from @supabase/postgrest-typegen. introspect() issues its introspection work as ten concurrent operations on the same PostgresMeta instance under an un-caught Promise.all. Promise.all rejects on the first rejection, so if one of them fails while the others are still outstanding, the finally calls end() on a pool that is not idle — and end() returns before that outstanding work is done.

Suggested fix

       async end() {
-        Sentry.startSpan({ op: 'db', name: 'init.end' }, async () => {
+        return Sentry.startSpan({ op: 'db', name: 'init.end' }, async () => {

I have this plus the regression test above ready and will open a PR against this issue.

System information

  • OS: macOS (arm64)
  • postgres-meta: f380cc5be21edef4e77d9838c732d1f20af0b3c0 (master at time of writing)
  • Node.js: v22.23.2
  • Test database: the test/db Docker fixture (supabase/postgres:14.1.0)
  • @sentry/node: 9.12.0
  • pg: npm:@supabase/pg@8.21.1
Dominant language
TypeScript
Stars
1.2k
Forks
223
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

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.

More from supabase/postgres-meta

All issues in supabase/postgres-meta

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.