Skip to content

Commit 75574a1

Browse files
committed
fix(lockfile): add uuid to lock instance
- add "uuidv4" to an lock instance - fix that lockfile could get overwritten by the same process
1 parent 7ca52da commit 75574a1

File tree

2 files changed

+101
-39
lines changed

2 files changed

+101
-39
lines changed

packages/mongodb-memory-server-core/src/util/__tests__/lockfile.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('LockFile', () => {
2626
expect(lockFile.LockFile.files.size).toBe(1);
2727
expect(lockFile.LockFile.files.has(lockPath)).toBeTruthy();
2828

29-
const lockReadout = parseInt((await fspromises.readFile(lockPath)).toString());
29+
const lockReadout = parseInt((await fspromises.readFile(lockPath)).toString().split(' ')[0]);
3030
expect(lockReadout).toEqual(process.pid);
3131

3232
await lock.unlock();

packages/mongodb-memory-server-core/src/util/lockfile.ts

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,35 @@ import * as path from 'path';
55
import mkdirp from 'mkdirp';
66
import { promises as fspromises } from 'fs';
77
import { Mutex } from 'async-mutex';
8+
import { v4 as uuidv4 } from 'uuid';
89

910
const log = debug('MongoMS:LockFile');
1011

12+
/**
13+
* Error used to cause an promise to stop and re-wait for an lockfile
14+
*/
15+
class RepeatError extends Error {
16+
constructor(public repeat: boolean) {
17+
super();
18+
}
19+
}
20+
1121
export enum LockFileStatus {
22+
/**
23+
* Status is "available" to be grabbed (lockfile not existing or being invalid)
24+
*/
1225
available,
26+
/**
27+
* Status is "available for asking instance" (instance that asked has the lock)
28+
*/
29+
availableInstance,
30+
/**
31+
* Status is "locked by another instance in this process"
32+
*/
1333
lockedSelf,
34+
/**
35+
* Status is "locked by another process"
36+
*/
1437
lockedDifferent,
1538
}
1639

@@ -65,21 +88,37 @@ export class LockFile {
6588
* Check the status of the lockfile
6689
* @param file The file to use as the LockFile
6790
*/
68-
protected static async checkLock(file: string): Promise<LockFileStatus> {
69-
log(`checkLock: for file "${file}"`);
91+
protected static async checkLock(file: string, uuid?: string): Promise<LockFileStatus> {
92+
log(`checkLock: for file "${file}" with uuid: "${uuid}"`);
7093

7194
// if file / path does not exist, directly acquire lock
7295
if (!(await utils.pathExists(file))) {
7396
return LockFileStatus.available;
7497
}
7598

7699
try {
77-
const readout = parseInt((await fspromises.readFile(file)).toString().trim());
100+
const fileData = (await fspromises.readFile(file)).toString().trim().split(' ');
101+
const readout = parseInt(fileData[0]);
78102

79103
if (readout === process.pid) {
80-
log('checkLock: Lock File Already exists, and is for *this* process');
104+
log(
105+
`checkLock: Lock File Already exists, and is for *this* process, with uuid: "${fileData[1]}"`
106+
);
107+
108+
// early return if "file"(input) dosnt exists within the files Map anymore
109+
if (!this.files.has(file)) {
110+
return LockFileStatus.available;
111+
}
112+
113+
// check if "uuid"(input) matches the filereadout, if yes say "available" (for unlock check)
114+
if (!utils.isNullOrUndefined(uuid)) {
115+
return uuid === fileData[1]
116+
? LockFileStatus.availableInstance
117+
: LockFileStatus.lockedSelf;
118+
}
81119

82-
return !this.files.has(file) ? LockFileStatus.available : LockFileStatus.lockedSelf;
120+
// as fallback say "lockedSelf"
121+
return LockFileStatus.lockedSelf;
83122
}
84123

85124
log(`checkLock: Lock File Aready exists, for an different process: "${readout}"`);
@@ -156,24 +195,35 @@ export class LockFile {
156195
*/
157196
protected static async createLock(file: string): Promise<LockFile> {
158197
// this function only gets called by processed "file" input, so no re-checking
159-
log(`createLock: Creating lock file "${file}"`);
198+
log(`createLock: trying to create a lock file for "${file}"`);
199+
const uuid = uuidv4();
160200

161-
if (this.files.has(file)) {
162-
log(`createLock: Set already has file "${file}", ignoring`);
163-
}
201+
// This is not an ".catch" because in an callback running "return" dosnt "return" the parent function
202+
try {
203+
await this.mutex.runExclusive(async () => {
204+
// this may cause "Stack Size" errors, because of an infinite loop if too many times this gets called
205+
if (this.files.has(file)) {
206+
log(`createLock: Map already has file "${file}"`);
164207

165-
await this.mutex.runExclusive(async () => {
166-
await mkdirp(path.dirname(file));
208+
throw new RepeatError(true);
209+
}
167210

168-
await fspromises.writeFile(file, process.pid.toString());
211+
await mkdirp(path.dirname(file));
169212

170-
this.files.add(file);
171-
this.events.emit(LockFileEvents.lock, file);
172-
});
213+
await fspromises.writeFile(file, `${process.pid.toString()} ${uuid}`);
214+
215+
this.files.add(file);
216+
this.events.emit(LockFileEvents.lock, file);
217+
});
218+
} catch (err) {
219+
if (err instanceof RepeatError && err.repeat) {
220+
return this.waitForLock(file);
221+
}
222+
}
173223

174-
log('createLock: Lock File Created');
224+
log(`createLock: Lock File Created for file "${file}"`);
175225

176-
return new this(file);
226+
return new this(file, uuid);
177227
}
178228

179229
// Below here are instance functions & values
@@ -182,9 +232,14 @@ export class LockFile {
182232
* File locked by this instance
183233
*/
184234
public file?: string;
235+
/**
236+
* UUID Unique to this lock instance
237+
*/
238+
public uuid?: string;
185239

186-
constructor(file: string) {
240+
constructor(file: string, uuid: string) {
187241
this.file = file;
242+
this.uuid = uuid;
188243
}
189244

190245
/**
@@ -200,16 +255,22 @@ export class LockFile {
200255
return;
201256
}
202257

203-
switch (await LockFile.checkLock(this.file)) {
258+
// No "case-fallthrough" because this is more clear (and no linter will complain)
259+
switch (await LockFile.checkLock(this.file, this.uuid)) {
204260
case LockFileStatus.available:
205-
log(`unlock: Lock Status was already "available" for file "${this.file}", ignoring`);
206-
await LockFile.mutex.runExclusive(this.unlockCleanup.bind(this, false));
261+
log(`unlock: Lock Status was already "available" for file "${this.file}"`);
262+
await this.unlockCleanup(false);
207263

208264
return;
209-
case LockFileStatus.lockedSelf:
210-
await LockFile.mutex.runExclusive(this.unlockCleanup.bind(this, true));
265+
case LockFileStatus.availableInstance:
266+
log(`unlock: Lock Status was "availableInstance" for file "${this.file}"`);
267+
await this.unlockCleanup(true);
211268

212269
return;
270+
case LockFileStatus.lockedSelf:
271+
throw new Error(
272+
`Cannot unlock file "${this.file}", because it is not locked by this instance!`
273+
);
213274
default:
214275
throw new Error(
215276
`Cannot unlock Lock File "${this.file}" because it is not locked by this process!`
@@ -222,24 +283,25 @@ export class LockFile {
222283
* @param fileio Unlink the file?
223284
*/
224285
protected async unlockCleanup(fileio: boolean = true): Promise<void> {
225-
log(`unlockCleanup: for file "${this.file}"`);
286+
return await LockFile.mutex.runExclusive(async () => {
287+
log(`unlockCleanup: for file "${this.file}"`);
226288

227-
if (utils.isNullOrUndefined(this.file)) {
228-
return;
229-
}
230-
231-
if (fileio) {
232-
await fspromises.unlink(this.file).catch((reason) => {
233-
log(`unlockCleanup: lock file unlink failed: "${reason}"`);
234-
});
235-
}
289+
if (utils.isNullOrUndefined(this.file)) {
290+
return;
291+
}
236292

237-
LockFile.files.delete(this.file);
238-
LockFile.events.emit(LockFileEvents.unlock, this.file);
293+
if (fileio) {
294+
await fspromises.unlink(this.file).catch((reason) => {
295+
log(`unlockCleanup: lock file unlink failed: "${reason}"`);
296+
});
297+
}
239298

240-
// make this LockFile instance unusable (to prevent double unlock calling)
241-
this.file = undefined;
299+
LockFile.files.delete(this.file);
300+
LockFile.events.emit(LockFileEvents.unlock, this.file);
242301

243-
await utils.ensureAsync();
302+
// make this LockFile instance unusable (to prevent double unlock calling)
303+
this.file = undefined;
304+
this.uuid = undefined;
305+
});
244306
}
245307
}

0 commit comments

Comments
 (0)