From d70d0152b63e4109b26bda12f0d20c37069d9cdb Mon Sep 17 00:00:00 2001 From: Casper Beyer Date: Sun, 19 Sep 2021 20:56:30 +0800 Subject: [PATCH] feat(runtime): add lock and unlock methods to `Deno.File` --- cli/dts/lib.deno.unstable.d.ts | 7 ++ cli/tests/unit/lock_test.ts | 161 +++++++++++++++++++++++++++++++++ runtime/js/40_files.js | 27 +++++- 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 cli/tests/unit/lock_test.ts diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 1a27f2b3896875..7a5a4c6625cce2 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -802,6 +802,13 @@ declare namespace Deno { options: CreateHttpClientOptions, ): HttpClient; + export interface File { + lock(exclusive?: boolean): Promise; + lockSync(exclusive?: boolean): void; + unlock(): Promise; + unlockSync(): void; + } + /** **UNSTABLE**: needs investigation into high precision time. * * Synchronously changes the access (`atime`) and modification (`mtime`) times diff --git a/cli/tests/unit/lock_test.ts b/cli/tests/unit/lock_test.ts new file mode 100644 index 00000000000000..f2552a8929de97 --- /dev/null +++ b/cli/tests/unit/lock_test.ts @@ -0,0 +1,161 @@ +// 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 lockFileSync() { + await runLockTests({ sync: true }); + }, +); + +unitTest( + { perms: { read: true, run: true, hrtime: true } }, + async function lockFileAsync() { + await runLockTests({ sync: false }); + }, +); + +async function runLockTests(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 = runLockTestProcess({ + exclusive: opts.firstExclusive, + sync: opts.sync, + }); + const secondProcess = runLockTestProcess({ + 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 + // signal the second to enter the lock + await secondProcess.signal(); + await secondProcess.waitSignal(); // entering signal + await sleep(100); + // signal to the first to exit the lock + await firstProcess.signal(); + // collect the final output so we know it's exited the lock + const firstPsTimes = await firstProcess.getTimes(); + // signal to the second to exit the lock + await secondProcess.waitSignal(); // entered signal + await secondProcess.signal(); + const secondPsTimes = await secondProcess.getTimes(); + return firstPsTimes.exitTime < secondPsTimes.enterTime; + } finally { + firstProcess.close(); + secondProcess.close(); + } +} + +function runLockTestProcess(opts: { exclusive: boolean; sync: boolean }) { + const path = "cli/tests/testdata/fixture.json"; + 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(); + Deno.sleepSync(100); + + // release the lock + ${opts.sync ? "file.unlockSync();" : "await file.unlock();"} + + // 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(); + }, + }; +} diff --git a/runtime/js/40_files.js b/runtime/js/40_files.js index dfd4717507b0a0..549f44785187c4 100644 --- a/runtime/js/40_files.js +++ b/runtime/js/40_files.js @@ -4,7 +4,16 @@ ((window) => { const core = window.Deno.core; const { read, readSync, write, writeSync } = window.__bootstrap.io; - const { ftruncate, ftruncateSync, fstat, fstatSync } = window.__bootstrap.fs; + const { + ftruncate, + ftruncateSync, + fstat, + fstatSync, + flock, + flockSync, + funlock, + funlockSync, + } = window.__bootstrap.fs; const { pathFromURL } = window.__bootstrap.util; const { Error, @@ -125,6 +134,22 @@ return fstatSync(this.rid); } + lock(exclusive) { + return flock(this.rid, exclusive); + } + + lockSync(exclusive) { + return flockSync(this.rid, exclusive); + } + + unlock() { + return funlock(this.rid); + } + + unlockSync() { + return funlockSync(this.rid); + } + close() { core.close(this.rid); }