Improve embedded Postgres bootstrap errors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
5913706329
commit
76a692c260
6 changed files with 180 additions and 38 deletions
|
|
@ -41,6 +41,8 @@ import {
|
||||||
projects,
|
projects,
|
||||||
runDatabaseBackup,
|
runDatabaseBackup,
|
||||||
runDatabaseRestore,
|
runDatabaseRestore,
|
||||||
|
createEmbeddedPostgresLogBuffer,
|
||||||
|
formatEmbeddedPostgresError,
|
||||||
} from "@paperclipai/db";
|
} from "@paperclipai/db";
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { ensureAgentJwtSecret, loadPaperclipEnvFile, mergePaperclipEnvEntries, readPaperclipEnvEntries, resolvePaperclipEnvFile } from "../config/env.js";
|
import { ensureAgentJwtSecret, loadPaperclipEnvFile, mergePaperclipEnvEntries, readPaperclipEnvEntries, resolvePaperclipEnvFile } from "../config/env.js";
|
||||||
|
|
@ -806,6 +808,7 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = await findAvailablePort(preferredPort);
|
const port = await findAvailablePort(preferredPort);
|
||||||
|
const logBuffer = createEmbeddedPostgresLogBuffer();
|
||||||
const instance = new EmbeddedPostgres({
|
const instance = new EmbeddedPostgres({
|
||||||
databaseDir: dataDir,
|
databaseDir: dataDir,
|
||||||
user: "paperclip",
|
user: "paperclip",
|
||||||
|
|
@ -813,17 +816,31 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P
|
||||||
port,
|
port,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
onLog: () => {},
|
onLog: logBuffer.append,
|
||||||
onError: () => {},
|
onError: logBuffer.append,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existsSync(path.resolve(dataDir, "PG_VERSION"))) {
|
if (!existsSync(path.resolve(dataDir, "PG_VERSION"))) {
|
||||||
await instance.initialise();
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
} catch (error) {
|
||||||
|
throw formatEmbeddedPostgresError(error, {
|
||||||
|
fallbackMessage: `Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${port}`,
|
||||||
|
recentLogs: logBuffer.getRecentLogs(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (existsSync(postmasterPidFile)) {
|
if (existsSync(postmasterPidFile)) {
|
||||||
rmSync(postmasterPidFile, { force: true });
|
rmSync(postmasterPidFile, { force: true });
|
||||||
}
|
}
|
||||||
await instance.start();
|
try {
|
||||||
|
await instance.start();
|
||||||
|
} catch (error) {
|
||||||
|
throw formatEmbeddedPostgresError(error, {
|
||||||
|
fallbackMessage: `Failed to start embedded PostgreSQL on port ${port}`,
|
||||||
|
recentLogs: logBuffer.getRecentLogs(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port,
|
port,
|
||||||
|
|
|
||||||
28
packages/db/src/embedded-postgres-error.test.ts
Normal file
28
packages/db/src/embedded-postgres-error.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createEmbeddedPostgresLogBuffer, formatEmbeddedPostgresError } from "./embedded-postgres-error.js";
|
||||||
|
|
||||||
|
describe("formatEmbeddedPostgresError", () => {
|
||||||
|
it("adds a shared-memory hint when initdb logs expose the real cause", () => {
|
||||||
|
const error = formatEmbeddedPostgresError("Postgres init script exited with code 1.", {
|
||||||
|
fallbackMessage: "Failed to initialize embedded PostgreSQL cluster",
|
||||||
|
recentLogs: [
|
||||||
|
"running bootstrap script ...",
|
||||||
|
"FATAL: could not create shared memory segment: Cannot allocate memory",
|
||||||
|
"DETAIL: Failed system call was shmget(key=123, size=56, 03600).",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.message).toContain("could not allocate shared memory");
|
||||||
|
expect(error.message).toContain("kern.sysv.shm");
|
||||||
|
expect(error.message).toContain("could not create shared memory segment");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps only recent non-empty log lines in the collector", () => {
|
||||||
|
const buffer = createEmbeddedPostgresLogBuffer(2);
|
||||||
|
buffer.append("line one\n\n");
|
||||||
|
buffer.append("line two");
|
||||||
|
buffer.append("line three");
|
||||||
|
|
||||||
|
expect(buffer.getRecentLogs()).toEqual(["line two", "line three"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
89
packages/db/src/embedded-postgres-error.ts
Normal file
89
packages/db/src/embedded-postgres-error.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
const DEFAULT_RECENT_LOG_LIMIT = 40;
|
||||||
|
const RECENT_LOG_SUMMARY_LINES = 8;
|
||||||
|
|
||||||
|
function toError(error: unknown, fallbackMessage: string): Error {
|
||||||
|
if (error instanceof Error) return error;
|
||||||
|
if (error === undefined) return new Error(fallbackMessage);
|
||||||
|
if (typeof error === "string") return new Error(`${fallbackMessage}: ${error}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Error(`${fallbackMessage}: ${JSON.stringify(error)}`);
|
||||||
|
} catch {
|
||||||
|
return new Error(`${fallbackMessage}: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeRecentLogs(recentLogs: string[]): string | null {
|
||||||
|
if (recentLogs.length === 0) return null;
|
||||||
|
return recentLogs
|
||||||
|
.slice(-RECENT_LOG_SUMMARY_LINES)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectEmbeddedPostgresHint(recentLogs: string[]): string | null {
|
||||||
|
const haystack = recentLogs.join("\n").toLowerCase();
|
||||||
|
if (!haystack.includes("could not create shared memory segment")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Embedded PostgreSQL bootstrap could not allocate shared memory. " +
|
||||||
|
"On macOS, this usually means the host's kern.sysv.shm* limits are too low for another local PostgreSQL cluster. " +
|
||||||
|
"Stop other local PostgreSQL servers or raise the shared-memory sysctls, then retry."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEmbeddedPostgresLogBuffer(limit = DEFAULT_RECENT_LOG_LIMIT): {
|
||||||
|
append(message: unknown): void;
|
||||||
|
getRecentLogs(): string[];
|
||||||
|
} {
|
||||||
|
const recentLogs: string[] = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
append(message: unknown) {
|
||||||
|
const text =
|
||||||
|
typeof message === "string"
|
||||||
|
? message
|
||||||
|
: message instanceof Error
|
||||||
|
? message.message
|
||||||
|
: String(message ?? "");
|
||||||
|
|
||||||
|
for (const rawLine of text.split(/\r?\n/)) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) continue;
|
||||||
|
recentLogs.push(line);
|
||||||
|
if (recentLogs.length > limit) {
|
||||||
|
recentLogs.splice(0, recentLogs.length - limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getRecentLogs() {
|
||||||
|
return [...recentLogs];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatEmbeddedPostgresError(
|
||||||
|
error: unknown,
|
||||||
|
input: {
|
||||||
|
fallbackMessage: string;
|
||||||
|
recentLogs?: string[];
|
||||||
|
},
|
||||||
|
): Error {
|
||||||
|
const baseError = toError(error, input.fallbackMessage);
|
||||||
|
const recentLogs = input.recentLogs ?? [];
|
||||||
|
const parts = [baseError.message];
|
||||||
|
const hint = detectEmbeddedPostgresHint(recentLogs);
|
||||||
|
const recentSummary = summarizeRecentLogs(recentLogs);
|
||||||
|
|
||||||
|
if (hint) {
|
||||||
|
parts.push(hint);
|
||||||
|
}
|
||||||
|
if (recentSummary) {
|
||||||
|
parts.push(`Recent embedded Postgres logs: ${recentSummary}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error(parts.join(" "));
|
||||||
|
}
|
||||||
|
|
@ -25,4 +25,8 @@ export {
|
||||||
type RunDatabaseBackupResult,
|
type RunDatabaseBackupResult,
|
||||||
type RunDatabaseRestoreOptions,
|
type RunDatabaseRestoreOptions,
|
||||||
} from "./backup-lib.js";
|
} from "./backup-lib.js";
|
||||||
|
export {
|
||||||
|
createEmbeddedPostgresLogBuffer,
|
||||||
|
formatEmbeddedPostgresError,
|
||||||
|
} from "./embedded-postgres-error.js";
|
||||||
export * from "./schema/index.js";
|
export * from "./schema/index.js";
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||||
import { createServer } from "node:net";
|
import { createServer } from "node:net";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { ensurePostgresDatabase, getPostgresDataDirectory } from "./client.js";
|
import { ensurePostgresDatabase, getPostgresDataDirectory } from "./client.js";
|
||||||
|
import { createEmbeddedPostgresLogBuffer, formatEmbeddedPostgresError } from "./embedded-postgres-error.js";
|
||||||
import { resolveDatabaseTarget } from "./runtime-config.js";
|
import { resolveDatabaseTarget } from "./runtime-config.js";
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
type EmbeddedPostgresInstance = {
|
||||||
|
|
@ -27,18 +28,6 @@ export type MigrationConnection = {
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toError(error: unknown, fallbackMessage: string): Error {
|
|
||||||
if (error instanceof Error) return error;
|
|
||||||
if (error === undefined) return new Error(fallbackMessage);
|
|
||||||
if (typeof error === "string") return new Error(`${fallbackMessage}: ${error}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new Error(`${fallbackMessage}: ${JSON.stringify(error)}`);
|
|
||||||
} catch {
|
|
||||||
return new Error(`${fallbackMessage}: ${String(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRunningPostmasterPid(postmasterPidFile: string): number | null {
|
function readRunningPostmasterPid(postmasterPidFile: string): number | null {
|
||||||
if (!existsSync(postmasterPidFile)) return null;
|
if (!existsSync(postmasterPidFile)) return null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -109,6 +98,7 @@ async function ensureEmbeddedPostgresConnection(
|
||||||
const runningPid = readRunningPostmasterPid(postmasterPidFile);
|
const runningPid = readRunningPostmasterPid(postmasterPidFile);
|
||||||
const runningPort = readPidFilePort(postmasterPidFile);
|
const runningPort = readPidFilePort(postmasterPidFile);
|
||||||
const preferredAdminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${preferredPort}/postgres`;
|
const preferredAdminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${preferredPort}/postgres`;
|
||||||
|
const logBuffer = createEmbeddedPostgresLogBuffer();
|
||||||
|
|
||||||
if (!runningPid && existsSync(pgVersionFile)) {
|
if (!runningPid && existsSync(pgVersionFile)) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -151,18 +141,19 @@ async function ensureEmbeddedPostgresConnection(
|
||||||
port: selectedPort,
|
port: selectedPort,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
onLog: () => {},
|
onLog: logBuffer.append,
|
||||||
onError: () => {},
|
onError: logBuffer.append,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existsSync(path.resolve(dataDir, "PG_VERSION"))) {
|
if (!existsSync(path.resolve(dataDir, "PG_VERSION"))) {
|
||||||
try {
|
try {
|
||||||
await instance.initialise();
|
await instance.initialise();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw toError(
|
throw formatEmbeddedPostgresError(error, {
|
||||||
error,
|
fallbackMessage:
|
||||||
`Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${selectedPort}`,
|
`Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${selectedPort}`,
|
||||||
);
|
recentLogs: logBuffer.getRecentLogs(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (existsSync(postmasterPidFile)) {
|
if (existsSync(postmasterPidFile)) {
|
||||||
|
|
@ -171,7 +162,10 @@ async function ensureEmbeddedPostgresConnection(
|
||||||
try {
|
try {
|
||||||
await instance.start();
|
await instance.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw toError(error, `Failed to start embedded PostgreSQL on port ${selectedPort}`);
|
throw formatEmbeddedPostgresError(error, {
|
||||||
|
fallbackMessage: `Failed to start embedded PostgreSQL on port ${selectedPort}`,
|
||||||
|
recentLogs: logBuffer.getRecentLogs(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${selectedPort}/postgres`;
|
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${selectedPort}/postgres`;
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ import { and, eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
createDb,
|
createDb,
|
||||||
ensurePostgresDatabase,
|
ensurePostgresDatabase,
|
||||||
|
formatEmbeddedPostgresError,
|
||||||
getPostgresDataDirectory,
|
getPostgresDataDirectory,
|
||||||
inspectMigrations,
|
inspectMigrations,
|
||||||
applyPendingMigrations,
|
applyPendingMigrations,
|
||||||
|
createEmbeddedPostgresLogBuffer,
|
||||||
reconcilePendingMigrationHistory,
|
reconcilePendingMigrationHistory,
|
||||||
formatDatabaseBackupResult,
|
formatDatabaseBackupResult,
|
||||||
runDatabaseBackup,
|
runDatabaseBackup,
|
||||||
|
|
@ -272,29 +274,31 @@ export async function startServer(): Promise<StartedServer> {
|
||||||
const dataDir = resolve(config.embeddedPostgresDataDir);
|
const dataDir = resolve(config.embeddedPostgresDataDir);
|
||||||
const configuredPort = config.embeddedPostgresPort;
|
const configuredPort = config.embeddedPostgresPort;
|
||||||
let port = configuredPort;
|
let port = configuredPort;
|
||||||
const embeddedPostgresLogBuffer: string[] = [];
|
const logBuffer = createEmbeddedPostgresLogBuffer(120);
|
||||||
const EMBEDDED_POSTGRES_LOG_BUFFER_LIMIT = 120;
|
|
||||||
const verboseEmbeddedPostgresLogs = process.env.PAPERCLIP_EMBEDDED_POSTGRES_VERBOSE === "true";
|
const verboseEmbeddedPostgresLogs = process.env.PAPERCLIP_EMBEDDED_POSTGRES_VERBOSE === "true";
|
||||||
const appendEmbeddedPostgresLog = (message: unknown) => {
|
const appendEmbeddedPostgresLog = (message: unknown) => {
|
||||||
const text = typeof message === "string" ? message : message instanceof Error ? message.message : String(message ?? "");
|
logBuffer.append(message);
|
||||||
for (const lineRaw of text.split(/\r?\n/)) {
|
if (!verboseEmbeddedPostgresLogs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = typeof message === "string"
|
||||||
|
? message.split(/\r?\n/)
|
||||||
|
: message instanceof Error
|
||||||
|
? [message.message]
|
||||||
|
: [String(message ?? "")];
|
||||||
|
for (const lineRaw of lines) {
|
||||||
const line = lineRaw.trim();
|
const line = lineRaw.trim();
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
embeddedPostgresLogBuffer.push(line);
|
logger.info({ embeddedPostgresLog: line }, "embedded-postgres");
|
||||||
if (embeddedPostgresLogBuffer.length > EMBEDDED_POSTGRES_LOG_BUFFER_LIMIT) {
|
|
||||||
embeddedPostgresLogBuffer.splice(0, embeddedPostgresLogBuffer.length - EMBEDDED_POSTGRES_LOG_BUFFER_LIMIT);
|
|
||||||
}
|
|
||||||
if (verboseEmbeddedPostgresLogs) {
|
|
||||||
logger.info({ embeddedPostgresLog: line }, "embedded-postgres");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const logEmbeddedPostgresFailure = (phase: "initialise" | "start", err: unknown) => {
|
const logEmbeddedPostgresFailure = (phase: "initialise" | "start", err: unknown) => {
|
||||||
if (embeddedPostgresLogBuffer.length > 0) {
|
const recentLogs = logBuffer.getRecentLogs();
|
||||||
|
if (recentLogs.length > 0) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
phase,
|
phase,
|
||||||
recentLogs: embeddedPostgresLogBuffer,
|
recentLogs,
|
||||||
err,
|
err,
|
||||||
},
|
},
|
||||||
"Embedded PostgreSQL failed; showing buffered startup logs",
|
"Embedded PostgreSQL failed; showing buffered startup logs",
|
||||||
|
|
@ -371,7 +375,10 @@ export async function startServer(): Promise<StartedServer> {
|
||||||
await embeddedPostgres.initialise();
|
await embeddedPostgres.initialise();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logEmbeddedPostgresFailure("initialise", err);
|
logEmbeddedPostgresFailure("initialise", err);
|
||||||
throw err;
|
throw formatEmbeddedPostgresError(err, {
|
||||||
|
fallbackMessage: `Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${port}`,
|
||||||
|
recentLogs: logBuffer.getRecentLogs(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`);
|
logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`);
|
||||||
|
|
@ -385,7 +392,10 @@ export async function startServer(): Promise<StartedServer> {
|
||||||
await embeddedPostgres.start();
|
await embeddedPostgres.start();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logEmbeddedPostgresFailure("start", err);
|
logEmbeddedPostgresFailure("start", err);
|
||||||
throw err;
|
throw formatEmbeddedPostgresError(err, {
|
||||||
|
fallbackMessage: `Failed to start embedded PostgreSQL on port ${port}`,
|
||||||
|
recentLogs: logBuffer.getRecentLogs(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
embeddedPostgresStartedByThisProcess = true;
|
embeddedPostgresStartedByThisProcess = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue