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

Implement robustness improvements in the compiler downloader #6056

Merged
merged 15 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 14 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
52 changes: 18 additions & 34 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion v-next/hardhat-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"./resolve": "./dist/src/resolve.js",
"./string": "./dist/src/string.js",
"./stream": "./dist/src/stream.js",
"./subprocess": "./dist/src/subprocess.js"
"./subprocess": "./dist/src/subprocess.js",
"./synchronization": "./dist/src/synchronization.js"
},
"keywords": [
"ethereum",
Expand Down
126 changes: 126 additions & 0 deletions v-next/hardhat-utils/src/synchronization.ts
Copy link
Member Author

Choose a reason for hiding this comment

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

I have copied the multi-process mutex from the v2.

Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Logic explanation: the fs.writeFile function, when used with the wx+ flag, performs an atomic operation to create a file.
// If multiple processes try to create the same file simultaneously, only one will succeed.
// This logic can be utilized to implement a mutex.
// ATTENTION: in the current implementation, there's still a risk of two processes running simultaneously.
// For example, if processA has locked the mutex and is running, processB will wait.
// During this wait, processB continuously checks the elapsed time since the mutex lock file was created.
// If an excessive amount of time has passed, processB will assume ownership of the mutex to avoid stale locks.
// However, there's a possibility that processB might take ownership because the mutex creation file is outdated, even though processA is still running

import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import debug from "debug";

import { ensureError } from "./error.js";
import { FileSystemAccessError } from "./errors/fs.js";
import { sleep } from "./lang.js";

const log = debug("hardhat:util:multi-process-mutex");
const DEFAULT_MAX_MUTEX_LIFESPAN_IN_MS = 60000;
const MUTEX_LOOP_WAITING_TIME_IN_MS = 100;

export class MultiProcessMutex {
readonly #mutexFilePath: string;
readonly #mutexLifespanInMs: number;

constructor(mutexName: string, maxMutexLifespanInMs?: number) {
log(`Creating mutex with name '${mutexName}'`);

this.#mutexFilePath = path.join(os.tmpdir(), `${mutexName}.txt`);
this.#mutexLifespanInMs =
maxMutexLifespanInMs ?? DEFAULT_MAX_MUTEX_LIFESPAN_IN_MS;
}

public async use<T>(f: () => Promise<T>): Promise<T> {
log(`Starting mutex process with mutex file '${this.#mutexFilePath}'`);

while (true) {
if (await this.#tryToAcquireMutex()) {
// Mutex has been acquired
return this.#executeFunctionAndReleaseMutex(f);
}

// Mutex not acquired
if (this.#isMutexFileTooOld()) {
// If the mutex file is too old, it likely indicates a stale lock, so the file should be removed
log(
`Current mutex file is too old, removing it at path '${this.#mutexFilePath}'`,
);
this.#deleteMutexFile();
} else {
// wait
await sleep(MUTEX_LOOP_WAITING_TIME_IN_MS / 1000);
}
}
}

async #tryToAcquireMutex() {
try {
// Create a file only if it does not exist
fs.writeFileSync(this.#mutexFilePath, "", { flag: "wx+" });
return true;
} catch (e) {
ensureError<NodeJS.ErrnoException>(e);

if (e.code === "EEXIST") {
// File already exists, so the mutex is already acquired
return false;
}

throw new FileSystemAccessError(e.message, e);
}
}

async #executeFunctionAndReleaseMutex<T>(f: () => Promise<T>): Promise<T> {
log(`Mutex acquired at path '${this.#mutexFilePath}'`);

try {
return await f();
} finally {
// Release the mutex
log(`Mutex released at path '${this.#mutexFilePath}'`);
this.#deleteMutexFile();
log(`Mutex released at path '${this.#mutexFilePath}'`);
}
}

#isMutexFileTooOld(): boolean {
let fileStat;
try {
fileStat = fs.statSync(this.#mutexFilePath);
} catch (e) {
ensureError<NodeJS.ErrnoException>(e);

if (e.code === "ENOENT") {
// The file might have been deleted by another process while this function was trying to access it.
return false;
}

throw new FileSystemAccessError(e.message, e);
}

const now = new Date();
const fileDate = new Date(fileStat.ctime);
const diff = now.getTime() - fileDate.getTime();

return diff > this.#mutexLifespanInMs;
}

#deleteMutexFile() {
try {
log(`Deleting mutex file at path '${this.#mutexFilePath}'`);
fs.unlinkSync(this.#mutexFilePath);
} catch (e) {
ensureError<NodeJS.ErrnoException>(e);

if (e.code === "ENOENT") {
// The file might have been deleted by another process while this function was trying to access it.
return;
}

throw new FileSystemAccessError(e.message, e);
}
}
}
1 change: 0 additions & 1 deletion v-next/hardhat-utils/test/subprocess.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import assert from "node:assert/strict";
import assert from "node:assert/strict";
import path from "node:path";
import { afterEach, beforeEach, describe, it } from "node:test";
Expand Down
Loading
Loading