fix(core): join running embedded Postgres correctly

Read the live port from PostgreSQL's actual postmaster.pid field so extension TaskStore boot reuses the existing server instead of wedging on a colliding start.
This commit is contained in:
gsxdsm
2026-07-18 14:05:00 -07:00
parent 9bc0eb6943
commit 7b1a89d1bc
3 changed files with 39 additions and 29 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Prevent fn_task_show timeouts when another Fusion process already owns embedded PostgreSQL.
category: fix
dev: Reads the port from PostgreSQL's actual postmaster.pid line 4 field before joining the running instance.

View File

@@ -808,7 +808,7 @@ describe("embedded-lifecycle: startup race (cross-process)", () => {
async start() { async start() {
writeFileSync( writeFileSync(
join(dataDir, "postmaster.pid"), join(dataDir, "postmaster.pid"),
["12345", dataDir, "/tmp", "localhost", "55440", "5432101", String(Date.now())].join("\n") + "\n", ["12345", dataDir, String(Date.now()), "55440", "/tmp", "localhost", "5432101", "ready"].join("\n") + "\n",
); );
throw new Error('lock file "postmaster.pid" already exists'); throw new Error('lock file "postmaster.pid" already exists');
} }
@@ -864,7 +864,7 @@ describe("embedded-lifecycle: startup race only joins on a lock collision", () =
// A postmaster.pid exists (ours, or a racer's) but the failure is NOT a lock collision. // A postmaster.pid exists (ours, or a racer's) but the failure is NOT a lock collision.
writeFileSync( writeFileSync(
join(dataDir, "postmaster.pid"), join(dataDir, "postmaster.pid"),
["12345", dataDir, "/tmp", "localhost", "55442", "5432101", String(Date.now())].join("\n") + "\n", ["12345", dataDir, String(Date.now()), "55442", "/tmp", "localhost", "5432101", "ready"].join("\n") + "\n",
); );
throw new Error("could not start postgres: readiness poll timed out"); throw new Error("could not start postgres: readiness poll timed out");
} }
@@ -891,7 +891,7 @@ describe("embedded-lifecycle: startup race only joins on a lock collision", () =
async start() { async start() {
writeFileSync( writeFileSync(
join(dataDir, "postmaster.pid"), join(dataDir, "postmaster.pid"),
["12345", dataDir, "/tmp", "localhost", "55444", "5432101", String(Date.now())].join("\n") + "\n", ["12345", dataDir, String(Date.now()), "55444", "/tmp", "localhost", "5432101", "ready"].join("\n") + "\n",
); );
throw new Error('lock file "postmaster.pid" already exists'); throw new Error('lock file "postmaster.pid" already exists');
} }
@@ -918,7 +918,7 @@ describe("embedded-lifecycle: join-path database verify is best-effort", () => {
// A port nothing is listening on: the verify's probe cannot succeed. // A port nothing is listening on: the verify's probe cannot succeed.
writeFileSync( writeFileSync(
join(dataDir, "postmaster.pid"), join(dataDir, "postmaster.pid"),
["12345", dataDir, "/tmp", "localhost", "55441", "5432101", String(Date.now())].join("\n") + "\n", ["12345", dataDir, String(Date.now()), "55441", "/tmp", "localhost", "5432101", "ready"].join("\n") + "\n",
); );
const logLines: string[] = []; const logLines: string[] = [];
@@ -1027,28 +1027,30 @@ describe("embedded-lifecycle: startup timeout (P1 #24)", () => {
}); });
describe("embedded-lifecycle: readPortFromPostmasterPid (P1 code-review fix)", () => { describe("embedded-lifecycle: readPortFromPostmasterPid (P1 code-review fix)", () => {
it("reads the TCP port from line 5 (index 4) of postmaster.pid", () => { it("reads the TCP port from PostgreSQL's real line 4 (index 3) postmaster.pid layout", () => {
const dir = mkdtempSync(join(tmpdir(), "fusion-embedded-pid-")); const dir = mkdtempSync(join(tmpdir(), "fusion-embedded-pid-"));
try { try {
const { writeFileSync } = require("node:fs"); const { writeFileSync } = require("node:fs");
// Standard PostgreSQL postmaster.pid format: // Standard PostgreSQL postmaster.pid format:
// Line 1: PID // Line 1: PID
// Line 2: data directory // Line 2: data directory
// Line 3: unix socket directory // Line 3: postmaster start timestamp
// Line 4: listen address // Line 4: port number
// Line 5: port number // Line 5: unix socket directory
// Line 6: shared memory key // Line 6: listen address
// Line 7: postmaster start timestamp // Line 7: shared memory key and id
// Line 8: status
writeFileSync( writeFileSync(
join(dir, "postmaster.pid"), join(dir, "postmaster.pid"),
[ [
"12345", "12345",
"/home/user/.fusion/embedded-postgres/default", "/home/user/.fusion/embedded-postgres/default",
"1784361395",
"55432",
"/tmp", "/tmp",
"localhost", "localhost",
"55432", "18446744071752735336 19857409",
"5432101", "ready",
String(Date.now()),
].join("\n") + "\n", ].join("\n") + "\n",
); );
@@ -1065,7 +1067,7 @@ describe("embedded-lifecycle: readPortFromPostmasterPid (P1 code-review fix)", (
const { writeFileSync } = require("node:fs"); const { writeFileSync } = require("node:fs");
writeFileSync( writeFileSync(
join(dir, "postmaster.pid"), join(dir, "postmaster.pid"),
["12345", "/data", "/tmp", "localhost", "not-a-port", "5432101"].join("\n") + "\n", ["12345", "/data", "1784361395", "not-a-port", "/tmp", "localhost", "5432101", "ready"].join("\n") + "\n",
); );
expect(readPortFromPostmasterPid(dir)).toBeNull(); expect(readPortFromPostmasterPid(dir)).toBeNull();
} finally { } finally {
@@ -1082,16 +1084,15 @@ describe("embedded-lifecycle: readPortFromPostmasterPid (P1 code-review fix)", (
} }
}); });
it("does NOT read line 3 (index 2, socket dir) as the port", () => { it("does NOT read line 3 (index 2, start timestamp) as the port", () => {
// Regression: the bug read lines[2] (socket dir) which is never the port. // The start timestamp is numeric, so parsing the wrong adjacent line would
// If the socket dir happened to contain digits, parseInt would produce // produce a plausible-looking but unusable port.
// a wrong port. This test ensures we skip past it.
const dir = mkdtempSync(join(tmpdir(), "fusion-embedded-pid-")); const dir = mkdtempSync(join(tmpdir(), "fusion-embedded-pid-"));
try { try {
const { writeFileSync } = require("node:fs"); const { writeFileSync } = require("node:fs");
writeFileSync( writeFileSync(
join(dir, "postmaster.pid"), join(dir, "postmaster.pid"),
["12345", "/data", "/var/run/postgresql", "localhost", "5433", "5432101"].join("\n") + "\n", ["12345", "/data", "1784361395", "5433", "/var/run/postgresql", "localhost", "5432101", "ready"].join("\n") + "\n",
); );
const port = readPortFromPostmasterPid(dir); const port = readPortFromPostmasterPid(dir);
expect(port).toBe(5433); expect(port).toBe(5433);

View File

@@ -927,16 +927,18 @@ const runningInstances = new Map<string, { port: number; database: string }>();
* Read the port from a postmaster.pid file. The standard PostgreSQL format is: * Read the port from a postmaster.pid file. The standard PostgreSQL format is:
* Line 1 (index 0): PID * Line 1 (index 0): PID
* Line 2 (index 1): Data directory path * Line 2 (index 1): Data directory path
* Line 3 (index 2): Unix socket directory * Line 3 (index 2): Postmaster start timestamp
* Line 4 (index 3): Listen address (e.g. localhost or *) * Line 4 (index 3): Port number
* Line 5 (index 4): Port number * Line 5 (index 4): Unix socket directory
* Line 6 (index 5): Shared memory key * Line 6 (index 5): Listen address (e.g. localhost or *)
* Line 7 (index 6): Postmaster start timestamp * Line 7 (index 6): Shared memory key and id
* Line 8 (index 7): Status
* *
* FNXC:PostgresCutover 2026-06-27-14:30 (fix code-review P1): * FNXC:PostgresCutover 2026-06-27-14:30 (fix code-review P1):
* Previously read line 3 (index 2, the socket dir) which is never a port * The earlier correction still encoded the wrong field order and read line 5
* number, so singleton detection via postmaster.pid ALWAYS failed. Fixed to * (the socket directory). Read PostgreSQL's actual line 4 port field so a
* read line 5 (index 4, the TCP port). * second Fusion process joins the running postmaster instead of attempting a
* colliding start that can wedge extension TaskStore boot.
* *
* Returns null if the file cannot be read or parsed. * Returns null if the file cannot be read or parsed.
*/ */
@@ -944,8 +946,8 @@ export function readPortFromPostmasterPid(dataDir: string): number | null {
try { try {
const content = readFileSync(join(dataDir, "postmaster.pid"), "utf-8"); const content = readFileSync(join(dataDir, "postmaster.pid"), "utf-8");
const lines = content.split("\n"); const lines = content.split("\n");
// Line 5 (index 4) is the TCP port in standard PostgreSQL postmaster.pid // Line 4 (index 3) is the TCP port in standard PostgreSQL postmaster.pid.
const portStr = lines[4]?.trim(); const portStr = lines[3]?.trim();
if (portStr) { if (portStr) {
const port = parseInt(portStr, 10); const port = parseInt(portStr, 10);
if (!isNaN(port) && port > 0) return port; if (!isNaN(port) && port > 0) return port;