test(FN-8146): speed up PG test harness with per-process schema template

Applying the full schema baseline on every fresh test database cost ~530ms
per file and dominated PG gate wall-time under fork contention. Apply the
schema once per worker process into a pid-keyed template database, then
create each test DB via CREATE DATABASE ... TEMPLATE (fast server-side copy).

Dead-pid templates from crashed/prior runs are swept before creating a new
one; template copies are serialized since CREATE DATABASE ... TEMPLATE forbids
concurrent access to the source. Removes the throwaway probe2 timing test.

Fusion-Task-Id: FN-8146

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-16 22:22:21 -07:00
parent 5f70447a62
commit c64c1014e2

View File

@@ -267,6 +267,157 @@ function adminExecAsync(statement: string, timeoutMs = 15_000): Promise<void> {
});
}
/**
* FNXC:PgTestTemplateDb 2026-07-16-17:40:
* Slow-test fix: applying the full Fusion schema baseline on every fresh test
* database cost ~530ms per test file. With ~23 pg-gate files fanned across
* forks against one PostgreSQL server, those DDL applies serialize and dominate
* gate wall-time (mission-store.pg alone measured 6s isolated / 28s under load).
*
* Instead we apply the schema ONCE per worker process into a reusable template
* database, then create each test database with `CREATE DATABASE ... TEMPLATE`
* — a fast server-side file copy that skips re-running the DDL. The template is
* keyed by pid so concurrent forks never collide, and dead-pid templates left
* by crashed/prior runs are swept before creating a new one.
*/
const SCHEMA_TEMPLATE_PREFIX = "fusion_schema_template";
/** The per-process template database name (pid-keyed so forks never collide). */
function templateDbName(): string {
return `${SCHEMA_TEMPLATE_PREFIX}_${process.pid}`;
}
/** Extract the pid embedded in a template DB name, or null if it doesn't match. */
function parseTemplatePid(dbName: string): number | null {
const match = new RegExp(`^${SCHEMA_TEMPLATE_PREFIX}_(\\d+)$`).exec(dbName);
if (!match) return null;
const pid = Number.parseInt(match[1], 10);
return Number.isFinite(pid) ? pid : null;
}
/**
* FNXC:PgTestTemplateDb 2026-07-16-17:40:
* A template left behind by a crashed/finished process is only reclaimable if
* its owning pid is gone. `process.kill(pid, 0)` probes liveness without
* signalling: ESRCH => dead (sweepable); EPERM => alive under another user
* (keep). Treat any non-ESRCH error as alive so we never drop a live template.
*/
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (err) {
return (err as NodeJS.ErrnoException).code !== "ESRCH";
}
}
/**
* Open a short-lived admin connection to the maintenance ("postgres") database
* on the same server, run `fn`, and always close. Used for template lifecycle
* (listing/sweeping/creating template DBs) where we need query results back —
* unlike `adminExecAsync`, which fires psql and returns no rows.
*/
async function withMaintenanceSql<T>(
fn: (client: ReturnType<typeof postgres>) => Promise<T>,
): Promise<T> {
const maintUrl = new URL(PG_TEST_URL_BASE);
maintUrl.pathname = "/postgres";
const client = postgres(maintUrl.toString(), {
max: 1,
prepare: false,
onnotice: () => {},
});
try {
return await fn(client);
} finally {
await client.end({ timeout: 5 }).catch(() => {});
}
}
/**
* FNXC:PgTestTemplateDb 2026-07-16-17:40:
* CREATE DATABASE ... TEMPLATE connects to the source template and errors if
* any other session is attached ("source database is being accessed by other
* users"). Vitest forks get distinct pid-keyed templates, but a single process
* can still issue concurrent `createTaskStoreForTest()` calls, so serialize the
* template-copy step through a chained mutex.
*/
let createFromTemplateChain: Promise<unknown> = Promise.resolve();
function serializeTemplateCopy<T>(fn: () => Promise<T>): Promise<T> {
const run = createFromTemplateChain.then(fn, fn);
createFromTemplateChain = run.then(
() => {},
() => {},
);
return run;
}
/**
* FNXC:PgTestTemplateDb 2026-07-16-17:40:
* Ensure this process's schema-template database exists with the full Fusion
* schema baseline applied, and return its name. Memoized per process so the
* ~530ms baseline apply happens exactly once regardless of how many test files
* or `createTaskStoreForTest()` calls run in the worker.
*
* On entry it sweeps dead-pid templates (crashed prior runs), then recreates
* this pid's template from scratch so a half-built template from an earlier
* aborted run in the same pid can never leak a corrupt schema into copies.
* On failure the memo is cleared so a later call can retry.
*/
let schemaTemplateReady: Promise<string> | null = null;
function ensureSchemaTemplate(): Promise<string> {
if (schemaTemplateReady) return schemaTemplateReady;
const ready = (async (): Promise<string> => {
const templateName = templateDbName();
await withMaintenanceSql(async (client) => {
// Sweep templates orphaned by crashed/finished processes.
const rows = await client<{ datname: string }[]>`
SELECT datname FROM pg_database
WHERE datname LIKE ${SCHEMA_TEMPLATE_PREFIX + "\\_%"}
`;
for (const row of rows) {
if (row.datname === templateName) continue;
const pid = parseTemplatePid(row.datname);
if (pid !== null && isPidAlive(pid)) continue;
await client
.unsafe(`DROP DATABASE IF EXISTS "${row.datname}" WITH (FORCE)`)
.catch(() => {});
}
// Recreate this pid's template fresh.
await client
.unsafe(`DROP DATABASE IF EXISTS "${templateName}" WITH (FORCE)`)
.catch(() => {});
await client.unsafe(`CREATE DATABASE "${templateName}"`);
});
// Apply the schema baseline once into the template, then close so the
// template has no live connections when copies are taken from it.
const templateUrl = `${PG_TEST_URL_BASE}/${templateName}`;
const schemaBackend: ResolvedBackend = {
mode: "external",
runtimeUrl: templateUrl,
migrationUrl: templateUrl,
migrationUrlOverridden: false,
};
const schemaConnections = await createConnectionSetFromUrl(schemaBackend, {
poolMax: 1,
connectTimeoutSeconds: 5,
});
try {
await applySchemaBaseline(schemaConnections.migration);
} finally {
await schemaConnections.close();
}
return templateName;
})();
ready.catch(() => {
// Allow a later call to rebuild the template after a transient failure.
if (schemaTemplateReady === ready) schemaTemplateReady = null;
});
schemaTemplateReady = ready;
return ready;
}
/**
* FNXC:TestMigrationTail 2026-06-24-16:00:
* Create a fresh, isolated PostgreSQL database with the Fusion schema applied,
@@ -287,29 +438,32 @@ export async function createTaskStoreForTest(options?: {
const prefix = options?.prefix ?? "fusion_test";
const dbName = uniqueDbName(prefix);
try {
await adminExecAsync(`DROP DATABASE IF EXISTS "${dbName}"`);
} catch {
// may not exist — safe to ignore
}
await adminExecAsync(`CREATE DATABASE "${dbName}"`);
// FNXC:PgTestTemplateDb 2026-07-16-17:40:
// Create the test database as a copy of the pre-baked schema template
// (fast server-side file copy) instead of re-running the schema baseline per
// test. The template apply is memoized once per worker process; the copy is
// serialized because CREATE DATABASE ... TEMPLATE forbids concurrent access
// to the source template.
const template = await ensureSchemaTemplate();
await serializeTemplateCopy(async () => {
try {
await adminExecAsync(`DROP DATABASE IF EXISTS "${dbName}"`);
} catch {
// may not exist — safe to ignore
}
await adminExecAsync(`CREATE DATABASE "${dbName}" TEMPLATE "${template}"`);
});
const testUrl = `${PG_TEST_URL_BASE}/${dbName}`;
// Apply schema baseline via a dedicated migration connection.
// The database already carries the full schema (copied from the template),
// so open the runtime connection pool and construct the AsyncDataLayer.
const schemaBackend: ResolvedBackend = {
mode: "external",
runtimeUrl: testUrl,
migrationUrl: testUrl,
migrationUrlOverridden: false,
};
const schemaConnections = await createConnectionSetFromUrl(schemaBackend, {
poolMax: 1,
connectTimeoutSeconds: 5,
});
await applySchemaBaseline(schemaConnections.migration);
await schemaConnections.close();
// Open the runtime connection pool and construct the AsyncDataLayer.
const connections = await createConnectionSetFromUrl(schemaBackend, {
poolMax,
connectTimeoutSeconds: 5,