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); + }); +});