-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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, | ||
}; | ||
}; |
29c59e0
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
699481
ops/sec (±1.56%
)480471
ops/sec (±1.80%
)0.69
Logging a string on root logger
791610
ops/sec (±5.90%
)830939
ops/sec (±5.86%
)1.05
Logging an object on root logger
603986
ops/sec (±7.67%
)508485
ops/sec (±7.87%
)0.84
Logging an object with a message on root logger
8806
ops/sec (±205.56%
)15488
ops/sec (±187.13%
)1.76
Logging an object with a redacted prop on root logger
432979
ops/sec (±9.52%
)376461
ops/sec (±7.28%
)0.87
Logging a nested 3-level object on root logger
365611
ops/sec (±9.13%
)328781
ops/sec (±7.64%
)0.90
This comment was automatically generated by workflow using github-action-benchmark.
29c59e0
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
587812
ops/sec (±0.54%
)348225
ops/sec (±0.35%
)0.59
Logging a string on root logger
763983
ops/sec (±6.94%
)825105
ops/sec (±6.01%
)1.08
Logging an object on root logger
513955
ops/sec (±9.03%
)652905
ops/sec (±7.64%
)1.27
Logging an object with a message on root logger
294241
ops/sec (±10.56%
)23494
ops/sec (±184.38%
)0.07984611254040055
Logging an object with a redacted prop on root logger
2627
ops/sec (±275.01%
)476442
ops/sec (±6.37%
)181.36
Logging a nested 3-level object on root logger
274455
ops/sec (±6.14%
)328575
ops/sec (±5.44%
)1.20
This comment was automatically generated by workflow using github-action-benchmark.
29c59e0
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 an object with a redacted prop on root logger
2627
ops/sec (±275.01%
)476442
ops/sec (±6.37%
)181.36
This comment was automatically generated by workflow using github-action-benchmark.