FN-9136: Characterize PostgreSQL fork database reuse
Characterize safe per-fork PostgreSQL database reuse while documenting why the production experiment remains reverted. - add a bounded pool primitive with reset, verification, discard, and ownership fencing - cover recycling, poison handling, capacity, and reclaim namespace behavior - align PostgreSQL campaign documentation with the dead-fork leak rejection Files changed: .../test-failures/postgres-ddl-admission-bound.md | 4 +- .../suite-only-flakes-observed-register.md | 4 +- docs/testing.md | 4 +- .../core/src/__test-utils__/pg-fork-db-pool.ts | 184 +++++++++++++++++++++ .../__tests__/pg-fork-db-pool-ownership.test.ts | 36 ++++ .../core/src/__tests__/pg-fork-db-pool.test.ts | 60 +++++++ 6 files changed, 287 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-9136 Fusion-Task-Lineage: 9e14e0eb-d957-4207-977e-2d913e78750f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -31,7 +31,7 @@ The prospective K=4 queue arithmetic was 7.6s at the measured 1.88s p95, below t
|
||||
|
||||
FN-9130 then implemented bounded deferred draining (R=2, Q=8), per-file flush, and a dead-pid sweep. It structurally zeroed hook-inline drop watchdogs, but this is explicitly not acceptance evidence. The first 12-worker run leaked one database because root setup's `afterAll` ran before a shared harness registered its teardown; adding a shared-harness flush eliminated that leak. The next two green runs took 117.2s and 122.4s, already slower than the 108.1s baseline maximum; a fourth took 143.8s and timed out in `workflow-events-outbox.pg.test.ts` setup and `pg-test-harness-template-concurrency.pg.test.ts`. The wiring was reverted. This is a measured non-remedy, not a timeout/quarantine change.
|
||||
|
||||
Candidate C, per-fork database reuse with `TRUNCATE`, remains unexplored and is filed as FN-9136. Lowering workers, widening timeouts, drop retries, and core-test quarantine were not remedies and were not used.
|
||||
Candidate C, per-fork database reuse with `TRUNCATE`, was implemented behind `FUSION_PG_TEST_REUSE_POOL=1`, including a golden-template fingerprint/emptiness release gate and acquire re-check. Its cluster isolation regression passed, and the first repaired 12-worker candidate lane was green (177 files, 1,386 tests, 103.65s). The required seven interleaved 12-worker pairs then rejected it: controls were 105.85, 118.40, 121.45, 129.78, 131.99, 138.26 (red), and 135.22s; candidates were 108.30, 111.83, 126.46, 137.24, 143.22, 129.93, and 135.80s. More importantly, the post-campaign catalog query found pooled `fusion_pool_*` databases retained by dead fork PIDs: the experiment had no awaited fork-exit flush and its direct-import identity degraded to the shared `local` token. That is a zero-own-token-survivor failure regardless of wall time, so the harness wiring, flag, and cluster test were reverted. The deterministic pool/ownership primitive remains unwired as regression characterization only. Lowering workers, widening timeouts, drop retries, and core-test quarantine were not remedies and were not used.
|
||||
|
||||
## FN-9139 setup-boundary pre-admission result — rejected
|
||||
|
||||
@@ -54,4 +54,4 @@ The retained `pg-setup-participation.ts` signal is connectionless and explicit (
|
||||
|
||||
FN-9134 shipped `scripts/pg-ddl-lane-metric.mjs`, a report-only parser with a pre-registered median-wall-time rule: at least seven interleaved green control/candidate samples, zero leaks, candidate median below control p25, and candidate worst no slower than control median. The completed alternating 12-worker campaign recorded all seven pairs: control/candidate wall times were 177.02/139.13s, 139.34/156.62s, 133.92/129.22s, 140.76/143.86s, 135.72/173.14s, 137.81/157.50s, and 125.73/146.91s. The tool reported control median 137.81s and candidate median 146.91s, with verdict `no-improvement`.
|
||||
|
||||
The prototype's deterministic no-cluster tests proved the cap, immediate inline overflow, joins over already-issued work, and one failed name recorded without another executor call. The campaign nevertheless had red candidate runs (pairs 02–06) and every sample saw non-zero pre-existing `fusion_test_%` survivors (32 or 33); pair 04 increased the count from 32 to 33. The protocol treats any non-zero survivor count as an automatic rejection, independent of timing. Therefore the prototype, its tests, and all harness/lifecycle wiring were removed together. This is a measured **REVERTED** terminal state, not a zero-watchdog success claim. The symptom remains open; FN-9136's isolation-preserving per-fork reuse is the next untried avenue once a valid green control lane exists. Full JSONL/log paths, per-run watchdogs, green status, and leak counts are retained in task document `FN-9134/evidence`.
|
||||
The prototype's deterministic no-cluster tests proved the cap, immediate inline overflow, joins over already-issued work, and one failed name recorded without another executor call. The campaign nevertheless had red candidate runs (pairs 02–06) and every sample saw non-zero pre-existing `fusion_test_%` survivors (32 or 33); pair 04 increased the count from 32 to 33. The protocol treats any non-zero survivor count as an automatic rejection, independent of timing. Therefore the prototype, its tests, and all harness/lifecycle wiring were removed together. This is a measured **REVERTED** terminal state, not a zero-watchdog success claim. The symptom remains open; FN-9136 subsequently evaluated and rejected isolation-preserving per-fork reuse after its seven-pair campaign leaked pooled databases owned by dead fork PIDs. Full JSONL/log paths, per-run watchdogs, green status, and leak counts are retained in task document `FN-9134/evidence`.
|
||||
|
||||
@@ -185,7 +185,7 @@ FN-8928 evicted the file from the blocking gate under the AGENTS.md gate rule; d
|
||||
|
||||
The timeout occurred after all test assertions and is unrelated to FN-8979's canonical mission-blocker contract. This file retains substantial coverage, so this first observation is recorded rather than quarantined. A second sighting requires the normal file-level quarantine decision.
|
||||
|
||||
**Evidence gathering pending 2026-08-16 (FN-9125):** The shared-harness teardown is serial (store, layer, admin client, `DROP DATABASE WITH (FORCE)`, temporary directory), so a loaded close/drop block remains a plausible historical mechanism. Three targeted dot-reporter runs and loaded core reproduction did not produce a timeout or a measurable slow phase. This is not superseded or resolved: the required complete loaded failure capture is absent. FN-9127 owns CI/host-specific phase instrumentation, full failure capture, and the escalation decision; core PostgreSQL quarantine is policy-forbidden.
|
||||
**Evidence gathering pending 2026-08-17 (FN-9136):** The shared-harness teardown is serial (store, layer, admin client, `DROP DATABASE WITH (FORCE)`, temporary directory), so a loaded close/drop block remains a plausible historical mechanism. FN-9136's seven-pair per-fork `TRUNCATE` reuse campaign was rejected because its experimental fork cleanup leaked dead-owner databases; that rejection preserves isolation but does not resolve this original loaded timing symptom. FN-9127 retains CI/host-specific phase instrumentation and full failure capture ownership; core PostgreSQL quarantine is policy-forbidden.
|
||||
|
||||
| verification | result |
|
||||
|---|---|
|
||||
@@ -202,7 +202,7 @@ The timeout occurred after all test assertions and is unrelated to FN-8979's can
|
||||
| full core, 8 workers | unrelated settings attribution failure | 1,905ms globally | no / none |
|
||||
| full core, 12 workers | unrelated settings attribution + schema-applier timeout | `dropDatabase` 3,582ms globally | 30 / 30 |
|
||||
|
||||
The 12-worker snapshots show 21 backends and concurrent template `CREATE DATABASE`/`DROP DATABASE WITH (FORCE)` work, including `IPC/CheckpointDone` and `IPC/ProcSignalBarrier`; they do not implicate this mission-store suite. FN-9130 measured advisory admission as a non-remedy: uniform pooling regressed to 49 watchdogs / 5,068ms and drop-only wiring to 27 / 3,361ms against the 4–5 / 3,284ms baseline. A bounded deferred-drop reaper also failed the end-to-end criterion: watchdogs became zero by construction, but two green runs took 117.2s and 122.4s versus the 108.1s baseline maximum, and a later run timed out in unrelated loaded setup. The reaper was reverted and candidate C is tracked by FN-9136. FN-9134 supplied a pre-registered report-only lane metric and completed its required seven-pair alternating control/candidate campaign at 12 workers. The control/candidate medians were 137.81s/146.91s, candidate pairs 02–06 were red, and every sample observed 32 or 33 surviving `fusion_test_%` databases (pair 04 increased 32 to 33). The tool's `no-improvement` verdict and the automatic non-zero-leak rejection removed the prototype and all of its wiring/tests together. The full per-run JSONL/log evidence is retained in task document `FN-9134/evidence`; this remains unresolved rather than becoming a quarantine or timeout change. No teardown behavior was changed: there is no evidence-backed cause for this entry's historical 15s afterAll abort. This first-sighting record remains retained; a second sighting follows the normal escalation. Core PostgreSQL files cannot be quarantined inline because the gate-policy assertion requires `quarantinedCoreTests` to remain empty; that is an owner-escalated decision.
|
||||
The 12-worker snapshots show 21 backends and concurrent template `CREATE DATABASE`/`DROP DATABASE WITH (FORCE)` work, including `IPC/CheckpointDone` and `IPC/ProcSignalBarrier`; they do not implicate this mission-store suite. FN-9130 measured advisory admission as a non-remedy: uniform pooling regressed to 49 watchdogs / 5,068ms and drop-only wiring to 27 / 3,361ms against the 4–5 / 3,284ms baseline. A bounded deferred-drop reaper also failed the end-to-end criterion: watchdogs became zero by construction, but two green runs took 117.2s and 122.4s versus the 108.1s baseline maximum, and a later run timed out in unrelated loaded setup. The reaper was reverted. FN-9136 then rejected candidate C after its golden-template gate passed: the required seven-pair 12-worker campaign left pooled `fusion_pool_*` databases owned by dead fork PIDs because the experiment lacked an awaited fork-exit flush and direct imports degraded to the shared `local` identity. The isolation failure required removing all harness wiring regardless of wall time. FN-9134 supplied a pre-registered report-only lane metric and completed its required seven-pair alternating control/candidate campaign at 12 workers. The control/candidate medians were 137.81s/146.91s, candidate pairs 02–06 were red, and every sample observed 32 or 33 surviving `fusion_test_%` databases (pair 04 increased 32 to 33). The tool's `no-improvement` verdict and the automatic non-zero-leak rejection removed the prototype and all of its wiring/tests together. The full per-run JSONL/log evidence is retained in task document `FN-9134/evidence`; this remains unresolved rather than becoming a quarantine or timeout change. No teardown behavior was changed: there is no evidence-backed cause for this entry's historical 15s afterAll abort. This first-sighting record remains retained; a second sighting follows the normal escalation. Core PostgreSQL files cannot be quarantined inline because the gate-policy assertion requires `quarantinedCoreTests` to remain empty; that is an owner-escalated decision.
|
||||
|
||||
## 8. Planning Mode duplicate-response generation reconciliation
|
||||
|
||||
|
||||
@@ -833,8 +833,10 @@ This repository has no Playwright test runner: Vitest runs `playwright-core` as
|
||||
|
||||
### PostgreSQL DDL admission bound
|
||||
|
||||
FN-9136 evaluated and rejected per-fork `TRUNCATE` database reuse: its two-sided golden-template gate passed the cluster smoke, but the required seven-pair 12-worker campaign left dead-fork pooled databases because the experiment lacked an awaited fork-exit flush and degraded to a shared local identity. The experimental wiring was removed rather than weaken isolation; see `docs/solutions/test-failures/postgres-ddl-admission-bound.md`.
|
||||
|
||||
<!-- FNXC:PgTestDdlAdmission 2026-08-16-21:29: FN-9130 measured uniform and DROP-only advisory wiring as worse than the ungated 12-worker baseline. The primitive stays independently tested, but harness DDL remains direct until a structural candidate proves non-regression; do not mask this with timeout, retry, quarantine, or worker-cap changes. -->
|
||||
|
||||
FN-9130 evaluated a reusable advisory admission primitive: server session locks coordinate forks while a process-local ledger coordinates counted same-session locks within a fork; async context allows true nesting only. It uses one maintenance-database connection per fork, holds a slot for one statement only, and reports fail-open degradation. `FUSION_PG_TEST_DDL_MAX_CONCURRENCY` and `FUSION_PG_TEST_DDL_ADMISSION_ACQUIRE_TIMEOUT_MS` configure that primitive for its deterministic coverage.
|
||||
|
||||
The harness does **not** currently wire the primitive into `CREATE DATABASE` or `DROP DATABASE`. Uniform CREATE/DROP pooling regressed the 12-worker lane (49 watchdogs / 5,068ms versus a 4–5 / 3,284ms baseline); drop-only wiring also regressed (27 / 3,361ms). A bounded off-hook reaper (R=2/Q=8, flush, and dead-pid sweep) was then measured and reverted: its watchdog zero was structural, but green runs took 117.2s and 122.4s against a 108.1s baseline maximum and a later run timed out. Candidate C (per-fork reuse plus `TRUNCATE`) is FN-9136. The pg-gate's four-worker cap remains a separate lane-shape policy.
|
||||
The harness does **not** currently wire the primitive into `CREATE DATABASE` or `DROP DATABASE`. Uniform CREATE/DROP pooling regressed the 12-worker lane (49 watchdogs / 5,068ms versus a 4–5 / 3,284ms baseline); drop-only wiring also regressed (27 / 3,361ms). A bounded off-hook reaper (R=2/Q=8, flush, and dead-pid sweep) was then measured and reverted: its watchdog zero was structural, but green runs took 117.2s and 122.4s against a 108.1s baseline maximum and a later run timed out. FN-9136 subsequently evaluated and rejected candidate C (per-fork reuse plus `TRUNCATE`) because its seven-pair campaign leaked dead-fork pooled databases. The pg-gate's four-worker cap remains a separate lane-shape policy.
|
||||
|
||||
184
packages/core/src/__test-utils__/pg-fork-db-pool.ts
Normal file
184
packages/core/src/__test-utils__/pg-fork-db-pool.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
FNXC:PgTestDbReuse 2026-08-17-03:38:
|
||||
FN-9136 evaluates per-fork PostgreSQL database reuse without weakening DB-per-test
|
||||
isolation. A lease is poisoned until release has reset and verified it against
|
||||
its golden-template fingerprint; acquire verifies again because post-release
|
||||
mutation is otherwise invisible to a release-only gate.
|
||||
*/
|
||||
|
||||
export const PG_FORK_DB_POOL_PREFIX = "fusion_pool";
|
||||
|
||||
export interface PgForkDbLease {
|
||||
readonly dbName: string;
|
||||
readonly recycled: boolean;
|
||||
}
|
||||
|
||||
export interface PgForkDbPoolHooks {
|
||||
create(name: string): Promise<void>;
|
||||
reset(name: string): Promise<void>;
|
||||
verify(name: string): Promise<boolean>;
|
||||
drop(name: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PgForkDbPoolOptions {
|
||||
readonly enabled: boolean;
|
||||
readonly cap?: number;
|
||||
readonly forkPid?: number;
|
||||
readonly runToken?: string;
|
||||
readonly prefix?: string;
|
||||
/** Bound every gate operation so a stuck cleanup is discarded, never reused. */
|
||||
readonly gateTimeoutMs?: number;
|
||||
readonly hooks: PgForkDbPoolHooks;
|
||||
}
|
||||
|
||||
interface Entry { readonly name: string; state: "leased" | "releasing" | "free" | "dropped"; }
|
||||
|
||||
/** A small, non-blocking pool; overflow deliberately falls through to fresh DDL. */
|
||||
export class PgForkDbPool {
|
||||
private readonly cap: number;
|
||||
private readonly entries = new Map<string, Entry>();
|
||||
private readonly free: string[] = [];
|
||||
private readonly dropped = new Set<string>();
|
||||
private sequence = 0;
|
||||
private readonly gateTimeoutMs: number;
|
||||
|
||||
constructor(private readonly options: PgForkDbPoolOptions) {
|
||||
const requestedCap = options.cap ?? 2;
|
||||
const requestedGateTimeoutMs = options.gateTimeoutMs ?? 10_000;
|
||||
this.cap = Number.isFinite(requestedCap) ? Math.max(1, Math.min(8, Math.trunc(requestedCap))) : 2;
|
||||
this.gateTimeoutMs = Number.isFinite(requestedGateTimeoutMs)
|
||||
? Math.max(1, Math.min(15_000, Math.trunc(requestedGateTimeoutMs)))
|
||||
: 10_000;
|
||||
}
|
||||
|
||||
get enabled(): boolean { return this.options.enabled; }
|
||||
get size(): number { return this.entries.size; }
|
||||
|
||||
async acquire(): Promise<PgForkDbLease | null> {
|
||||
if (!this.enabled) return null;
|
||||
while (this.free.length > 0) {
|
||||
const name = this.free.pop()!;
|
||||
const entry = this.entries.get(name);
|
||||
if (!entry || entry.state !== "free") continue;
|
||||
// Acquire-side verification fences mutations after a prior passing release.
|
||||
if (await this.safeVerify(name)) {
|
||||
entry.state = "leased";
|
||||
return { dbName: name, recycled: true };
|
||||
}
|
||||
await this.discard(name);
|
||||
}
|
||||
if (this.entries.size >= this.cap) return null;
|
||||
const name = this.nextName();
|
||||
// Reserve before the first await so concurrent acquires cannot pass the cap.
|
||||
this.entries.set(name, { name, state: "leased" });
|
||||
try {
|
||||
await this.options.hooks.create(name);
|
||||
return { dbName: name, recycled: false };
|
||||
} catch {
|
||||
await this.discard(name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async release(name: string): Promise<void> {
|
||||
const entry = this.entries.get(name);
|
||||
if (!entry || entry.state !== "leased") return;
|
||||
// Claim the transition before awaiting so only one completed release can free a lease.
|
||||
entry.state = "releasing";
|
||||
// Free-list membership is granted only by this complete reset+verification path.
|
||||
try {
|
||||
await this.withDeadline(this.options.hooks.reset(name));
|
||||
if (!await this.safeVerify(name)) throw new Error("poisoned pooled database");
|
||||
if (this.entries.get(name) !== entry || this.dropped.has(name)) return;
|
||||
entry.state = "free";
|
||||
this.free.push(name);
|
||||
} catch {
|
||||
await this.discard(name);
|
||||
}
|
||||
}
|
||||
|
||||
async discard(name: string): Promise<void> {
|
||||
if (this.dropped.has(name)) return;
|
||||
this.dropped.add(name);
|
||||
const entry = this.entries.get(name);
|
||||
if (entry) entry.state = "dropped";
|
||||
this.entries.delete(name);
|
||||
const index = this.free.indexOf(name);
|
||||
if (index >= 0) this.free.splice(index, 1);
|
||||
await this.safeDrop(name);
|
||||
}
|
||||
|
||||
/** Fork-exit path: leased (abandoned) and free entries are both poison and dropped. */
|
||||
async flush(): Promise<void> {
|
||||
await Promise.all([...this.entries.keys()].map((name) => this.discard(name)));
|
||||
}
|
||||
|
||||
private async safeVerify(name: string): Promise<boolean> {
|
||||
try { return await this.withDeadline(this.options.hooks.verify(name)); } catch { return false; }
|
||||
}
|
||||
|
||||
private async safeDrop(name: string): Promise<void> {
|
||||
try { await this.withDeadline(this.options.hooks.drop(name)); } catch { /* teardown must not reject */ }
|
||||
}
|
||||
|
||||
private async withDeadline<T>(operation: Promise<T>): Promise<T> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
operation,
|
||||
new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error("pooled database gate timed out")), this.gateTimeoutMs);
|
||||
timer.unref?.();
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
private nextName(): string {
|
||||
this.sequence += 1;
|
||||
const token = (this.options.runToken ?? "local").replace(/[^a-z0-9]/gi, "").toLowerCase().slice(0, 24) || "local";
|
||||
const pid = this.options.forkPid ?? process.pid;
|
||||
const prefix = this.options.prefix ?? PG_FORK_DB_POOL_PREFIX;
|
||||
return `${prefix}_${pid}_${token}_${this.sequence}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePgForkDbPoolName(name: string): { pid: number; token: string } | null {
|
||||
const match = /^fusion_pool_(\d+)_([a-z0-9]+)_\d+_[a-z0-9]+$/i.exec(name);
|
||||
if (!match) return null;
|
||||
return { pid: Number(match[1]), token: match[2]! };
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:PgTestDbReuse 2026-08-17-04:05:
|
||||
* Pool cleanup is invocation-scoped. A prefix-only sweep can see a sibling
|
||||
* Vitest process and destroy its live database, so foreign tokens are only
|
||||
* candidates after their owner is proved dead.
|
||||
*/
|
||||
export function pgForkDbPoolIdentity(env: NodeJS.ProcessEnv = process.env): {
|
||||
pid: number;
|
||||
token: string;
|
||||
sharedInvocation: boolean;
|
||||
} {
|
||||
const owner = env.FUSION_PG_TEMPLATE_OWNER_PID?.trim();
|
||||
const root = env.FUSION_TEST_WORKER_ROOT?.trim();
|
||||
const pid = owner && /^\d+$/.test(owner) ? Number(owner) : process.pid;
|
||||
const token = (root ? root.split(/[\\/]/).filter(Boolean).at(-1) : env.FUSION_TEST_RUN_TOKEN)
|
||||
?.replace(/[^a-z0-9]/gi, "").toLowerCase().slice(-24) || "local";
|
||||
return { pid, token, sharedInvocation: Boolean(owner && root) };
|
||||
}
|
||||
|
||||
export function shouldReclaimPgForkDbPoolName(
|
||||
name: string,
|
||||
identity: { token: string; sharedInvocation: boolean },
|
||||
isAlive: (pid: number) => boolean,
|
||||
tier: "fork" | "invocation" | "orphan",
|
||||
): boolean {
|
||||
const parsed = parsePgForkDbPoolName(name);
|
||||
if (!parsed) return false;
|
||||
if (tier === "fork") return parsed.pid === process.pid;
|
||||
if (tier === "invocation") return parsed.token === identity.token && !isAlive(parsed.pid);
|
||||
return identity.sharedInvocation && parsed.token !== identity.token && !isAlive(parsed.pid);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
PG_FORK_DB_POOL_PREFIX,
|
||||
parsePgForkDbPoolName,
|
||||
pgForkDbPoolIdentity,
|
||||
shouldReclaimPgForkDbPoolName,
|
||||
} from "../__test-utils__/pg-fork-db-pool.js";
|
||||
|
||||
describe("PgForkDbPool ownership namespace", () => {
|
||||
it("is disjoint from the schema-template namespace in both directions", () => {
|
||||
const poolName = `${PG_FORK_DB_POOL_PREFIX}_123_run_1_nonce`;
|
||||
const templateName = "fusion_schema_template_123_goldenrun";
|
||||
const templateParser = /^fusion_schema_template_(\d+)(?:_[a-z0-9]+)?$/;
|
||||
expect(templateParser.test(poolName)).toBe(false);
|
||||
expect(parsePgForkDbPoolName(templateName)).toBeNull();
|
||||
});
|
||||
|
||||
it("extracts only a pool owner pid and token", () => {
|
||||
expect(parsePgForkDbPoolName("fusion_pool_123_runabc_1_nonce")).toEqual({ pid: 123, token: "runabc" });
|
||||
expect(parsePgForkDbPoolName("fusion_pool_not-a-pid_run_1_nonce")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps foreign and unknown owners outside this invocation's reclaim boundary", () => {
|
||||
const identity = pgForkDbPoolIdentity({
|
||||
FUSION_PG_TEMPLATE_OWNER_PID: "999",
|
||||
FUSION_TEST_WORKER_ROOT: "/tmp/fusion-test-workers-runabc",
|
||||
});
|
||||
const dead = () => false;
|
||||
const alive = () => true;
|
||||
expect(shouldReclaimPgForkDbPoolName("fusion_pool_123_fusiontestworkersrunabc_1_nonce", identity, dead, "invocation")).toBe(true);
|
||||
expect(shouldReclaimPgForkDbPoolName("fusion_pool_123_other_1_nonce", identity, dead, "invocation")).toBe(false);
|
||||
expect(shouldReclaimPgForkDbPoolName("fusion_pool_123_other_1_nonce", identity, dead, "orphan")).toBe(true);
|
||||
expect(shouldReclaimPgForkDbPoolName("fusion_pool_123_other_1_nonce", identity, alive, "orphan")).toBe(false);
|
||||
expect(shouldReclaimPgForkDbPoolName("fusion_pool_123_other_1_nonce", { ...identity, sharedInvocation: false }, dead, "orphan")).toBe(false);
|
||||
});
|
||||
});
|
||||
60
packages/core/src/__tests__/pg-fork-db-pool.test.ts
Normal file
60
packages/core/src/__tests__/pg-fork-db-pool.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { PgForkDbPool } from "../__test-utils__/pg-fork-db-pool.js";
|
||||
|
||||
function fixture(cap = 2) {
|
||||
const hooks = { create: vi.fn(async () => {}), reset: vi.fn(async () => {}), verify: vi.fn(async () => true), drop: vi.fn(async () => {}) };
|
||||
return { hooks, pool: new PgForkDbPool({ enabled: true, cap, forkPid: 42, runToken: "run", hooks }) };
|
||||
}
|
||||
|
||||
describe("PgForkDbPool", () => {
|
||||
it("only recycles after a passing reset and two-sided verification", async () => {
|
||||
const { hooks, pool } = fixture();
|
||||
const first = await pool.acquire();
|
||||
expect(first).not.toBeNull();
|
||||
await pool.release(first!.dbName);
|
||||
expect(hooks.reset).toHaveBeenCalledWith(first!.dbName);
|
||||
const second = await pool.acquire();
|
||||
expect(second).toEqual({ dbName: first!.dbName, recycled: true });
|
||||
expect(hooks.verify).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("discards reset failures and acquire-side drift rather than handing them out", async () => {
|
||||
const { hooks, pool } = fixture();
|
||||
const lease = (await pool.acquire())!;
|
||||
hooks.verify.mockResolvedValueOnce(false);
|
||||
await pool.release(lease.dbName);
|
||||
expect(hooks.drop).toHaveBeenCalledWith(lease.dbName);
|
||||
const replacement = (await pool.acquire())!;
|
||||
await pool.release(replacement.dbName);
|
||||
hooks.verify.mockResolvedValueOnce(false);
|
||||
const fresh = (await pool.acquire())!;
|
||||
expect(fresh.recycled).toBe(false);
|
||||
expect(hooks.drop).toHaveBeenCalledWith(replacement.dbName);
|
||||
});
|
||||
|
||||
it("bounds concurrent retained leases, falls through on cap, and drops abandoned leases once", async () => {
|
||||
const { hooks, pool } = fixture(1);
|
||||
const [lease, overflow] = await Promise.all([pool.acquire(), pool.acquire()]);
|
||||
expect(lease).not.toBeNull();
|
||||
expect(overflow).toBeNull();
|
||||
await Promise.all([pool.flush(), pool.discard(lease!.dbName), pool.flush()]);
|
||||
expect(hooks.drop).toHaveBeenCalledTimes(1);
|
||||
expect(hooks.drop).toHaveBeenCalledWith(lease!.dbName);
|
||||
});
|
||||
|
||||
it("uses bounded defaults for non-finite configuration", async () => {
|
||||
const { hooks } = fixture();
|
||||
const pool = new PgForkDbPool({ enabled: true, cap: Number.NaN, gateTimeoutMs: Number.NaN, hooks });
|
||||
const leases = await Promise.all([pool.acquire(), pool.acquire(), pool.acquire()]);
|
||||
expect(leases.filter(Boolean)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("is inert while disabled", async () => {
|
||||
const { hooks } = fixture();
|
||||
const pool = new PgForkDbPool({ enabled: false, hooks });
|
||||
expect(await pool.acquire()).toBeNull();
|
||||
await pool.flush();
|
||||
expect(hooks.create).not.toHaveBeenCalled();
|
||||
expect(hooks.drop).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user