@@ -5,12 +5,35 @@ import * as path from 'path';
55import mkdirp from 'mkdirp' ;
66import { promises as fspromises } from 'fs' ;
77import { Mutex } from 'async-mutex' ;
8+ import { v4 as uuidv4 } from 'uuid' ;
89
910const 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+
1121export 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