Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/sql/postgresjs.exit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { test, expect } from "vitest";
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { execFile } from "node:child_process";

function runScript(
script: string,
): Promise<{ exitCode: number; timedOut: boolean }> {
return new Promise((resolve) => {
const child = execFile(
"node",
["--import=tsx", "--input-type=module", "-e", script],
{ timeout: 5_000 },
(error) => {
resolve({
exitCode: error ? Number(error.code ?? 1) : 0,
timedOut: error?.killed === true,
});
},
);
// Pipe to a pipe (not inherit) to mimic CI where SIGPIPE can occur
child.stdout?.resume();
child.stderr?.resume();
});
}

/**
* Verifies that a process with an idle source pool connection exits
* naturally without hanging. Without allowExitOnIdle, pg-pool keeps
* idle connections ref'd, which blocks Node from exiting.
*/
test("process exits without hanging when source pool has idle connections", async () => {
const pg = await new PostgreSqlContainer("postgres:17").start();

const script = `
import { connectToSource } from "./src/sql/postgresjs.ts";
import { Connectable } from "./src/sync/connectable.ts";
const db = connectToSource(Connectable.fromString("${pg.getConnectionUri()}"));
await db.exec("SELECT 1");
// Intentionally do NOT call db.close() — idle connection stays in pool.
// With allowExitOnIdle, the process should still exit promptly.
// Without it, the idle connection's ref'd socket blocks exit.
`;

try {
const { exitCode, timedOut } = await runScript(script);

expect(timedOut, "process should not hang on idle pool connections").toBe(
false,
);
expect(exitCode, "process should exit 0, not SIGPIPE (13)").toBe(0);
} finally {
await pg.stop();
}
});

/**
* Verifies that a process exits cleanly (code 0) after explicitly
* closing the source pool. This guards against SIGPIPE (exit 13)
* caused by process.exit() killing I/O mid-flush.
*/
test("process exits with code 0 after explicit pool close", async () => {
const pg = await new PostgreSqlContainer("postgres:17").start();

const script = `
import { connectToSource } from "./src/sql/postgresjs.ts";
import { Connectable } from "./src/sync/connectable.ts";
const db = connectToSource(Connectable.fromString("${pg.getConnectionUri()}"));
await db.exec("SELECT 1");
await db.close();
`;

try {
const { exitCode, timedOut } = await runScript(script);

expect(timedOut, "process should not hang after pool.end()").toBe(false);
expect(exitCode, "process should exit 0, not SIGPIPE (13)").toBe(0);
} finally {
await pg.stop();
}
});
1 change: 1 addition & 0 deletions src/sql/postgresjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function connectToSource(
const config: PoolConfig = {
max: 20,
idleTimeoutMillis: DEFAULT_IDLE_TIMEOUT_MS,
allowExitOnIdle: true,
};

return connect(connectable, config);
Expand Down