fix(sql): allow process to exit when PostgreSQL connection is idle

Previously, idle PostgreSQL connections would keep the Bun process alive
indefinitely after queries completed. This happened because `updateRef()`
always kept the poll reference when `pending_activity_count > 0`, which
includes any non-disconnected connection status.

This fix modifies `updateRef()` to unref the poll when the connection is
idle (connected with no pending queries and no data to write), matching
Node.js behavior where idle database connections allow the process to exit.

Fixes #3548

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-15 01:08:55 +00:00
parent 22bebfc467
commit 12bc09718c
4 changed files with 118 additions and 1 deletions

View File

@@ -1792,7 +1792,12 @@ pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.enum_litera
pub fn updateRef(this: *PostgresSQLConnection) void {
this.updateHasPendingActivity();
if (this.pending_activity_count.raw > 0) {
// Don't keep the process alive when the connection is idle (connected with no pending work).
// This matches Node.js behavior where idle database connections allow the process to exit.
if (this.status == .connected and !this.hasQueryRunning() and this.write_buffer.remaining().len == 0) {
this.poll_ref.unref(this.vm);
} else if (this.status != .disconnected) {
// Keep alive during connection establishment or when there's pending work
this.poll_ref.ref(this.vm);
} else {
this.poll_ref.unref(this.vm);

23
test/js/sql/sql-fixture-unref.ts generated Normal file
View File

@@ -0,0 +1,23 @@
// This test verifies that idle PostgreSQL connections allow the process to exit.
// Fixes #3548: Database clients that maintain persistent connections should not
// prevent the Bun process from exiting after queries complete.
//
// This test passes by:
// 1. Printing "query_done"
// 2. Exiting with code 0 within a reasonable timeout
//
// If the bug is present, the process will hang indefinitely after the query completes.
import { sql } from "bun";
async function main() {
// Execute a query
const result = await sql`select 1 as x`;
console.log("query_done");
// The connection is now idle. The process should exit naturally
// without needing to explicitly close the connection.
// Note: We intentionally do NOT call sql.close() here to test that
// idle connections don't keep the process alive.
}
main();

View File

@@ -3907,6 +3907,36 @@ CREATE TABLE ${table_name} (
expect(result.stdout.toString().split("\n")).toEqual(["1", "2", ""]);
});
test("idle connection allows process to exit #3548", async () => {
// This test verifies that idle PostgreSQL connections don't keep the process alive.
// Before the fix, the process would hang indefinitely after queries completed.
const file = path.posix.join(__dirname, "sql-fixture-unref.ts");
// Use Bun.spawn with a timeout to detect if the process hangs
await using proc = Bun.spawn([bunExe(), file], {
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
stdout: "pipe",
stderr: "pipe",
});
// Wait for exit with a 10 second timeout - process should exit much faster
const exitPromise = proc.exited;
const timeoutPromise = new Promise<"timeout">(resolve => setTimeout(() => resolve("timeout"), 10000));
const result = await Promise.race([exitPromise, timeoutPromise]);
if (result === "timeout") {
proc.kill();
throw new Error("Process hung - idle connection prevented exit (issue #3548)");
}
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
expect(stdout.trim()).toBe("query_done");
expect(result).toBe(0);
});
describe("Boolean Array Type", () => {
test("should handle empty boolean array", async () => {
await using sql = postgres({ ...options, max: 1 });

View File

@@ -0,0 +1,59 @@
// #3548 - Database clients that maintain persistent connections don't allow Bun process to exit
//
// This test verifies that idle PostgreSQL connections allow the process to exit.
// The fix modifies `updateRef()` in PostgresSQLConnection.zig to unref the poll
// when the connection is idle (connected with no pending queries).
//
// The actual fix test is in test/js/sql/sql.test.ts ("idle connection allows process to exit #3548")
// This file documents the issue and provides a reference.
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isDockerEnabled, tempDir } from "harness";
import path from "path";
describe.skipIf(!isDockerEnabled())("issue #3548 - idle SQL connection should allow process exit", () => {
test("process exits after SQL query completes", async () => {
// Create a test script that runs a query and should exit naturally
using dir = tempDir("issue-3548", {
"test.ts": `
import { sql } from "bun";
async function main() {
// Run a simple query
const result = await sql\`select 1 as x\`;
console.log("done:", result[0].x);
// Process should exit here without explicitly closing the connection
}
main();
`,
});
// This test requires DATABASE_URL to be set with a valid PostgreSQL connection
if (!process.env.DATABASE_URL) {
console.log("Skipping: DATABASE_URL not set");
return;
}
await using proc = Bun.spawn([bunExe(), path.join(String(dir), "test.ts")], {
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
stdout: "pipe",
stderr: "pipe",
});
// Wait for exit with timeout - before fix, this would hang indefinitely
const exitPromise = proc.exited;
const timeout = new Promise<"timeout">(resolve => setTimeout(() => resolve("timeout"), 10000));
const result = await Promise.race([exitPromise, timeout]);
if (result === "timeout") {
proc.kill();
throw new Error("Process hung - idle connection prevented exit (issue #3548)");
}
const stdout = await new Response(proc.stdout).text();
expect(stdout.trim()).toBe("done: 1");
expect(result).toBe(0);
});
});