diff --git a/.changeset/fix-extension-taskstore-postmaster-port.md b/.changeset/fix-extension-taskstore-postmaster-port.md new file mode 100644 index 0000000000..ed882ce44e --- /dev/null +++ b/.changeset/fix-extension-taskstore-postmaster-port.md @@ -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. diff --git a/packages/core/src/__tests__/postgres/embedded-lifecycle.test.ts b/packages/core/src/__tests__/postgres/embedded-lifecycle.test.ts index f28a156583..ec19d438e8 100644 --- a/packages/core/src/__tests__/postgres/embedded-lifecycle.test.ts +++ b/packages/core/src/__tests__/postgres/embedded-lifecycle.test.ts @@ -808,7 +808,7 @@ describe("embedded-lifecycle: startup race (cross-process)", () => { async start() { writeFileSync( 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'); } @@ -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. writeFileSync( 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"); } @@ -891,7 +891,7 @@ describe("embedded-lifecycle: startup race only joins on a lock collision", () = async start() { writeFileSync( 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'); } @@ -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. writeFileSync( 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[] = []; @@ -1027,28 +1027,30 @@ describe("embedded-lifecycle: startup timeout (P1 #24)", () => { }); 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-")); try { const { writeFileSync } = require("node:fs"); // Standard PostgreSQL postmaster.pid format: // Line 1: PID // Line 2: data directory - // Line 3: unix socket directory - // Line 4: listen address - // Line 5: port number - // Line 6: shared memory key - // Line 7: postmaster start timestamp + // Line 3: postmaster start timestamp + // Line 4: port number + // Line 5: unix socket directory + // Line 6: listen address + // Line 7: shared memory key and id + // Line 8: status writeFileSync( join(dir, "postmaster.pid"), [ "12345", "/home/user/.fusion/embedded-postgres/default", + "1784361395", + "55432", "/tmp", "localhost", - "55432", - "5432101", - String(Date.now()), + "18446744071752735336 19857409", + "ready", ].join("\n") + "\n", ); @@ -1065,7 +1067,7 @@ describe("embedded-lifecycle: readPortFromPostmasterPid (P1 code-review fix)", ( const { writeFileSync } = require("node:fs"); writeFileSync( 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(); } 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", () => { - // Regression: the bug read lines[2] (socket dir) which is never the port. - // If the socket dir happened to contain digits, parseInt would produce - // a wrong port. This test ensures we skip past it. + it("does NOT read line 3 (index 2, start timestamp) as the port", () => { + // The start timestamp is numeric, so parsing the wrong adjacent line would + // produce a plausible-looking but unusable port. const dir = mkdtempSync(join(tmpdir(), "fusion-embedded-pid-")); try { const { writeFileSync } = require("node:fs"); writeFileSync( 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); expect(port).toBe(5433); diff --git a/packages/core/src/postgres/embedded-lifecycle.ts b/packages/core/src/postgres/embedded-lifecycle.ts index 0a08844ed8..fa259599d2 100644 --- a/packages/core/src/postgres/embedded-lifecycle.ts +++ b/packages/core/src/postgres/embedded-lifecycle.ts @@ -927,16 +927,18 @@ const runningInstances = new Map(); * Read the port from a postmaster.pid file. The standard PostgreSQL format is: * Line 1 (index 0): PID * Line 2 (index 1): Data directory path - * Line 3 (index 2): Unix socket directory - * Line 4 (index 3): Listen address (e.g. localhost or *) - * Line 5 (index 4): Port number - * Line 6 (index 5): Shared memory key - * Line 7 (index 6): Postmaster start timestamp + * Line 3 (index 2): Postmaster start timestamp + * Line 4 (index 3): Port number + * Line 5 (index 4): Unix socket directory + * Line 6 (index 5): Listen address (e.g. localhost or *) + * 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): - * Previously read line 3 (index 2, the socket dir) which is never a port - * number, so singleton detection via postmaster.pid ALWAYS failed. Fixed to - * read line 5 (index 4, the TCP port). + * The earlier correction still encoded the wrong field order and read line 5 + * (the socket directory). Read PostgreSQL's actual line 4 port field so a + * 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. */ @@ -944,8 +946,8 @@ export function readPortFromPostmasterPid(dataDir: string): number | null { try { const content = readFileSync(join(dataDir, "postmaster.pid"), "utf-8"); const lines = content.split("\n"); - // Line 5 (index 4) is the TCP port in standard PostgreSQL postmaster.pid - const portStr = lines[4]?.trim(); + // Line 4 (index 3) is the TCP port in standard PostgreSQL postmaster.pid. + const portStr = lines[3]?.trim(); if (portStr) { const port = parseInt(portStr, 10); if (!isNaN(port) && port > 0) return port;