From 9d7484a4667898757b7c23be3dec7458c337cb84 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 10 Jun 2022 16:48:55 +0200 Subject: [PATCH] Add a shared cache per worker for authorized tags MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit And fix some file changes watch leak Signed-off-by: Jérôme Benoit --- src/charging-station/AuthorizedTagsCache.ts | 110 ++++++++++++++++++ src/charging-station/ChargingStation.ts | 33 +++--- src/charging-station/ChargingStationUtils.ts | 28 ----- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 7 +- src/utils/FileUtils.ts | 12 +- 5 files changed, 138 insertions(+), 52 deletions(-) create mode 100644 src/charging-station/AuthorizedTagsCache.ts diff --git a/src/charging-station/AuthorizedTagsCache.ts b/src/charging-station/AuthorizedTagsCache.ts new file mode 100644 index 00000000..4a9da4ce --- /dev/null +++ b/src/charging-station/AuthorizedTagsCache.ts @@ -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; + private readonly FSWatchers: Map; + + private constructor() { + this.tagsCaches = new Map(); + this.FSWatchers = new Map(); + } + + 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}' |`); + } +} diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index e9f31682..80735db6 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -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; 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(); this.requests = new Map(); this.messageBuffer = new Set(); @@ -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( - 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(); diff --git a/src/charging-station/ChargingStationUtils.ts b/src/charging-station/ChargingStationUtils.ts index ffe90a77..964c606b 100644 --- a/src/charging-station/ChargingStationUtils.ts +++ b/src/charging-station/ChargingStationUtils.ts @@ -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 && diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 67967fd9..7aad17a1 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -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; diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index b057d7f7..f38bde6c 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -8,16 +8,16 @@ import fs from 'fs'; import logger from './Logger'; export default class FileUtils { - static watchJsonFile( + public static watchJsonFile( logPrefix: string, fileType: FileType, file: string, - attribute?: T, + refreshedVariable?: T, listener: fs.WatchListener = (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, -- 2.34.1