feat(unstable): Deno.FsFile.lock[Sync]() and Deno.FsFile.unlock[Sync]() (#22235)

Closes #22178.
This commit is contained in:
Asher Gomez 2024-02-05 10:11:54 +11:00 committed by GitHub
parent 07a94984e1
commit 0f7f987951
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 235 additions and 0 deletions

View File

@ -898,3 +898,198 @@ Deno.test(
await Deno.remove(filename);
},
);
Deno.test(
{ permissions: { read: true, run: true, hrtime: true } },
async function fsFileLockFileSync() {
await runFlockTests({ sync: true });
},
);
Deno.test(
{ permissions: { read: true, run: true, hrtime: true } },
async function fsFileLockFileAsync() {
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,
// need to wait for both to enter the lock to prevent the case where the
// first process enters and exits the lock before the second even enters
waitBothEnteredLock: true,
}),
false,
"shared does not block shared",
);
}
async function checkFirstBlocksSecond(opts: {
firstExclusive: boolean;
secondExclusive: boolean;
sync: boolean;
waitBothEnteredLock?: 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));
await Promise.all([
firstProcess.waitStartup(),
secondProcess.waitStartup(),
]);
await firstProcess.enterLock();
await firstProcess.waitEnterLock();
await secondProcess.enterLock();
await sleep(100);
if (!opts.waitBothEnteredLock) {
await firstProcess.exitLock();
}
await secondProcess.waitEnterLock();
if (opts.waitBothEnteredLock) {
await firstProcess.exitLock();
}
await secondProcess.exitLock();
// collect the final output
const firstPsTimes = await firstProcess.getTimes();
const secondPsTimes = await secondProcess.getTimes();
return firstPsTimes.exitTime < secondPsTimes.enterTime;
} finally {
await firstProcess.close();
await secondProcess.close();
}
}
function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) {
const path = "cli/tests/testdata/assets/lock_target.txt";
const scriptText = `
const file = 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
? `file.lockSync(${opts.exclusive ? "true" : "false"});`
: `await file.lock(${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
const exitTime = new Date().getTime();
await new Promise(resolve => setTimeout(resolve, 100));
// release the lock
${opts.sync ? "file.unlockSync();" : "await file.unlock();"}
// exited signal
Deno.stdout.writeSync(new Uint8Array(1));
// output the enter and exit time
console.log(JSON.stringify({ enterTime, exitTime }));
`;
const process = new Deno.Command(Deno.execPath(), {
args: ["eval", "--unstable", scriptText],
stdin: "piped",
stdout: "piped",
stderr: "null",
}).spawn();
const waitSignal = async () => {
const reader = process.stdout.getReader({ mode: "byob" });
await reader.read(new Uint8Array(1));
reader.releaseLock();
};
const signal = async () => {
const writer = process.stdin.getWriter();
await writer.write(new Uint8Array(1));
writer.releaseLock();
};
return {
async waitStartup() {
await waitSignal();
},
async enterLock() {
await signal();
await waitSignal(); // entering signal
},
async waitEnterLock() {
await waitSignal();
},
async exitLock() {
await signal();
await waitSignal();
},
getTimes: async () => {
const { stdout } = await process.output();
const text = new TextDecoder().decode(stdout);
return JSON.parse(text) as {
enterTime: number;
exitTime: number;
};
},
close: async () => {
await process.status;
await process.stdin.close();
},
};
}

View File

@ -2666,6 +2666,30 @@ declare namespace Deno {
* @category File System
*/
utimeSync(atime: number | Date, mtime: number | Date): void;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Acquire an advisory file-system lock for the file.
*
* @param [exclusive=false]
*/
lock(exclusive?: boolean): Promise<void>;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Synchronously acquire an advisory file-system lock synchronously for the file.
*
* @param [exclusive=false]
*/
lockSync(exclusive?: boolean): void;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Release an advisory file-system lock for the file.
*/
unlock(): Promise<void>;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Synchronously release an advisory file-system lock for the file.
*/
unlockSync(): void;
/** Close the file. Closing a file when you are finished with it is
* important to avoid leaking resources.
*

View File

@ -766,6 +766,22 @@ class FsFile {
futimeSync(this.#rid, atime, mtime);
}
lockSync(exclusive = false) {
op_fs_flock_sync(this.#rid, exclusive);
}
async lock(exclusive = false) {
await op_fs_flock_async(this.#rid, exclusive);
}
unlockSync() {
op_fs_funlock_sync(this.#rid);
}
async unlock() {
await op_fs_funlock_async(this.#rid);
}
[SymbolDispose]() {
core.tryClose(this.#rid);
}