fix: use homebrew async locking primitive to order file writing
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 30 Apr 2023 20:53:54 +0000 (22:53 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 30 Apr 2023 20:53:54 +0000 (22:53 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
package.json
pnpm-lock.yaml
rollup.config.mjs
src/charging-station/ChargingStation.ts
src/performance/storage/JsonFileStorage.ts
src/utils/AsyncLock.ts [new file with mode: 0644]
src/utils/index.ts
src/utils/internal.ts

index f6c55269fb3511779f04676951424de319a6dee9..d5ecb1f57ffaa3030c19a9d06652ca747d2b7d52 100644 (file)
@@ -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",
     "@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",
index dcd22637ea6a9bd4e0228e5f01a72c1b23eece10..e306bcfe881fa95b979a6ff6892d141fa278d224 100644 (file)
@@ -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==}
index 0b43e8f8d3677921ab5fc3d8054e48dbaaac2382..a251ff5dab1ce283387aaac2246466b27be669ee 100644 (file)
@@ -47,7 +47,6 @@ export default {
     'node:util',
     'node:worker_threads',
     'poolifier',
-    'proper-lockfile',
     'tar',
     'winston',
     'winston-daily-rotate-file',
index 436d7b02fd3f3024807108e2c7aa307d5b813f96..9855c8cabdab3fca7242f5122a99ca99831b2cc9 100644 (file)
@@ -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 ${
index fcb35a4df0507569d941edbcb1aac223561e6be4..5effae906dc6965d3d60893c801d5e6b465e8d4d 100644 (file)
@@ -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 (file)
index 0000000..1b61532
--- /dev/null
@@ -0,0 +1,44 @@
+export enum AsyncLockType {
+  configuration = 'configuration',
+  performance = 'performance',
+}
+
+export class AsyncLock {
+  private static readonly instances = new Map<AsyncLockType, AsyncLock>();
+  private acquired = false;
+  private readonly resolveQueue: ((value: void | PromiseLike<void>) => 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<void> {
+    if (!this.acquired) {
+      this.acquired = true;
+    } else {
+      return new Promise((resolve) => {
+        this.resolveQueue.push(resolve);
+      });
+    }
+  }
+
+  public async release(): Promise<void> {
+    if (this.resolveQueue.length === 0 && this.acquired) {
+      this.acquired = false;
+      return;
+    }
+    const queuedResolve = this.resolveQueue.shift();
+    return new Promise((resolve) => {
+      queuedResolve();
+      resolve();
+    });
+  }
+}
index ff5de5a1d9e72bae5c1d48fcb9be721fc18c006d..6cb58d8328fcbac8dcef088aa9ee85c6d65080cb 100644 (file)
@@ -1,5 +1,7 @@
 export {
   ACElectricUtils,
+  AsyncLock,
+  AsyncLockType,
   CircularArray,
   Configuration,
   Constants,
index 59a2f6df509df34026e9d4f6ee70209fdbfb54ab..f727257b76039564ff09be8ce5a210031957f080 100644 (file)
@@ -1,3 +1,4 @@
+export * from './AsyncLock';
 export * from './CircularArray';
 export * from './Constants';
 export * from './ElectricUtils';