Add a shared cache per worker for authorized tags
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 10 Jun 2022 14:48:55 +0000 (16:48 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 10 Jun 2022 14:48:55 +0000 (16:48 +0200)
And fix some file changes watch leak

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/AuthorizedTagsCache.ts [new file with mode: 0644]
src/charging-station/ChargingStation.ts
src/charging-station/ChargingStationUtils.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/utils/FileUtils.ts

diff --git a/src/charging-station/AuthorizedTagsCache.ts b/src/charging-station/AuthorizedTagsCache.ts
new file mode 100644 (file)
index 0000000..4a9da4c
--- /dev/null
@@ -0,0 +1,110 @@
+import { FileType } from '../types/FileType';
+import FileUtils from '../utils/FileUtils';
+import Utils from '../utils/Utils';
+import fs from 'fs';
+import logger from '../utils/Logger';
+
+export default class AuthorizedTagsCache {
+  private static instance: AuthorizedTagsCache | null = null;
+  private readonly tagsCaches: Map<string, string[]>;
+  private readonly FSWatchers: Map<string, fs.FSWatcher>;
+
+  private constructor() {
+    this.tagsCaches = new Map<string, string[]>();
+    this.FSWatchers = new Map<string, fs.FSWatcher>();
+  }
+
+  public static getInstance(): AuthorizedTagsCache {
+    if (!AuthorizedTagsCache.instance) {
+      AuthorizedTagsCache.instance = new AuthorizedTagsCache();
+    }
+    return AuthorizedTagsCache.instance;
+  }
+
+  public getAuthorizedTags(file: string): string[] {
+    if (!this.hasTags(file)) {
+      this.setTags(file, this.getAuthorizedTagsFromFile(file));
+      // Monitor authorization file
+      !this.FSWatchers.has(file) &&
+        this.FSWatchers.set(
+          file,
+          FileUtils.watchJsonFile(
+            this.logPrefix(file),
+            FileType.Authorization,
+            file,
+            null,
+            (event, filename) => {
+              if (filename && event === 'change') {
+                try {
+                  logger.debug(
+                    this.logPrefix(file) +
+                      ' ' +
+                      FileType.Authorization +
+                      ' file have changed, reload'
+                  );
+                  this.deleteTags(file);
+                  this.deleteFSWatcher(file);
+                } catch (error) {
+                  FileUtils.handleFileException(
+                    this.logPrefix(file),
+                    FileType.Authorization,
+                    file,
+                    error as NodeJS.ErrnoException,
+                    {
+                      throwError: false,
+                    }
+                  );
+                }
+              }
+            }
+          )
+        );
+    }
+    return this.getTags(file);
+  }
+
+  private hasTags(file: string): boolean {
+    return this.tagsCaches.has(file);
+  }
+
+  private setTags(file: string, tags: string[]) {
+    return this.tagsCaches.set(file, tags);
+  }
+
+  private getTags(file: string): string[] {
+    return this.tagsCaches.get(file);
+  }
+
+  private deleteTags(file: string): boolean {
+    return this.tagsCaches.delete(file);
+  }
+
+  private deleteFSWatcher(file: string): boolean {
+    this.FSWatchers.get(file).close();
+    return this.FSWatchers.delete(file);
+  }
+
+  private getAuthorizedTagsFromFile(file: string): string[] {
+    let authorizedTags: string[] = [];
+    if (file) {
+      try {
+        // Load authorization file
+        authorizedTags = JSON.parse(fs.readFileSync(file, 'utf8')) as string[];
+      } catch (error) {
+        FileUtils.handleFileException(
+          this.logPrefix(file),
+          FileType.Authorization,
+          file,
+          error as NodeJS.ErrnoException
+        );
+      }
+    } else {
+      logger.info(this.logPrefix(file) + ' No authorization file given)');
+    }
+    return authorizedTags;
+  }
+
+  private logPrefix(file: string): string {
+    return Utils.logPrefix(` Authorized tags cache for authorization file '${file}' |`);
+  }
+}
index e9f31682924de1491781211713da9068b8bb873e..80735db6c7449551b519c20a6d1fec866463cbdb 100644 (file)
@@ -43,6 +43,7 @@ import { URL, fileURLToPath } from 'url';
 import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket';
 import WebSocket, { Data, RawData } from 'ws';
 
+import AuthorizedTagsCache from './AuthorizedTagsCache';
 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
 import { AutomaticTransactionGeneratorConfiguration } from '../types/AutomaticTransactionGenerator';
 import BaseError from '../exception/BaseError';
@@ -83,7 +84,7 @@ import path from 'path';
 export default class ChargingStation {
   public hashId!: string;
   public readonly templateFile: string;
-  public authorizedTags!: string[];
+  public authorizedTagsCache: AuthorizedTagsCache;
   public stationInfo!: ChargingStationInfo;
   public readonly connectors: Map<number, ConnectorStatus>;
   public ocppConfiguration!: ChargingStationOcppConfiguration;
@@ -105,6 +106,7 @@ export default class ChargingStation {
   private wsConnectionRestarted: boolean;
   private autoReconnectRetryCount: number;
   private stopped: boolean;
+  private templateFileWatcher!: fs.FSWatcher;
   private readonly cache: ChargingStationCache;
   private automaticTransactionGenerator!: AutomaticTransactionGenerator;
   private webSocketPingSetInterval!: NodeJS.Timeout;
@@ -116,6 +118,7 @@ export default class ChargingStation {
     this.wsConnectionRestarted = false;
     this.autoReconnectRetryCount = 0;
     this.cache = ChargingStationCache.getInstance();
+    this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
     this.connectors = new Map<number, ConnectorStatus>();
     this.requests = new Map<string, CachedRequest>();
     this.messageBuffer = new Set<string>();
@@ -149,12 +152,19 @@ export default class ChargingStation {
   }
 
   public getRandomIdTag(): string {
-    const index = Math.floor(Utils.secureRandom() * this.authorizedTags.length);
-    return this.authorizedTags[index];
+    const authorizationFile = ChargingStationUtils.getAuthorizationFile(this.stationInfo);
+    const index = Math.floor(
+      Utils.secureRandom() * this.authorizedTagsCache.getAuthorizedTags(authorizationFile).length
+    );
+    return this.authorizedTagsCache.getAuthorizedTags(authorizationFile)[index];
   }
 
   public hasAuthorizedTags(): boolean {
-    return !Utils.isEmptyArray(this.authorizedTags);
+    return !Utils.isEmptyArray(
+      this.authorizedTagsCache.getAuthorizedTags(
+        ChargingStationUtils.getAuthorizationFile(this.stationInfo)
+      )
+    );
   }
 
   public getEnableStatistics(): boolean | undefined {
@@ -490,15 +500,8 @@ export default class ChargingStation {
     this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
     // Handle WebSocket pong
     this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
-    // Monitor authorization file
-    FileUtils.watchJsonFile<string[]>(
-      this.logPrefix(),
-      FileType.Authorization,
-      ChargingStationUtils.getAuthorizationFile(this.stationInfo),
-      this.authorizedTags
-    );
     // Monitor charging station template file
-    FileUtils.watchJsonFile(
+    this.templateFileWatcher = FileUtils.watchJsonFile(
       this.logPrefix(),
       FileType.ChargingStationTemplate,
       this.templateFile,
@@ -559,6 +562,7 @@ export default class ChargingStation {
       this.performanceStatistics.stop();
     }
     this.cache.deleteChargingStationConfiguration(this.configurationFileHash);
+    this.templateFileWatcher.close();
     this.cache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
     this.bootNotificationResponse = null;
     parentPort.postMessage({
@@ -897,11 +901,6 @@ export default class ChargingStation {
     this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
       this.stationInfo
     );
-    this.authorizedTags = ChargingStationUtils.getAuthorizedTags(
-      this.stationInfo,
-      this.templateFile,
-      this.logPrefix()
-    );
     this.powerDivider = this.getPowerDivider();
     // OCPP configuration
     this.ocppConfiguration = this.getOcppConfiguration();
index ffe90a776167d0e5535cce7b2057e5c5fe935528..964c606b0d2a51234c5cfea1af844bb7ec929bbc 100644 (file)
@@ -14,8 +14,6 @@ import { ChargingStationConfigurationUtils } from './ChargingStationConfiguratio
 import ChargingStationInfo from '../types/ChargingStationInfo';
 import Configuration from '../utils/Configuration';
 import Constants from '../utils/Constants';
-import { FileType } from '../types/FileType';
-import FileUtils from '../utils/FileUtils';
 import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
 import { StandardParametersKey } from '../types/ocpp/Configuration';
 import Utils from '../utils/Utils';
@@ -23,7 +21,6 @@ import { WebSocketCloseEventStatusString } from '../types/WebSocket';
 import { WorkerProcessType } from '../types/Worker';
 import crypto from 'crypto';
 import { fileURLToPath } from 'url';
-import fs from 'fs';
 import logger from '../utils/Logger';
 import moment from 'moment';
 import path from 'path';
@@ -517,31 +514,6 @@ export class ChargingStationUtils {
     );
   }
 
-  public static getAuthorizedTags(
-    stationInfo: ChargingStationInfo,
-    templateFile: string,
-    logPrefix: string
-  ): string[] {
-    let authorizedTags: string[] = [];
-    const authorizationFile = ChargingStationUtils.getAuthorizationFile(stationInfo);
-    if (authorizationFile) {
-      try {
-        // Load authorization file
-        authorizedTags = JSON.parse(fs.readFileSync(authorizationFile, 'utf8')) as string[];
-      } catch (error) {
-        FileUtils.handleFileException(
-          logPrefix,
-          FileType.Authorization,
-          authorizationFile,
-          error as NodeJS.ErrnoException
-        );
-      }
-    } else {
-      logger.info(logPrefix + ' No authorization file given in template file ' + templateFile);
-    }
-    return authorizedTags;
-  }
-
   public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
     return (
       stationInfo.authorizationFile &&
index 67967fd9718d7a4ac7bb38896b8537d8bf212013..7aad17a15e9a95d2ee3cfb173a89296d86cb1d06 100644 (file)
@@ -62,6 +62,7 @@ import { URL, fileURLToPath } from 'url';
 
 import type ChargingStation from '../../ChargingStation';
 import { ChargingStationConfigurationUtils } from '../../ChargingStationConfigurationUtils';
+import { ChargingStationUtils } from '../../ChargingStationUtils';
 import Constants from '../../../utils/Constants';
 import { DefaultResponse } from '../../../types/ocpp/Responses';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
@@ -614,7 +615,11 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
           if (
             chargingStation.getLocalAuthListEnabled() &&
             chargingStation.hasAuthorizedTags() &&
-            chargingStation.authorizedTags.find((value) => value === commandPayload.idTag)
+            chargingStation.authorizedTagsCache
+              .getAuthorizedTags(
+                ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo)
+              )
+              .find((value) => value === commandPayload.idTag)
           ) {
             connectorStatus.localAuthorizeIdTag = commandPayload.idTag;
             connectorStatus.idTagLocalAuthorized = true;
index b057d7f7e844b4684f823903bc22a60f98e200dd..f38bde6c8ea66218887438eefa23832c7e6aa0e9 100644 (file)
@@ -8,16 +8,16 @@ import fs from 'fs';
 import logger from './Logger';
 
 export default class FileUtils {
-  static watchJsonFile<T extends JsonType>(
+  public static watchJsonFile<T extends JsonType>(
     logPrefix: string,
     fileType: FileType,
     file: string,
-    attribute?: T,
+    refreshedVariable?: T,
     listener: fs.WatchListener<string> = (event, filename) => {
       if (filename && event === 'change') {
         try {
           logger.debug(logPrefix + ' ' + fileType + ' file ' + file + ' have changed, reload');
-          attribute && (attribute = JSON.parse(fs.readFileSync(file, 'utf8')) as T);
+          refreshedVariable && (refreshedVariable = JSON.parse(fs.readFileSync(file, 'utf8')) as T);
         } catch (error) {
           FileUtils.handleFileException(logPrefix, fileType, file, error as NodeJS.ErrnoException, {
             throwError: false,
@@ -25,10 +25,10 @@ export default class FileUtils {
         }
       }
     }
-  ) {
+  ): fs.FSWatcher {
     if (file) {
       try {
-        fs.watch(file, listener);
+        return fs.watch(file, listener);
       } catch (error) {
         FileUtils.handleFileException(logPrefix, fileType, file, error as NodeJS.ErrnoException, {
           throwError: false,
@@ -39,7 +39,7 @@ export default class FileUtils {
     }
   }
 
-  static handleFileException(
+  public static handleFileException(
     logPrefix: string,
     fileType: FileType,
     file: string,