chore(tests): fix flaky flock tests (#12099)

This commit is contained in:
David Sherret 2021-09-17 09:02:23 -04:00 committed by GitHub
parent 4b79e5a459
commit c8b43a0328
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -1,102 +1,164 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { assertEquals, unitTest } from "./test_util.ts";
import { readAll } from "../../../test_util/std/io/util.ts";
unitTest(
{ perms: { read: true, run: true, hrtime: true } },
async function flockFileSync() {
const path = "cli/tests/testdata/fixture.json";
const script = (exclusive: boolean, wait: number) => `
const { rid } = Deno.openSync("${path}");
Deno.flockSync(rid, ${exclusive ? "true" : "false"});
await new Promise(res => setTimeout(res, ${wait}));
Deno.funlockSync(rid);
`;
const run = (e: boolean, w: number) =>
Deno.run({ cmd: [Deno.execPath(), "eval", "--unstable", script(e, w)] });
const firstBlocksSecond = async (
first: boolean,
second: boolean,
): Promise<boolean> => {
const firstPs = run(first, 1000);
await new Promise((res) => setTimeout(res, 250));
const start = performance.now();
const secondPs = run(second, 0);
await secondPs.status();
const didBlock = (performance.now() - start) > 500;
firstPs.close();
secondPs.close();
return didBlock;
};
assertEquals(
await firstBlocksSecond(true, false),
true,
"exclusive blocks shared",
);
assertEquals(
await firstBlocksSecond(false, true),
true,
"shared blocks exclusive",
);
assertEquals(
await firstBlocksSecond(true, true),
true,
"exclusive blocks exclusive",
);
assertEquals(
await firstBlocksSecond(false, false),
false,
"shared does not block shared",
);
await runFlockTests({ sync: true });
},
);
unitTest(
{ perms: { read: true, run: true, hrtime: true } },
async function flockFileAsync() {
const path = "cli/tests/testdata/fixture.json";
const script = (exclusive: boolean, wait: number) => `
const { rid } = await Deno.open("${path}");
await Deno.flock(rid, ${exclusive ? "true" : "false"});
await new Promise(res => setTimeout(res, ${wait}));
await Deno.funlock(rid);
`;
const run = (e: boolean, w: number) =>
Deno.run({ cmd: [Deno.execPath(), "eval", "--unstable", script(e, w)] });
const firstBlocksSecond = async (
first: boolean,
second: boolean,
): Promise<boolean> => {
const firstPs = run(first, 1000);
await new Promise((res) => setTimeout(res, 250));
const start = performance.now();
const secondPs = run(second, 0);
await secondPs.status();
const didBlock = (performance.now() - start) > 500;
firstPs.close();
secondPs.close();
return didBlock;
};
assertEquals(
await firstBlocksSecond(true, false),
true,
"exclusive blocks shared",
);
assertEquals(
await firstBlocksSecond(false, true),
true,
"shared blocks exclusive",
);
assertEquals(
await firstBlocksSecond(true, true),
true,
"exclusive blocks exclusive",
);
assertEquals(
await firstBlocksSecond(false, false),
false,
"shared does not block shared",
);
await runFlockTests({ sync: false });
},
);
async function runFlockTests(opts: { sync: boolean }) {
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: true,
secondExclusive: false,
sync: opts.sync,
}),
true,
"exclusive blocks shared",
);
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: false,
secondExclusive: true,
sync: opts.sync,
}),
true,
"shared blocks exclusive",
);
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: true,
secondExclusive: true,
sync: opts.sync,
}),
true,
"exclusive blocks exclusive",
);
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: false,
secondExclusive: false,
sync: opts.sync,
}),
false,
"shared does not block shared",
);
}
async function checkFirstBlocksSecond(opts: {
firstExclusive: boolean;
secondExclusive: boolean;
sync: boolean;
}) {
const firstProcess = runFlockTestProcess({
exclusive: opts.firstExclusive,
sync: opts.sync,
});
const secondProcess = runFlockTestProcess({
exclusive: opts.secondExclusive,
sync: opts.sync,
});
try {
const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
// wait for both processes to signal that they're ready
await Promise.all([firstProcess.waitSignal(), secondProcess.waitSignal()]);
// signal to the first process to enter the lock
await firstProcess.signal();
await firstProcess.waitSignal(); // entering signal
await firstProcess.waitSignal(); // entered signal
await sleep(20);
// signal the second to enter the lock
await secondProcess.signal();
await secondProcess.waitSignal(); // entering signal
await sleep(20);
// signal to the first to exit the lock
await firstProcess.signal();
await sleep(20);
// signal to the second to exit the lock
await secondProcess.waitSignal(); // entered signal
await secondProcess.signal();
// collect the remaining JSON output of both processes
const firstPsTimes = await firstProcess.getTimes();
const secondPsTimes = await secondProcess.getTimes();
return firstPsTimes.exitTime < secondPsTimes.enterTime;
} finally {
firstProcess.close();
secondProcess.close();
}
}
function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) {
const path = "cli/tests/testdata/fixture.json";
const scriptText = `
const { rid } = Deno.openSync("${path}");
// ready signal
Deno.stdout.writeSync(new Uint8Array(1));
// wait for enter lock signal
Deno.stdin.readSync(new Uint8Array(1));
// entering signal
Deno.stdout.writeSync(new Uint8Array(1));
// lock and record the entry time
${
opts.sync
? `Deno.flockSync(rid, ${opts.exclusive ? "true" : "false"});`
: `await Deno.flock(rid, ${opts.exclusive ? "true" : "false"});`
}
const enterTime = new Date().getTime();
// entered signal
Deno.stdout.writeSync(new Uint8Array(1));
// wait for exit lock signal
Deno.stdin.readSync(new Uint8Array(1));
// record the exit time and wait a little bit before releasing
// the lock so that the enter time of the next process doesn't
// occur at the same time as this exit time (do double the
// windows clock resolution)
const exitTime = new Date().getTime();
await new Promise((resolve) => setTimeout(resolve, 30));
// release the lock
${opts.sync ? "Deno.funlockSync(rid);" : "await Deno.funlock(rid);"}
// output the enter and exit time
console.log(JSON.stringify({ enterTime, exitTime }));
`;
const process = Deno.run({
cmd: [Deno.execPath(), "eval", "--unstable", scriptText],
stdout: "piped",
stdin: "piped",
});
return {
waitSignal: () => process.stdout.read(new Uint8Array(1)),
signal: () => process.stdin.write(new Uint8Array(1)),
getTimes: async () => {
const outputBytes = await readAll(process.stdout);
const text = new TextDecoder().decode(outputBytes);
return JSON.parse(text) as {
enterTime: number;
exitTime: number;
};
},
close: () => {
process.stdout.close();
process.stdin.close();
process.close();
},
};
}