hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
134 lines (111 loc) • 4.32 kB
text/typescript
/* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */
import debug from "debug";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
// 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
// For more info check the Nomic Notion page (internal link).
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 {
private _mutexFilePath: string;
private _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 this._waitMs();
}
}
}
private async _tryToAcquireMutex() {
try {
// Create a file only if it does not exist
fs.writeFileSync(this._mutexFilePath, "", { flag: "wx+" });
return true;
} catch (error: any) {
if (error.code === "EEXIST") {
// File already exists, so the mutex is already acquired
return false;
}
throw error;
}
}
private async _executeFunctionAndReleaseMutex<T>(
f: () => Promise<T>
): Promise<T> {
log(`Mutex acquired at path '${this._mutexFilePath}'`);
try {
const res = await f();
// Release the mutex
log(`Mutex released at path '${this._mutexFilePath}'`);
this._deleteMutexFile();
log(`Mutex released at path '${this._mutexFilePath}'`);
return res;
} catch (error: any) {
// Catch any error to avoid stale locks.
// Remove the mutex file and re-throw the error
this._deleteMutexFile();
throw error;
}
}
private _isMutexFileTooOld(): boolean {
let fileStat;
try {
fileStat = fs.statSync(this._mutexFilePath);
} catch (error: any) {
if (error.code === "ENOENT") {
// The file might have been deleted by another process while this function was trying to access it.
return false;
}
throw error;
}
const now = new Date();
const fileDate = new Date(fileStat.ctime);
const diff = now.getTime() - fileDate.getTime();
return diff > this._mutexLifespanInMs;
}
private _deleteMutexFile() {
try {
log(`Deleting mutex file at path '${this._mutexFilePath}'`);
fs.unlinkSync(this._mutexFilePath);
} catch (error: any) {
if (error.code === "ENOENT") {
// The file might have been deleted by another process while this function was trying to access it.
return;
}
throw error;
}
}
private async _waitMs() {
return new Promise((resolve) =>
setTimeout(resolve, MUTEX_LOOP_WAITING_TIME_IN_MS)
);
}
}