fix(test): wait for a PostgreSQL fixture slot while the queue moves (BUG-1254)
The slot wait was a fixed 5 minutes from the moment a file started queueing. With 46 fixture files sharing 2 slots the queue outlasted that, so whichever file came last failed the gate at random (runs 1742 and 1746). The deadline now restarts whenever a slot changes hands; only 10 minutes with no slot changing hands (a hung holder) fails. The 2-slot cap from BUG-281 stays. Full suite 4967 / fail 24, identical to 171a3f25; tsc 0; lint 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fe9d03a9b7
commit
52d14e8fef
@@ -79,7 +79,15 @@ function releasePort(reservation: PortReservation): void {
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_CONCURRENT_POSTGRES_FIXTURES = 2;
|
||||
const FIXTURE_SLOT_WAIT_MS = 5 * 60 * 1000;
|
||||
/**
|
||||
* How long the slot queue may stand still before a waiter gives up (BUG-1254).
|
||||
* It used to be a fixed 5-minute wait from the moment a file started queueing;
|
||||
* with 46 fixture files sharing 2 slots the queue itself outlasted that, and
|
||||
* whichever file happened to be last failed the gate at random. The window now
|
||||
* restarts whenever a slot changes hands, so a moving queue is waited out and
|
||||
* only a queue where no slot changes for this long (a hung holder) fails.
|
||||
*/
|
||||
export const FIXTURE_SLOT_STALL_MS = 10 * 60 * 1000;
|
||||
|
||||
type FixtureSlot = { id: number; release(): void };
|
||||
|
||||
@@ -135,9 +143,37 @@ function tryClaimSlot(id: number): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Who holds each slot right now: `id:pid` per held slot, empty when free. */
|
||||
function slotOwnersSnapshot(limit: number): string {
|
||||
const owners: string[] = [];
|
||||
for (let id = 0; id < limit; id += 1) {
|
||||
try {
|
||||
owners.push(`${id}:${readFileSync(join(fixtureSlotDirectory(id), "pid"), "utf8")}`);
|
||||
} catch {
|
||||
owners.push(`${id}:`);
|
||||
}
|
||||
}
|
||||
return owners.join("|");
|
||||
}
|
||||
|
||||
/**
|
||||
* The waiter's deadline after one poll: pushed out by a full stall window when
|
||||
* the slot owners changed since the last poll, unchanged otherwise.
|
||||
*/
|
||||
export function nextFixtureSlotDeadline(
|
||||
previousOwners: string,
|
||||
currentOwners: string,
|
||||
deadline: number,
|
||||
now: number,
|
||||
stallMs: number = FIXTURE_SLOT_STALL_MS,
|
||||
): number {
|
||||
return currentOwners === previousOwners ? deadline : now + stallMs;
|
||||
}
|
||||
|
||||
function acquireFixtureSlot(): FixtureSlot {
|
||||
const limit = maxConcurrentPostgresFixtures();
|
||||
const deadline = Date.now() + FIXTURE_SLOT_WAIT_MS;
|
||||
let owners = slotOwnersSnapshot(limit);
|
||||
let deadline = Date.now() + FIXTURE_SLOT_STALL_MS;
|
||||
while (Date.now() < deadline) {
|
||||
for (let id = 0; id < limit; id += 1) {
|
||||
if (tryClaimSlot(id)) {
|
||||
@@ -150,9 +186,12 @@ function acquireFixtureSlot(): FixtureSlot {
|
||||
}
|
||||
}
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
||||
const current = slotOwnersSnapshot(limit);
|
||||
deadline = nextFixtureSlotDeadline(owners, current, deadline, Date.now());
|
||||
owners = current;
|
||||
}
|
||||
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.`,
|
||||
`timed out waiting for a PostgreSQL fixture slot (${limit} concurrent compose networks): no slot changed hands for ${FIXTURE_SLOT_STALL_MS / 60000} minutes, so a holder is likely hung. Docker address pools cannot host one network per parallel test file.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { FIXTURE_SLOT_STALL_MS, nextFixtureSlotDeadline } from "./helpers/postgres-fixture.ts";
|
||||
|
||||
const fixtureSource = readFileSync(
|
||||
fileURLToPath(new URL("./helpers/postgres-fixture.ts", import.meta.url)),
|
||||
"utf8",
|
||||
@@ -42,3 +44,25 @@ test("large PostgreSQL scripts use stdin without changing shared -c helpers", ()
|
||||
assert.match(fixtureSource, /psql\(sql\)[\s\S]*"-Atc",\s*sql/);
|
||||
assert.match(fixtureSource, /psqlAs\(role, password, sql\)[\s\S]*"-Atc",\s*sql/);
|
||||
});
|
||||
|
||||
test("a waiter keeps waiting while the slot queue moves and gives up only when it stalls (BUG-1254)", () => {
|
||||
// Fixed 5-minute waits failed the gate at random once 46 fixture files
|
||||
// queued for 2 slots: the queue itself outlasted the wait.
|
||||
assert.doesNotMatch(fixtureSource, /FIXTURE_SLOT_WAIT_MS/);
|
||||
assert.equal(FIXTURE_SLOT_STALL_MS, 10 * 60 * 1000);
|
||||
const start = 1_000_000;
|
||||
const deadline = start + FIXTURE_SLOT_STALL_MS;
|
||||
// Nobody released or claimed a slot: the deadline does not move.
|
||||
assert.equal(nextFixtureSlotDeadline("0:11|1:12", "0:11|1:12", deadline, start + 60_000), deadline);
|
||||
// A slot changed hands: a full stall window from now.
|
||||
const later = start + 9 * 60_000;
|
||||
assert.equal(nextFixtureSlotDeadline("0:11|1:12", "0:13|1:12", deadline, later), later + FIXTURE_SLOT_STALL_MS);
|
||||
// A slot was released and is momentarily free: that is progress too.
|
||||
assert.equal(nextFixtureSlotDeadline("0:11|1:12", "0:|1:12", deadline, later), later + FIXTURE_SLOT_STALL_MS);
|
||||
// The acquire loop recomputes the deadline from the owners after every poll.
|
||||
assert.match(
|
||||
fixtureSource,
|
||||
/function acquireFixtureSlot\(\)[\s\S]*slotOwnersSnapshot\(limit\)[\s\S]*deadline = nextFixtureSlotDeadline\(owners, current, deadline, Date\.now\(\)\)/,
|
||||
);
|
||||
assert.match(fixtureSource, /no slot changed hands for/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user