fix(ci): cap postgres fixtures and reclaim leftover compose networks
Independent Staging Quality Gate / validate (push) Successful in 10m28s
Independent Staging Quality Gate / publish (push) Successful in 9m59s

xiaoxin's 20-core npm test opened one Docker network per database file and exhausted default address pools. Limit concurrent fixtures and remove unused jyotisha-postgres networks before the gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-18 19:30:13 +08:00
co-authored by Cursor
parent 927bdd7a21
commit 37e62094d7
5 changed files with 171 additions and 2 deletions
+90 -1
View File
@@ -3,6 +3,7 @@ import {
chmodSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
@@ -76,15 +77,101 @@ function releasePort(reservation: PortReservation): void {
rmSync(reservation.directory, { force: true, recursive: true });
}
const DEFAULT_MAX_CONCURRENT_POSTGRES_FIXTURES = 2;
const FIXTURE_SLOT_WAIT_MS = 5 * 60 * 1000;
type FixtureSlot = { id: number; release(): void };
function maxConcurrentPostgresFixtures(): number {
const raw = process.env.JYOTISHA_POSTGRES_MAX_FIXTURES;
if (raw === undefined || raw === "") {
return DEFAULT_MAX_CONCURRENT_POSTGRES_FIXTURES;
}
const parsed = Number(raw);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new Error("JYOTISHA_POSTGRES_MAX_FIXTURES must be a positive integer");
}
return parsed;
}
function fixtureSlotsRoot(): string {
return join(tmpdir(), "jyotisha-postgres-slots");
}
function fixtureSlotDirectory(id: number): string {
return join(fixtureSlotsRoot(), `slot-${id}`);
}
function pidIsAlive(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function tryClaimSlot(id: number): boolean {
const directory = fixtureSlotDirectory(id);
mkdirSync(fixtureSlotsRoot(), { recursive: true });
try {
mkdirSync(directory);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
try {
const pid = Number(readFileSync(join(directory, "pid"), "utf8"));
if (pidIsAlive(pid)) return false;
rmSync(directory, { force: true, recursive: true });
mkdirSync(directory);
} catch {
return false;
}
}
writeFileSync(join(directory, "pid"), String(process.pid));
return true;
}
function acquireFixtureSlot(): FixtureSlot {
const limit = maxConcurrentPostgresFixtures();
const deadline = Date.now() + FIXTURE_SLOT_WAIT_MS;
while (Date.now() < deadline) {
for (let id = 0; id < limit; id += 1) {
if (tryClaimSlot(id)) {
return {
id,
release() {
rmSync(fixtureSlotDirectory(id), { force: true, recursive: true });
},
};
}
}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
}
throw new Error(
`timed out waiting for a PostgreSQL fixture slot (${limit} concurrent compose networks). Docker address pools cannot host one network per parallel test file.`,
);
}
export function startPostgresFixture(): PostgresFixture {
const slot = acquireFixtureSlot();
const projectName = `jyotisha-postgres-${process.pid}-${Date.now()}`;
const temporaryDirectory = mkdtempSync(join(tmpdir(), "jyotisha-postgres-"));
let temporaryDirectory: string;
try {
temporaryDirectory = mkdtempSync(join(tmpdir(), "jyotisha-postgres-"));
} catch (error) {
slot.release();
throw error;
}
const databaseEnvFile = join(temporaryDirectory, "database.env");
let portReservation: PortReservation;
try {
portReservation = reserveAvailablePort();
} catch (error) {
rmSync(temporaryDirectory, { force: true, recursive: true });
slot.release();
throw error;
}
const hostPort = portReservation.port;
@@ -137,6 +224,7 @@ export function startPostgresFixture(): PostgresFixture {
} finally {
releasePort(portReservation);
rmSync(temporaryDirectory, { force: true, recursive: true });
slot.release();
}
throw error;
}
@@ -190,6 +278,7 @@ export function startPostgresFixture(): PostgresFixture {
} finally {
releasePort(portReservation);
rmSync(temporaryDirectory, { force: true, recursive: true });
slot.release();
}
},
};
@@ -0,0 +1,35 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { fileURLToPath } from "node:url";
const fixtureSource = readFileSync(
fileURLToPath(new URL("./helpers/postgres-fixture.ts", import.meta.url)),
"utf8",
);
test("postgres fixtures cap concurrent compose networks so Docker address pools cannot be exhausted", () => {
assert.match(fixtureSource, /JYOTISHA_POSTGRES_MAX_FIXTURES/);
assert.match(fixtureSource, /DEFAULT_MAX_CONCURRENT_POSTGRES_FIXTURES = 2/);
assert.match(fixtureSource, /function acquireFixtureSlot\(/);
assert.match(fixtureSource, /process\.kill\(\s*pid,\s*0\s*\)/);
assert.match(
fixtureSource,
/export function startPostgresFixture\(\)[\s\S]*acquireFixtureSlot\(\)/,
);
assert.match(
fixtureSource,
/timed out waiting for a PostgreSQL fixture slot/,
);
});
test("postgres fixture start failure and stop both release the compose-network slot", () => {
assert.match(
fixtureSource,
/catch \(error\) \{[\s\S]*releasePort\(portReservation\)[\s\S]*slot\.release\(\)/,
);
assert.match(
fixtureSource,
/stop\(\) \{[\s\S]*releasePort\(portReservation\)[\s\S]*slot\.release\(\)/,
);
});
@@ -407,13 +407,18 @@ test("both gate jobs reclaim runner disk before they need it, and only unheld re
assert.match(script, /docker container prune --force --filter until=6h/);
assert.match(script, /--filter 'name=\^jyotisha-postgres-' --filter dangling=true/);
assert.match(script, /docker network prune --force --filter until=6h/);
assert.match(script, /docker network ls[^\n]*--filter 'name=jyotisha-postgres-'/);
assert.match(script, /docker network rm "\$network"/);
assert.match(script, /all predefined address pools have been fully subnetted/);
assert.match(script, /docker image prune --force\n/);
assert.match(script, /docker builder prune --force --all/);
assert.match(script, /grep -v -F -e "api-\$KEEP_TAG_SHA" -e "web-\$KEEP_TAG_SHA"/);
assert.match(script, /PostgreSQL fixtures and image builds need at least \$\{MINIMUM_FREE_GIB\} GiB/);
// A gate that silently proceeds on a full disk fails 23 database tests instead
// of naming the disk, and pruning what a container still holds would delete a
// concurrent job's fixture out from under it.
// concurrent job's fixture out from under it. Network rm is the same rule:
// unused jyotisha-postgres leftovers occupy address pools, but an in-use
// network still has endpoints and docker network rm will refuse it.
assert.match(script, /if \[ "\$AFTER_GIB" -lt "\$MINIMUM_FREE_GIB" \]; then\n\s+echo[^\n]+\n\s+exit 1/);
assert.doesNotMatch(script, /docker system prune|--volumes|prune[^\n]*--all --force|docker (?:rm|volume rm|kill)[^\n]*--force/);
});