Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(unstable): add lock and unlock methods to Deno.File #12139

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cli/dts/lib.deno.unstable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,13 @@ declare namespace Deno {
options: CreateHttpClientOptions,
): HttpClient;

export interface File {
lock(exclusive?: boolean): Promise<void>;
lockSync(exclusive?: boolean): void;
unlock(): Promise<void>;
unlockSync(): void;
}

/** **UNSTABLE**: needs investigation into high precision time.
*
* Synchronously changes the access (`atime`) and modification (`mtime`) times
Expand Down
161 changes: 161 additions & 0 deletions cli/tests/unit/lock_test.ts
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that these tests are flaky. Perhaps we could update these tests to use the changes in:

https://github.com/denoland/deno/pull/12255/files

Then remove that file and only use this one (since this transitively tests those functions).

{ 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();
},
};
}
27 changes: 26 additions & 1 deletion runtime/js/40_files.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down