diff options
author | Asher Gomez <ashersaupingomez@gmail.com> | 2024-02-05 10:11:54 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-05 00:11:54 +0100 |
commit | 0f7f98795129f3f6bef51d08da9accca5e9ca9cc (patch) | |
tree | cb71c1358b9235a7f5098623750712e3de693277 | |
parent | 07a94984e1e3ca24dcaf114ac5ff82c8c3510894 (diff) |
feat(unstable): `Deno.FsFile.lock[Sync]()` and `Deno.FsFile.unlock[Sync]()` (#22235)
Closes #22178.
-rw-r--r-- | cli/tests/unit/files_test.ts | 195 | ||||
-rw-r--r-- | cli/tsc/dts/lib.deno.ns.d.ts | 24 | ||||
-rw-r--r-- | ext/fs/30_fs.js | 16 |
3 files changed, 235 insertions, 0 deletions
diff --git a/cli/tests/unit/files_test.ts b/cli/tests/unit/files_test.ts index a96f60a7a..aed09e814 100644 --- a/cli/tests/unit/files_test.ts +++ b/cli/tests/unit/files_test.ts @@ -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(); + }, + }; +} diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index 7e9e4277a..8401d2302 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -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. * diff --git a/ext/fs/30_fs.js b/ext/fs/30_fs.js index 9343b6ec4..aa7f345e5 100644 --- a/ext/fs/30_fs.js +++ b/ext/fs/30_fs.js @@ -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); } |