The standing and retirement tests wait on what they mean, not on a slept fraction of a bound: announcements are timed from the clock at the moment they are said, short runs are counted in attempts, an addition is proved by a hold it must not wait for (the second timing race of one kind in a day; issue 1126)

This commit is contained in:
2026-10-26 21:44:06 +01:00
parent 3a095a66be
commit 36ae0c691b
2 changed files with 37 additions and 22 deletions
+9 -5
View File
@@ -109,8 +109,10 @@ test("the fifth pass and not the fourth retires", async () => {
// Driven by the clock: five passes 10 ms apart take at least 40 ms after the first sees the set.
const w = await world(["a", "b", "c"]);
try {
await w.ask(["b", "c"]);
// The clock starts before the file is written: a pass may read it the moment it lands, before any line after the
// write runs, and a start taken after would then count fewer than four intervals.
const start = Date.now();
await w.ask(["b", "c"]);
await until(() => w.retired.length === 1);
assert.ok(Date.now() - start >= 4 * EVERY - 2, `retired after ${Date.now() - start} ms, before five passes`);
} finally {
@@ -155,10 +157,10 @@ test("five passes are not enough: the same set must also hold for stableForMs",
const HOLD = 400;
const w = await world(["a", "b", "c"], { stableForMs: HOLD });
try {
const start = Date.now(); // before the write, as in the fifth-pass test
await w.ask(["b", "c"]);
const start = Date.now();
await sleep(EVERY * 10); // well past five passes, well short of the hold
assert.deepEqual(w.retired, [], "retired after five passes, before the set held long enough");
if (Date.now() - start < HOLD) assert.deepEqual(w.retired, [], "retired after five passes, before the set held long enough");
await until(() => w.retired.length === 1, 3000);
assert.ok(Date.now() - start >= HOLD - 5, `retired after ${Date.now() - start} ms, before ${HOLD} ms`);
assert.equal((await w.p.retirement()).stable_for_seconds, 0); // 400 ms rounds to 0 s
@@ -300,11 +302,13 @@ test("delete refuses an active consumer and deletes only the named retired one",
});
test("an addition acts on the first pass", async () => {
const w = await world(["a"]);
// Not gated by the stability a retirement waits for: with a hold of a minute, only a pass that acts at once creates b
// in the time the test waits. A deadline of three intervals, as before, was a race with the scheduler under load.
const w = await world(["a"], { stableForMs: 60_000 });
try {
const before = w.created.length;
await w.ask(["a", "b"]);
await until(() => w.created.length > before, EVERY * 3);
await until(() => w.created.length > before);
assert.equal(w.created.at(-1), "b");
} finally {
w.done();
+28 -17
View File
@@ -27,7 +27,7 @@ async function until(cond: () => boolean, ms = 4000): Promise<void> {
}
}
interface Said { event: string; body: Record<string, unknown> }
interface Said { event: string; body: Record<string, unknown>; at: number }
interface World {
dir: string;
@@ -37,6 +37,8 @@ interface World {
/** When set, holds fails only for these consumers (holdsErr's error). */
holdsErrFor: Set<string> | null;
created: string[];
/** How many times create was asked, failed or not. */
attempts: number;
/** Every event but the first success since start, which has its own test. */
said: Said[];
all: Said[];
@@ -46,7 +48,7 @@ interface World {
async function world(opts: { classify?: boolean; announce?: false; stableForMs?: number } = {}): Promise<World> {
const dir = await mkdtemp(join(tmpdir(), "standing-"));
const receives = join(dir, "postgres-database.json");
const w = { dir, failing: null, holdsErr: null, holdsErrFor: null, created: [], said: [], all: [] } as unknown as World;
const w = { dir, failing: null, holdsErr: null, holdsErrFor: null, created: [], attempts: 0, said: [], all: [] } as unknown as World;
w.ask = async (list) => {
const given = [];
for (const g of list) {
@@ -57,6 +59,7 @@ async function world(opts: { classify?: boolean; announce?: false; stableForMs?:
};
const adapter: Adapter = {
async create(p) {
w.attempts++;
if (w.failing) throw w.failing;
w.created.push(p.as);
},
@@ -75,8 +78,9 @@ async function world(opts: { classify?: boolean; announce?: false; stableForMs?:
failingAfterMs: FAILING_AFTER, sayAgainEveryMs: SAY_AGAIN, stableForMs: opts.stableForMs ?? 0,
...(opts.announce === false ? {} : {
announce: (event: string, body: Record<string, unknown>) => {
w.all.push({ event, body });
if (body.why !== "first-success") w.said.push({ event, body });
const said = { event, body, at: Date.now() };
w.all.push(said);
if (body.why !== "first-success") w.said.push(said);
},
}),
});
@@ -94,10 +98,12 @@ test("a consumer failed for minutes is announced, naming it, its machine and the
const w = await world();
try {
w.failing = new Error(`token request failed: 401 {"error":"invalid_grant"} for pw-a`);
// "Not before failingAfterMs" and "not again too soon" are read from the clock at the moment each is said, not
// from a sleep: a sleep overruns under load and lets the very announcement it checks the absence of fall due.
const asked = Date.now();
await w.ask([{ as: "a", node: "anchor" }]);
await sleep(FAILING_AFTER / 2);
assert.equal(events(w.said), "", "announced before failingAfterMs");
await until(() => w.said.length === 1);
assert.ok(w.said[0].at - asked >= FAILING_AFTER, `announced after ${w.said[0].at - asked} ms, before failingAfterMs`);
const b = w.said[0].body;
assert.equal(w.said[0].event, FAILING_EVENT);
assert.equal(b.consumer, "a");
@@ -105,15 +111,14 @@ test("a consumer failed for minutes is announced, naming it, its machine and the
assert.equal(b.class, CLASS_CREDENTIALS);
assert.equal(b.provider, "postgres-database");
assert.equal(b["provider-node"], "home-server");
assert.ok((b.attempts as number) > 3, `attempts ${b.attempts}`);
assert.ok((b.attempts as number) > 1, `attempts ${b.attempts}`);
assert.match(String(b.since), /^\d{4}-\d{2}-\d{2}T/);
assert.doesNotMatch(String(b.error), /pw-a/, "the consumer's password travelled");
// Said again while it lasts, not every pass.
await sleep(SAY_AGAIN / 2);
assert.equal(events(w.said), FAILING_EVENT, "repeated too soon");
await until(() => w.said.length === 2);
assert.equal(events(w.said), `${FAILING_EVENT},${FAILING_EVENT}`);
assert.ok(w.said[1].at - w.said[0].at >= SAY_AGAIN, `repeated after ${w.said[1].at - w.said[0].at} ms, before sayAgainEveryMs`);
// The first success takes it back.
w.failing = null;
@@ -129,15 +134,21 @@ test("a consumer failed for minutes is announced, naming it, its machine and the
test("one success between failures starts the run again", async () => {
const w = await world();
try {
w.failing = new Error("connection refused");
await w.ask([{ as: "a" }]);
await sleep(FAILING_AFTER * 0.6);
w.failing = null;
// Each failing run lasts two failed attempts, counted, not a slept fraction of failingAfterMs. A run the load
// stretched to failingAfterMs is a long run and may be announced, so only runs that stayed short are asserted.
const run = async (values?: Record<string, unknown>): Promise<number> => {
const start = Date.now();
const base = w.attempts;
w.failing = new Error("connection refused");
await w.ask([{ as: "a", values }]);
await until(() => w.attempts >= base + 2);
w.failing = null;
return Date.now() - start;
};
const first = await run();
await until(() => w.created.length === 1);
w.failing = new Error("connection refused");
await w.ask([{ as: "a", values: { name: "changed" } }]);
await sleep(FAILING_AFTER * 0.6);
assert.equal(events(w.said), "", "two short runs are not one long one");
const second = await run({ name: "changed" });
if (first < FAILING_AFTER && second < FAILING_AFTER) assert.equal(events(w.said), "", "two short runs are not one long one");
} finally {
w.stop();
}