UNPKG

mongodb-memory-server-core

Version:

MongoDB Server for testing (core package, without autodownload). The server will allow you to connect your favourite ODM or client library to the MongoDB Server and run parallel integration tests isolated from each other.

257 lines 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LockFile = exports.LockFileEvents = exports.LockFileStatus = void 0; const tslib_1 = require("tslib"); const events_1 = require("events"); const utils = tslib_1.__importStar(require("./utils")); const debug_1 = tslib_1.__importDefault(require("debug")); const path = tslib_1.__importStar(require("path")); const fs_1 = require("fs"); const async_mutex_1 = require("async-mutex"); const errors_1 = require("./errors"); const log = (0, debug_1.default)('MongoMS:LockFile'); /** * Error used to cause an promise to stop and re-wait for an lockfile */ class RepeatError extends Error { repeat; constructor(repeat) { super(); this.repeat = repeat; } } var LockFileStatus; (function (LockFileStatus) { /** * Status is "available" to be grabbed (lockfile not existing or being invalid) */ LockFileStatus[LockFileStatus["available"] = 0] = "available"; /** * Status is "available for asking instance" (instance that asked has the lock) */ LockFileStatus[LockFileStatus["availableInstance"] = 1] = "availableInstance"; /** * Status is "locked by another instance in this process" */ LockFileStatus[LockFileStatus["lockedSelf"] = 2] = "lockedSelf"; /** * Status is "locked by another process" */ LockFileStatus[LockFileStatus["lockedDifferent"] = 3] = "lockedDifferent"; })(LockFileStatus || (exports.LockFileStatus = LockFileStatus = {})); var LockFileEvents; (function (LockFileEvents) { LockFileEvents["lock"] = "lock"; LockFileEvents["unlock"] = "unlock"; })(LockFileEvents || (exports.LockFileEvents = LockFileEvents = {})); /** Dummy class for types */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging class LockFileEventsClass extends events_1.EventEmitter { } class LockFile { /** All Files that are handled by this process */ static files = new Set(); /** Listen for events from this process */ static events = new LockFileEventsClass(); /** Mutex to stop same-process race conditions */ static mutex = new async_mutex_1.Mutex(); /** * Acquire an lockfile * @param file The file to use as the LockFile */ static async lock(file) { await utils.ensureAsync(); log(`lock: Locking file "${file}"`); const useFile = path.resolve(file.trim()); // just to make sure "path" could resolve it to something utils.assertion(useFile.length > 0, new Error('Provided Path for lock file is length of 0')); const status = await this.checkLock(useFile); switch (status) { case LockFileStatus.lockedDifferent: case LockFileStatus.lockedSelf: return this.waitForLock(useFile); case LockFileStatus.available: return this.createLock(useFile); default: throw new errors_1.UnknownLockfileStatusError(status); } } /** * Check the status of the lockfile * @param file The file to use as the LockFile */ static async checkLock(file, uuid) { log(`checkLock: for file "${file}" with uuid: "${uuid}"`); // if file / path does not exist, directly acquire lock if (!(await utils.pathExists(file))) { return LockFileStatus.available; } try { const fileData = (await fs_1.promises.readFile(file)).toString().trim().split(' '); const readout = parseInt(fileData[0]); if (readout === process.pid) { log(`checkLock: Lock File Already exists, and is for *this* process, with uuid: "${fileData[1]}"`); // early return if "file"(input) dosnt exists within the files Map anymore if (!this.files.has(file)) { return LockFileStatus.available; } // check if "uuid"(input) matches the filereadout, if yes say "available" (for unlock check) if (!utils.isNullOrUndefined(uuid)) { return uuid === fileData[1] ? LockFileStatus.availableInstance : LockFileStatus.lockedSelf; } // as fallback say "lockedSelf" return LockFileStatus.lockedSelf; } log(`checkLock: Lock File Aready exists, for a different process: "${readout}"`); return utils.isAlive(readout) ? LockFileStatus.lockedDifferent : LockFileStatus.available; } catch (err) { if (utils.errorWithCode(err) && err.code === 'ENOENT') { log('checkLock: reading file failed with ENOENT'); return LockFileStatus.available; } throw err; } } /** * Wait for the Lock file to become available * @param file The file to use as the LockFile */ static async waitForLock(file) { log(`waitForLock: Starting to wait for file "${file}"`); /** Store the interval id to be cleared later */ let interval = undefined; /** Store the function in an value to be cleared later, without having to use an class-external or class function */ let eventCB = undefined; await new Promise((res) => { eventCB = (unlockedFile) => { if (unlockedFile === file) { res(); } }; interval = setInterval(async () => { const lockStatus = await this.checkLock(file); log(`waitForLock: Interval for file "${file}" with status "${lockStatus}"`); if (lockStatus === LockFileStatus.available) { res(); } }, 1000 * 3); // every 3 seconds this.events.on(LockFileEvents.unlock, eventCB); }); if (interval) { clearInterval(interval); } if (eventCB) { this.events.removeListener(LockFileEvents.unlock, eventCB); } log(`waitForLock: File became available "${file}"`); // i hope the following prevents race-conditions await utils.ensureAsync(); // to make sure all event listeners got executed const lockStatus = await this.checkLock(file); log(`waitForLock: Lock File Status reassessment for file "${file}": ${lockStatus}`); switch (lockStatus) { case LockFileStatus.lockedDifferent: case LockFileStatus.lockedSelf: return this.waitForLock(file); case LockFileStatus.available: return this.createLock(file); default: throw new errors_1.UnknownLockfileStatusError(lockStatus); } } /** * Function create the path and lock file * @param file The file to use as the LockFile */ static async createLock(file) { // this function only gets called by processed "file" input, so no re-checking log(`createLock: trying to create a lock file for "${file}"`); const uuid = utils.uuidv4(); // This is not an ".catch" because in an callback running "return" dosnt "return" the parent function try { await this.mutex.runExclusive(async () => { // this may cause "Stack Size" errors, because of an infinite loop if too many times this gets called if (this.files.has(file)) { log(`createLock: Map already has file "${file}"`); throw new RepeatError(true); } await utils.mkdir(path.dirname(file)); await fs_1.promises.writeFile(file, `${process.pid.toString()} ${uuid}`); this.files.add(file); this.events.emit(LockFileEvents.lock, file); }); } catch (err) { if (err instanceof RepeatError && err.repeat) { return this.waitForLock(file); } } log(`createLock: Lock File Created for file "${file}"`); return new this(file, uuid); } // Below here are instance functions & values /** * File locked by this instance */ file; /** * UUID Unique to this lock instance */ uuid; constructor(file, uuid) { this.file = file; this.uuid = uuid; } /** * Unlock the File that is locked by this instance */ async unlock() { await utils.ensureAsync(); log(`unlock: Unlocking file "${this.file}"`); if (utils.isNullOrUndefined(this.file) || this.file?.length <= 0) { log('unlock: invalid file, returning'); return; } // No "case-fallthrough" because this is more clear (and no linter will complain) switch (await LockFile.checkLock(this.file, this.uuid)) { case LockFileStatus.available: log(`unlock: Lock Status was already "available" for file "${this.file}"`); await this.unlockCleanup(false); return; case LockFileStatus.availableInstance: log(`unlock: Lock Status was "availableInstance" for file "${this.file}"`); await this.unlockCleanup(true); return; case LockFileStatus.lockedSelf: throw new errors_1.UnableToUnlockLockfileError(true, this.file); default: throw new errors_1.UnableToUnlockLockfileError(false, this.file); } } /** * Helper function for the unlock-cleanup * @param fileio Unlink the file? */ async unlockCleanup(fileio = true) { return await LockFile.mutex.runExclusive(async () => { log(`unlockCleanup: for file "${this.file}"`); if (utils.isNullOrUndefined(this.file)) { return; } if (fileio) { await fs_1.promises.unlink(this.file).catch((reason) => { log(`unlockCleanup: lock file unlink failed: "${reason}"`); }); } LockFile.files.delete(this.file); LockFile.events.emit(LockFileEvents.unlock, this.file); // make this LockFile instance unusable (to prevent double unlock calling) this.file = undefined; this.uuid = undefined; }); } } exports.LockFile = LockFile; //# sourceMappingURL=lockfile.js.map