From 1227a6f190a8020afd953a26bdede86e5f04eb0c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 30 Apr 2023 22:53:54 +0200 Subject: [PATCH] fix: use homebrew async locking primitive to order file writing MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- package.json | 2 - pnpm-lock.yaml | 25 +--------- rollup.config.mjs | 1 - src/charging-station/ChargingStation.ts | 38 ++++++++++++---- src/performance/storage/JsonFileStorage.ts | 53 +++++++++++----------- src/utils/AsyncLock.ts | 44 ++++++++++++++++++ src/utils/index.ts | 2 + src/utils/internal.ts | 1 + 8 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 src/utils/AsyncLock.ts diff --git a/package.json b/package.json index f6c55269..d5ecb1f5 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "moment": "^2.29.4", "mongodb": "^5.3.0", "poolifier": "^2.4.11", - "proper-lockfile": "^4.1.2", "source-map-support": "^0.5.21", "tar": "^6.1.13", "tslib": "^2.5.0", @@ -121,7 +120,6 @@ "@types/mocha": "^10.0.1", "@types/mochawesome": "^6.2.1", "@types/node": "^18.16.3", - "@types/proper-lockfile": "^4.1.2", "@types/sinon": "^10.0.14", "@types/tar": "^6.1.4", "@types/ws": "^8.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcd22637..e306bcfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,9 +49,6 @@ dependencies: poolifier: specifier: ^2.4.11 version: 2.4.11 - proper-lockfile: - specifier: ^4.1.2 - version: 4.1.2 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -110,9 +107,6 @@ devDependencies: '@types/node': specifier: ^18.16.3 version: 18.16.3 - '@types/proper-lockfile': - specifier: ^4.1.2 - version: 4.1.2 '@types/sinon': specifier: ^10.0.14 version: 10.0.14 @@ -1501,22 +1495,12 @@ packages: resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} dev: true - /@types/proper-lockfile@4.1.2: - resolution: {integrity: sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==} - dependencies: - '@types/retry': 0.12.2 - dev: true - /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: '@types/node': 18.16.3 dev: true - /@types/retry@0.12.2: - resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} - dev: true - /@types/seedrandom@2.4.30: resolution: {integrity: sha512-AnxLHewubLVzoF/A4qdxBGHCKifw8cY32iro3DQX9TPcetE95zBeVt3jnsvtvAUf1vwzMfwzp4t/L2yqPlnjkQ==} dev: true @@ -7909,14 +7893,6 @@ packages: react-is: 16.13.1 dev: true - /proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - dev: false - /proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} dev: true @@ -8421,6 +8397,7 @@ packages: /retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + optional: true /retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} diff --git a/rollup.config.mjs b/rollup.config.mjs index 0b43e8f8..a251ff5d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -47,7 +47,6 @@ export default { 'node:util', 'node:worker_threads', 'poolifier', - 'proper-lockfile', 'tar', 'winston', 'winston-daily-rotate-file', diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 436d7b02..9855c8ca 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -91,6 +91,8 @@ import { } from '../types'; import { ACElectricUtils, + AsyncLock, + AsyncLockType, Configuration, Constants, DCElectricUtils, @@ -1592,16 +1594,32 @@ export class ChargingStation { .update(JSON.stringify(configurationData)) .digest('hex'); if (this.configurationFileHash !== configurationHash) { - configurationData.configurationHash = configurationHash; - const measureId = `${FileType.ChargingStationConfiguration} write`; - const beginId = PerformanceStatistics.beginMeasure(measureId); - const fileDescriptor = fs.openSync(this.configurationFile, 'w'); - fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8'); - fs.closeSync(fileDescriptor); - PerformanceStatistics.endMeasure(measureId, beginId); - this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash); - this.sharedLRUCache.setChargingStationConfiguration(configurationData); - this.configurationFileHash = configurationHash; + const asyncLock = AsyncLock.getInstance(AsyncLockType.configuration); + asyncLock + .acquire() + .then(() => { + configurationData.configurationHash = configurationHash; + const measureId = `${FileType.ChargingStationConfiguration} write`; + const beginId = PerformanceStatistics.beginMeasure(measureId); + const fileDescriptor = fs.openSync(this.configurationFile, 'w'); + fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8'); + fs.closeSync(fileDescriptor); + PerformanceStatistics.endMeasure(measureId, beginId); + this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash); + this.sharedLRUCache.setChargingStationConfiguration(configurationData); + this.configurationFileHash = configurationHash; + }) + .catch((error) => { + FileUtils.handleFileException( + this.configurationFile, + FileType.ChargingStationConfiguration, + error as NodeJS.ErrnoException, + this.logPrefix() + ); + }) + .finally(() => { + asyncLock.release().catch(Constants.EMPTY_FUNCTION); + }); } else { logger.debug( `${this.logPrefix()} Not saving unchanged charging station configuration file ${ diff --git a/src/performance/storage/JsonFileStorage.ts b/src/performance/storage/JsonFileStorage.ts index fcb35a4d..5effae90 100644 --- a/src/performance/storage/JsonFileStorage.ts +++ b/src/performance/storage/JsonFileStorage.ts @@ -2,10 +2,8 @@ import fs from 'node:fs'; -import lockfile from 'proper-lockfile'; - import { FileType, type Statistics } from '../../types'; -import { Constants, FileUtils, Utils } from '../../utils'; +import { AsyncLock, AsyncLockType, Constants, FileUtils, Utils } from '../../utils'; import { Storage } from '../internal'; export class JsonFileStorage extends Storage { @@ -18,31 +16,32 @@ export class JsonFileStorage extends Storage { public storePerformanceStatistics(performanceStatistics: Statistics): void { this.checkPerformanceRecordsFile(); - lockfile - .lock(this.dbName, { stale: 5000, retries: 3 }) - .then(async (release) => { - try { - const fileData = fs.readFileSync(this.dbName, 'utf8'); - const performanceRecords: Statistics[] = fileData - ? (JSON.parse(fileData) as Statistics[]) - : []; - performanceRecords.push(performanceStatistics); - fs.writeFileSync( - this.dbName, - Utils.JSONStringifyWithMapSupport(performanceRecords, 2), - 'utf8' - ); - } catch (error) { - FileUtils.handleFileException( - this.dbName, - FileType.PerformanceRecords, - error as NodeJS.ErrnoException, - this.logPrefix - ); - } - await release(); + const asyncLock = AsyncLock.getInstance(AsyncLockType.performance); + asyncLock + .acquire() + .then(() => { + const fileData = fs.readFileSync(this.dbName, 'utf8'); + const performanceRecords: Statistics[] = fileData + ? (JSON.parse(fileData) as Statistics[]) + : []; + performanceRecords.push(performanceStatistics); + fs.writeFileSync( + this.dbName, + Utils.JSONStringifyWithMapSupport(performanceRecords, 2), + 'utf8' + ); + }) + .catch((error) => { + FileUtils.handleFileException( + this.dbName, + FileType.PerformanceRecords, + error as NodeJS.ErrnoException, + this.logPrefix + ); }) - .catch(Constants.EMPTY_FUNCTION); + .finally(() => { + asyncLock.release().catch(Constants.EMPTY_FUNCTION); + }); } public open(): void { diff --git a/src/utils/AsyncLock.ts b/src/utils/AsyncLock.ts new file mode 100644 index 00000000..1b615325 --- /dev/null +++ b/src/utils/AsyncLock.ts @@ -0,0 +1,44 @@ +export enum AsyncLockType { + configuration = 'configuration', + performance = 'performance', +} + +export class AsyncLock { + private static readonly instances = new Map(); + private acquired = false; + private readonly resolveQueue: ((value: void | PromiseLike) => void)[]; + + private constructor(private readonly type: AsyncLockType) { + this.acquired = false; + this.resolveQueue = []; + } + + public static getInstance(type: AsyncLockType): AsyncLock { + if (!AsyncLock.instances.has(type)) { + AsyncLock.instances.set(type, new AsyncLock(type)); + } + return AsyncLock.instances.get(type); + } + + public async acquire(): Promise { + if (!this.acquired) { + this.acquired = true; + } else { + return new Promise((resolve) => { + this.resolveQueue.push(resolve); + }); + } + } + + public async release(): Promise { + if (this.resolveQueue.length === 0 && this.acquired) { + this.acquired = false; + return; + } + const queuedResolve = this.resolveQueue.shift(); + return new Promise((resolve) => { + queuedResolve(); + resolve(); + }); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index ff5de5a1..6cb58d83 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,7 @@ export { ACElectricUtils, + AsyncLock, + AsyncLockType, CircularArray, Configuration, Constants, diff --git a/src/utils/internal.ts b/src/utils/internal.ts index 59a2f6df..f727257b 100644 --- a/src/utils/internal.ts +++ b/src/utils/internal.ts @@ -1,3 +1,4 @@ +export * from './AsyncLock'; export * from './CircularArray'; export * from './Constants'; export * from './ElectricUtils'; -- 2.34.1