37e62094d7
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>
286 lines
7.8 KiB
TypeScript
286 lines
7.8 KiB
TypeScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import {
|
|
chmodSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
export type PostgresFixture = {
|
|
projectName: string;
|
|
databaseEnvFile: string;
|
|
hostPort: number;
|
|
connectionUrl(role: string, password: string): string;
|
|
psql(sql: string): string;
|
|
psqlAs(role: string, password: string, sql: string): string;
|
|
stop(): void;
|
|
};
|
|
|
|
const databaseEnvironment = `POSTGRES_DB=jyotisha
|
|
POSTGRES_USER=postgres
|
|
POSTGRES_PASSWORD=postgres-test-password
|
|
SCHEMA_OWNER_PASSWORD=schema-owner-test-password
|
|
IDENTITY_RUNTIME_PASSWORD=identity-runtime-test-password
|
|
APP_RUNTIME_PASSWORD=app-runtime-test-password
|
|
SERVICE_RUNTIME_PASSWORD=service-runtime-test-password
|
|
ADMIN_RUNTIME_PASSWORD=admin-runtime-test-password
|
|
MIGRATION_RUNNER_PASSWORD=migration-runner-test-password
|
|
BACKUP_READER_PASSWORD=backup-reader-test-password
|
|
STAGING_BACKUP_ENCRYPTION_KEY=staging-backup-test-password
|
|
SCHEMA_DATABASE_URL=postgresql://schema_owner:schema-owner-test-password@postgres:5432/jyotisha
|
|
`;
|
|
|
|
function isPortAvailable(port: number): boolean {
|
|
return (
|
|
spawnSync(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
`const server = require("node:net").createServer();
|
|
server.once("error", () => process.exit(1));
|
|
server.listen({ host: "127.0.0.1", port: Number(process.argv[1]) }, () =>
|
|
server.close(() => process.exit(0)),
|
|
);`,
|
|
String(port),
|
|
],
|
|
{ stdio: "ignore" },
|
|
).status === 0
|
|
);
|
|
}
|
|
|
|
type PortReservation = { port: number; directory: string };
|
|
|
|
function reserveAvailablePort(): PortReservation {
|
|
for (let port = 55432; port <= 55531; port += 1) {
|
|
const directory = join(tmpdir(), `jyotisha-postgres-port-${port}.lock`);
|
|
try {
|
|
mkdirSync(directory);
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
|
|
throw error;
|
|
}
|
|
|
|
if (isPortAvailable(port)) {
|
|
return { port, directory };
|
|
}
|
|
rmSync(directory, { force: true, recursive: true });
|
|
}
|
|
|
|
throw new Error("no available PostgreSQL test port in 55432..55531");
|
|
}
|
|
|
|
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()}`;
|
|
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;
|
|
const composeArguments = [
|
|
"compose",
|
|
"--project-name",
|
|
projectName,
|
|
"--env-file",
|
|
databaseEnvFile,
|
|
"-f",
|
|
"../deploy/docker-compose.postgres.yml",
|
|
"-f",
|
|
"../deploy/docker-compose.postgres-ci.yml",
|
|
];
|
|
const environment = {
|
|
...process.env,
|
|
DATABASE_ENV_FILE: databaseEnvFile,
|
|
POSTGRES_HOST_PORT: String(hostPort),
|
|
};
|
|
|
|
try {
|
|
writeFileSync(databaseEnvFile, databaseEnvironment, { mode: 0o600 });
|
|
chmodSync(databaseEnvFile, 0o600);
|
|
execFileSync(
|
|
"docker",
|
|
[...composeArguments, "up", "-d", "--wait", "postgres"],
|
|
{ env: environment, stdio: "inherit" },
|
|
);
|
|
} catch (error) {
|
|
try {
|
|
try {
|
|
// The teardown below deletes the only account of why the server refused to start.
|
|
execFileSync(
|
|
"docker",
|
|
[...composeArguments, "logs", "--no-color", "--tail", "80", "postgres"],
|
|
{ env: environment, stdio: "inherit" },
|
|
);
|
|
} catch {
|
|
// Preserve the original startup failure.
|
|
}
|
|
try {
|
|
execFileSync(
|
|
"docker",
|
|
[...composeArguments, "down", "-v", "--remove-orphans"],
|
|
{ env: environment, stdio: "inherit" },
|
|
);
|
|
} catch {
|
|
// Preserve the original startup failure.
|
|
}
|
|
} finally {
|
|
releasePort(portReservation);
|
|
rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
slot.release();
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
projectName,
|
|
databaseEnvFile,
|
|
hostPort,
|
|
connectionUrl(role, password) {
|
|
return `postgresql://${role}:${password}@127.0.0.1:${hostPort}/jyotisha`;
|
|
},
|
|
psql(sql) {
|
|
return execFileSync(
|
|
"docker",
|
|
[...composeArguments, "exec", "-T", "postgres", "psql", "-U", "postgres", "-d", "jyotisha", "-Atc", sql],
|
|
{ encoding: "utf8", env: environment },
|
|
)
|
|
.trim()
|
|
.replace(/(^|:)false(?=:|$)/gm, "$1f");
|
|
},
|
|
psqlAs(role, password, sql) {
|
|
return execFileSync(
|
|
"docker",
|
|
[
|
|
...composeArguments,
|
|
"exec",
|
|
"-T",
|
|
"-e",
|
|
`PGPASSWORD=${password}`,
|
|
"postgres",
|
|
"psql",
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-U",
|
|
role,
|
|
"-d",
|
|
"jyotisha",
|
|
"-Atc",
|
|
sql,
|
|
],
|
|
{ encoding: "utf8", env: environment },
|
|
).trim();
|
|
},
|
|
stop() {
|
|
try {
|
|
execFileSync(
|
|
"docker",
|
|
[...composeArguments, "down", "-v", "--remove-orphans"],
|
|
{ env: environment, stdio: "inherit" },
|
|
);
|
|
} finally {
|
|
releasePort(portReservation);
|
|
rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
slot.release();
|
|
}
|
|
},
|
|
};
|
|
}
|