-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #981 from forcedotcom/sm/reusable-file-locks
feat: reusable file locks outside of ConfigFile
- Loading branch information
Showing
3 changed files
with
105 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright (c) 2023, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
import * as fs from 'node:fs'; | ||
import { dirname } from 'node:path'; | ||
import { lock, lockSync } from 'proper-lockfile'; | ||
import { SfError } from '../sfError'; | ||
import { Logger } from '../logger/logger'; | ||
import { lockRetryOptions } from './lockRetryOptions'; | ||
|
||
type LockInitResponse = { writeAndUnlock: (data: string) => Promise<void>; unlock: () => Promise<void> }; | ||
type LockInitSyncResponse = { writeAndUnlock: (data: string) => void; unlock: () => void }; | ||
|
||
/** | ||
* | ||
*This method exists as a separate function so it can be used by ConfigFile OR outside of ConfigFile. | ||
* | ||
* @param filePath where to save the file | ||
* @returns 2 functions: | ||
* - writeAndUnlock: a function that takes the data to write and writes it to the file, then unlocks the file whether write succeeded or not | ||
* - unlock: a function that unlocks the file (in case you don't end up calling writeAndUnlock) | ||
*/ | ||
export const lockInit = async (filePath: string): Promise<LockInitResponse> => { | ||
// make sure we can write to the directory | ||
try { | ||
await fs.promises.mkdir(dirname(filePath), { recursive: true }); | ||
} catch (err) { | ||
throw SfError.wrap(err as Error); | ||
} | ||
|
||
const [unlock] = await Promise.all( | ||
fs.existsSync(filePath) | ||
? // if the file exists, wait for it to be unlocked | ||
[lock(filePath, lockRetryOptions)] | ||
: // lock the entire directory to keep others from trying to create the file while we are | ||
[ | ||
lock(dirname(filePath), lockRetryOptions), | ||
( | ||
await Logger.child('fileLocking.lockInit') | ||
).debug( | ||
`No file found at ${filePath}. Write will create it. Locking the entire directory until file is written.` | ||
), | ||
] | ||
); | ||
|
||
return { | ||
writeAndUnlock: async (data: string): Promise<void> => { | ||
const logger = await Logger.child('fileLocking.writeAndUnlock'); | ||
logger.debug(`Writing to file: ${filePath}`); | ||
try { | ||
await fs.promises.writeFile(filePath, data); | ||
} finally { | ||
await unlock(); | ||
} | ||
}, | ||
unlock, | ||
}; | ||
}; | ||
|
||
/** | ||
* prefer async {@link lockInit}. | ||
* See its documentation for details. | ||
*/ | ||
export const lockInitSync = (filePath: string): LockInitSyncResponse => { | ||
// make sure we can write to the directory | ||
try { | ||
fs.mkdirSync(dirname(filePath), { recursive: true }); | ||
} catch (err) { | ||
throw SfError.wrap(err as Error); | ||
} | ||
|
||
const [unlock] = fs.existsSync(filePath) | ||
? // if the file exists, wait for it to be unlocked | ||
[lockSync(filePath, lockRetryOptions)] | ||
: // lock the entire directory to keep others from trying to create the file while we are | ||
[ | ||
lockSync(dirname(filePath), lockRetryOptions), | ||
Logger.childFromRoot('fileLocking.lockInit').debug( | ||
`No file found at ${filePath}. Write will create it. Locking the entire directory until file is written.` | ||
), | ||
]; | ||
return { | ||
writeAndUnlock: (data: string): void => { | ||
const logger = Logger.childFromRoot('fileLocking.writeAndUnlock'); | ||
logger.debug(`Writing to file: ${filePath}`); | ||
try { | ||
fs.writeFileSync(filePath, data); | ||
} finally { | ||
unlock(); | ||
} | ||
}, | ||
unlock, | ||
}; | ||
}; |
5562a3d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logger Benchmarks - ubuntu-latest
Child logger creation
465143
ops/sec (±4.21%
)480719
ops/sec (±0.45%
)1.03
Logging a string on root logger
490019
ops/sec (±15.46%
)796183
ops/sec (±7.84%
)1.62
Logging an object on root logger
323162
ops/sec (±15.95%
)641942
ops/sec (±7.51%
)1.99
Logging an object with a message on root logger
187505
ops/sec (±18.26%
)6007
ops/sec (±212.70%
)0.03203647902722594
Logging an object with a redacted prop on root logger
169424
ops/sec (±22.56%
)455906
ops/sec (±7.35%
)2.69
Logging a nested 3-level object on root logger
136545
ops/sec (±21.98%
)389850
ops/sec (±7.23%
)2.86
This comment was automatically generated by workflow using github-action-benchmark.
5562a3d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible performance regression was detected for benchmark 'Logger Benchmarks - ubuntu-latest'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold
2
.Logging an object with a redacted prop on root logger
169424
ops/sec (±22.56%
)455906
ops/sec (±7.35%
)2.69
Logging a nested 3-level object on root logger
136545
ops/sec (±21.98%
)389850
ops/sec (±7.23%
)2.86
This comment was automatically generated by workflow using github-action-benchmark.
5562a3d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logger Benchmarks - windows-latest
Child logger creation
458326
ops/sec (±0.56%
)349164
ops/sec (±0.33%
)0.76
Logging a string on root logger
645321
ops/sec (±11.02%
)841907
ops/sec (±9.94%
)1.30
Logging an object on root logger
320024
ops/sec (±22.34%
)495433
ops/sec (±7.89%
)1.55
Logging an object with a message on root logger
181644
ops/sec (±21.24%
)13296
ops/sec (±187.05%
)0.07319812380260289
Logging an object with a redacted prop on root logger
213966
ops/sec (±21.44%
)397143
ops/sec (±9.03%
)1.86
Logging a nested 3-level object on root logger
122796
ops/sec (±24.61%
)293738
ops/sec (±5.51%
)2.39
This comment was automatically generated by workflow using github-action-benchmark.
5562a3d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible performance regression was detected for benchmark 'Logger Benchmarks - windows-latest'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold
2
.Logging a nested 3-level object on root logger
122796
ops/sec (±24.61%
)293738
ops/sec (±5.51%
)2.39
This comment was automatically generated by workflow using github-action-benchmark.