db.ts: init().end() resolves before the connection pool is closed
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 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 insrc/server/routes/is awaited, andawait pgMeta.end()follows it — so nothing is in flight whenend()returns early. GET /generators/{typescript,go,swift,python}reachend()throughgetGeneratorMetadata'sfinally(src/lib/generators.ts), which wrapsintrospect()from@supabase/postgrest-typegen.introspect()issues its introspection work as ten concurrent operations on the samePostgresMetainstance under an un-caughtPromise.all.Promise.allrejects on the first rejection, so if one of them fails while the others are still outstanding, thefinallycallsend()on a pool that is not idle — andend()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/dbDocker fixture (supabase/postgres:14.1.0) @sentry/node: 9.12.0pg:npm:@supabase/pg@8.21.1
- Dominant language
- TypeScript
- Stars
- 1.2k
- Forks
- 223
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from supabase/postgres-meta
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
supabase/postgres-meta#1147 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
supabase/postgres-meta#1146 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
supabase/postgres-meta#1143 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
supabase/postgres-meta#1142 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
supabase/postgres-meta#1140 ·
All issues in supabase/postgres-meta
Similar issues
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 91/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100