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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user