sql: make close({ timeout: 0 }) force-close immediately with queries in flight#32039
sql: make close({ timeout: 0 }) force-close immediately with queries in flight#32039robobun wants to merge 3 commits into
Conversation
WalkthroughThe PR fixes a bug where ChangesSQL Pool Close Timeout Fix
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Updated 11:07 AM PT - Jun 12th, 2026
❌ @robobun, your commit 83ce87f has 3 failures in
🧪 To try this PR locally: bunx bun-pr 32039That installs a local version of the PR into your bun-32039 --bun |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/sql/sql-close-timeout-zero.test.ts`:
- Around line 76-95: The listen-await promise in postgresMock (the new
Promise<void>(r => server.listen(0, "127.0.0.1", () => r()))) does not reject on
server errors causing tests to hang; change it to use new Promise((resolve,
reject) => { server.once("error", reject); server.listen(..., () => {
server.removeListener("error", reject); resolve(); }) }) so bind/listen failures
reject immediately, and apply the same pattern to the other server.listen usage
in this file that follows the same pattern.
- Around line 201-246: Add a twin test for MySQL that covers the close({
timeout: null }) (graceful/drain) path: duplicate the existing "mysql: close({
timeout: 0 }) settles immediately with a query in flight" test, rename it to
something like "mysql: close({ timeout: null }) drains and waits for in-flight
query", and in the duplicate call sql.close({ timeout: null }) instead of
timeout: 0; keep the same mock server/queryReceived setup and assertions but
match the expected graceful behavior used in the Postgres null test (use the
same final expectation/assertion that the Postgres null test uses for
SQL/sql.close to verify the in-flight query is drained rather than immediately
closed). Ensure you reference the SQL instance (new SQL(...)) and sql.close({
timeout: null }) in the new test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 664f8ba0-7528-49cc-8207-a93fea1c0254
📒 Files selected for processing (3)
src/js/internal/sql/mysql.tssrc/js/internal/sql/postgres.tstest/js/sql/sql-close-timeout-zero.test.ts
|
CI status: four full runs of this diff (now rebased onto main with the fix in shared.ts), and every lane that runs this PR's files is green in all of them. Each run's only red was unrelated: bunx.test.ts live-registry breakage (repo-wide, skipped on main in #32042), a streams-leak.test.ts GC-counter flake (debian aarch64, build 61613), a duckdb.test.ts N-API segfault (debian x64, build 61702), and in build 62102 an AsyncLocalStorage-tracking.test.ts subprocess flake (darwin 14 aarch64) plus a darwin 26 shard that died building the vendor/elysia fixture (missing tsup, infra). None involve SQL code paths; the diff is two lines in BaseSQLAdapter.close plus its tests. Ready for review. |
|
#32091 was filed for this same truthiness gate and is now closed as a duplicate of #32038. While reproducing it I ended up with an overlapping branch,
Two adjacent pre-existing bugs surfaced while testing, filed separately: #32095 (a forced close never resolves while a pool connection is mid-handshake, so force-close tests need an established connection with the query already on the wire) and #32096 (the |
…in flight The pool close() in the postgres and mysql adapters gated the timeout option on truthiness, so timeout: 0 fell into the graceful-drain branch and waited for pending queries indefinitely, even though the code below the gate explicitly treats timeout === 0 as an immediate close. Gate on presence (!= null) instead, so 0 flows through validation and hits the immediate-close branch. undefined and null still mean graceful drain with no timer.
6a7e535 to
83ce87f
Compare
There was a problem hiding this comment.
LGTM — one-line truthiness→presence fix with thorough mock-server tests for both adapters.
Extended reasoning...
Overview
This PR fixes #32038: sql.close({ timeout: 0 }) was gated on if (timeout) (truthiness), so 0 fell into the graceful-drain branch instead of force-closing. The inner timeout === 0 immediate-close branch already existed but was unreachable. The fix is a single-line change in src/js/internal/sql/shared.ts (if (timeout) → if (timeout != null)), now applied once in the consolidated BaseSQLAdapter.close() after #32145 merged the duplicate postgres/mysql copies. The accompanying ~320-line test file adds mock TCP servers (no Docker) that complete the handshake and stall the query, then asserts force-close (timeout: 0) vs graceful-drain (timeout: null) for both postgres and mysql.
Security risks
None. This touches pool-close lifecycle timing only — no auth, crypto, permissions, or untrusted input handling. The only side effect noted is that timeout: NaN now throws ERR_INVALID_ARG_VALUE (consistent with other invalid values) instead of being silently ignored, which is a correctness improvement.
Level of scrutiny
Low. The source change is one effective line plus a clarifying comment, fixing a classic truthiness-vs-presence bug where the intended behavior was already coded (the timeout === 0 branch) but unreachable. The fix is the conventional != null presence check. undefined/null continue to mean graceful drain.
Other factors
- No bugs flagged by the bug-hunting system.
- Both CodeRabbit comments (reject on listen error, MySQL
timeout: nulldrain twin) were addressed in commit 8807866 and confirmed resolved. - robobun reports all relevant CI lanes green across three runs; remaining red is unrelated flake (bunx live-registry, streams-leak GC, duckdb N-API) outside this PR's code paths.
- No CODEOWNERS for these files.
- The follow-up note about applying the same gate to
reserved_sql.close/transaction_sql.closeis a validation-consistency nicety (NaN handling), not a blocker — the PR description confirms those paths already force-close on0.
Fixes #32038
Repro
Cause
The pool
close()(nowBaseSQLAdapter.closeinsrc/js/internal/sql/shared.ts, used by both the postgres and mysql adapters) gates the timeout option on truthiness:The inner
timeout === 0branch shows the intent (immediate close), butif (timeout)makes it unreachable:close({ timeout: 0 })with in-flight queries behaved exactly likeclose()with no options and waited for every pending query to drain. Long-standing behavior, not a regression.The fix is the gate:
if (timeout)becomesif (timeout != null), so0flows through validation and hits the immediate-close branch.undefinedandnullkeep meaning graceful drain with no timer. A side effect of the presence gate is thattimeout: NaNnow throwsERR_INVALID_ARG_VALUElike other invalid values (e.g.timeout: "abc") already did, instead of being silently ignored.Rebase note: the fix originally patched the two identical
close()copies inpostgres.tsandmysql.ts; after #32145 consolidated the pool plumbing intoshared.ts, the same one-line gate change now lands inBaseSQLAdapter.closeand the driver files are untouched.The similar-looking truthiness gates in
reserved_sql.close/transaction_sql.close(src/js/bun/sql.ts) are not affected: there, falling through for0lands on the immediate cancel-and-close path, which is already the correct semantics.Tests
test/js/sql/sql-close-timeout-zero.test.tsuses mock servers (no Docker) that complete the handshake and never answer the query, so a query stays in flight forever:close({ timeout: 0 })settles immediately and the in-flight query rejects withERR_POSTGRES_CONNECTION_CLOSED/ERR_MYSQL_CONNECTION_CLOSEDclose({ timeout: null })still drains gracefully (the pending query completes normally)On the unfixed build, both
timeout: 0tests hang inclose()and time out; with the fix all four pass. Existing suites that callclose({ timeout: 0 })(sql-connect-error-reporting, sql-mysql-query-string-leak, postgres-tls-ctx-leak) still pass.