From 12bc09718ce154f8cbbf015dc643eac397fc2c1f Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 15 Jan 2026 01:08:55 +0000 Subject: [PATCH] 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 --- src/sql/postgres/PostgresSQLConnection.zig | 7 ++- test/js/sql/sql-fixture-unref.ts | 23 +++++++++ test/js/sql/sql.test.ts | 30 +++++++++++ test/regression/issue/03548.test.ts | 59 ++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 test/js/sql/sql-fixture-unref.ts create mode 100644 test/regression/issue/03548.test.ts diff --git a/src/sql/postgres/PostgresSQLConnection.zig b/src/sql/postgres/PostgresSQLConnection.zig index 7cbceae629..66f1666f1b 100644 --- a/src/sql/postgres/PostgresSQLConnection.zig +++ b/src/sql/postgres/PostgresSQLConnection.zig @@ -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); diff --git a/test/js/sql/sql-fixture-unref.ts b/test/js/sql/sql-fixture-unref.ts new file mode 100644 index 0000000000..dcb2dc31c9 --- /dev/null +++ b/test/js/sql/sql-fixture-unref.ts @@ -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(); diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 5067ec783c..9f1e1585db 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -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 }); diff --git a/test/regression/issue/03548.test.ts b/test/regression/issue/03548.test.ts new file mode 100644 index 0000000000..a72db2caca --- /dev/null +++ b/test/regression/issue/03548.test.ts @@ -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); + }); +});