1
- import * as lockfile from "lockfile" ;
1
+ import * as lockfile from "proper- lockfile" ;
2
2
import * as path from "path" ;
3
3
import { cache } from "../decorators" ;
4
+ import { getHash } from "../helpers" ;
4
5
5
6
export class LockService implements ILockService {
6
7
private currentlyLockedFiles : string [ ] = [ ] ;
@@ -15,12 +16,11 @@ export class LockService implements ILockService {
15
16
}
16
17
17
18
private get defaultLockParams ( ) : ILockOptions {
18
- // We'll retry 100 times and time between retries is 100ms, i.e. full wait in case we are unable to get lock will be 10 seconds.
19
- // In case lock is older than the `stale` value, consider it stale and try to get a new lock.
20
19
const lockParams : ILockOptions = {
21
- retryWait : 100 ,
22
- retries : 100 ,
23
- stale : 30 * 1000 ,
20
+ // https://www.npmjs.com/package/retry#retrytimeoutsoptions
21
+ retriesObj : { retries : 13 , minTimeout : 100 , maxTimeout : 1000 , factor : 2 } ,
22
+ stale : 10 * 1000 ,
23
+ realpath : false
24
24
} ;
25
25
26
26
return lockParams ;
@@ -31,41 +31,49 @@ export class LockService implements ILockService {
31
31
private $processService : IProcessService ) {
32
32
this . $processService . attachToProcessExitSignals ( this , ( ) => {
33
33
const locksToRemove = _ . clone ( this . currentlyLockedFiles ) ;
34
- _ . each ( locksToRemove , lock => {
35
- this . unlock ( lock ) ;
36
- } ) ;
34
+ for ( const lockToRemove of locksToRemove ) {
35
+ lockfile . unlockSync ( lockToRemove ) ;
36
+ this . cleanLock ( lockToRemove ) ;
37
+ }
37
38
} ) ;
38
39
}
39
40
40
41
public async executeActionWithLock < T > ( action : ( ) => Promise < T > , lockFilePath ?: string , lockOpts ?: ILockOptions ) : Promise < T > {
41
- const resolvedLockFilePath = await this . lock ( lockFilePath , lockOpts ) ;
42
+ const releaseFunc = await this . lock ( lockFilePath , lockOpts ) ;
42
43
43
44
try {
44
45
const result = await action ( ) ;
45
46
return result ;
46
47
} finally {
47
- this . unlock ( resolvedLockFilePath ) ;
48
+ releaseFunc ( ) ;
48
49
}
49
50
}
50
51
51
- public lock ( lockFilePath ?: string , lockOpts ?: ILockOptions ) : Promise < string > {
52
+ public async lock ( lockFilePath ?: string , lockOpts ?: ILockOptions ) : Promise < ( ) => void > {
52
53
const { filePath, fileOpts } = this . getLockFileSettings ( lockFilePath , lockOpts ) ;
53
54
this . currentlyLockedFiles . push ( filePath ) ;
55
+ this . $fs . writeFile ( filePath , "" ) ;
54
56
55
- // Prevent ENOENT error when the dir, where lock should be created, does not exist.
56
- this . $fs . ensureDirectoryExists ( path . dirname ( filePath ) ) ;
57
-
58
- return new Promise < string > ( ( resolve , reject ) => {
59
- lockfile . lock ( filePath , fileOpts , ( err : Error ) => {
60
- err ? reject ( new Error ( `Timeout while waiting for lock "${ filePath } "` ) ) : resolve ( filePath ) ;
61
- } ) ;
62
- } ) ;
57
+ try {
58
+ const releaseFunc = await lockfile . lock ( filePath , fileOpts ) ;
59
+ return async ( ) => {
60
+ await releaseFunc ( ) ;
61
+ this . cleanLock ( filePath ) ;
62
+ } ;
63
+ } catch ( err ) {
64
+ throw new Error ( `Timeout while waiting for lock "${ filePath } "` ) ;
65
+ }
63
66
}
64
67
65
68
public unlock ( lockFilePath ?: string ) : void {
66
69
const { filePath } = this . getLockFileSettings ( lockFilePath ) ;
67
- _ . remove ( this . currentlyLockedFiles , e => e === lockFilePath ) ;
68
70
lockfile . unlockSync ( filePath ) ;
71
+ this . cleanLock ( filePath ) ;
72
+ }
73
+
74
+ private cleanLock ( lockPath : string ) : void {
75
+ _ . remove ( this . currentlyLockedFiles , e => e === lockPath ) ;
76
+ this . $fs . deleteFile ( lockPath ) ;
69
77
}
70
78
71
79
private getLockFileSettings ( filePath ?: string , fileOpts ?: ILockOptions ) : { filePath : string , fileOpts : ILockOptions } {
@@ -76,11 +84,31 @@ export class LockService implements ILockService {
76
84
filePath = filePath || this . defaultLockFilePath ;
77
85
fileOpts = fileOpts ? _ . assign ( { } , this . defaultLockParams , fileOpts ) : this . defaultLockParams ;
78
86
87
+ fileOpts . retriesObj = fileOpts . retriesObj || { } ;
88
+ if ( fileOpts . retries ) {
89
+ fileOpts . retriesObj . retries = fileOpts . retries ;
90
+ }
91
+
92
+ if ( fileOpts . retryWait ) {
93
+ // backwards compatibility
94
+ fileOpts . retriesObj . minTimeout = fileOpts . retriesObj . maxTimeout = fileOpts . retryWait ;
95
+ }
96
+
97
+ ( < any > fileOpts . retries ) = fileOpts . retriesObj ;
98
+
79
99
return {
80
- filePath,
100
+ filePath : this . getShortFileLock ( filePath ) ,
81
101
fileOpts
82
102
} ;
83
103
}
104
+
105
+ private getShortFileLock ( filePath : string ) {
106
+ const dirPath = path . dirname ( filePath ) ;
107
+ const fileName = path . basename ( filePath ) ;
108
+ const hashedFileName = getHash ( fileName , { algorithm : "MD5" } ) ;
109
+ filePath = path . join ( dirPath , hashedFileName ) ;
110
+ return filePath ;
111
+ }
84
112
}
85
113
86
114
$injector . register ( "lockService" , LockService ) ;
0 commit comments