From c72f6634184bc83a6476152446ac9f0d7d02acf5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 25 Sep 2022 22:57:31 +0200 Subject: [PATCH] ATG: add support for idTag distribution algorithms MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Close #179 Signed-off-by: Jérôme Benoit --- README.md | 24 ++++++++- .../AutomaticTransactionGenerator.ts | 47 +++++++++++++++-- src/charging-station/Bootstrap.ts | 3 +- src/charging-station/ChargingStation.ts | 52 +++++++------------ src/charging-station/ChargingStationUtils.ts | 13 ++--- src/charging-station/ChargingStationWorker.ts | 11 ++-- src/charging-station/MessageChannelUtils.ts | 10 ++-- src/types/AutomaticTransactionGenerator.ts | 9 +++- src/types/ChargingStationWorker.ts | 9 ++-- src/types/ConfigurationData.ts | 2 +- src/types/Worker.ts | 2 +- 11 files changed, 118 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 927aaffd..b21f6a3b 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ But the modifications to test have to be done to the files in the build target d | Key | Value(s) | Default Value | Value type | Description | | -------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | supervisionUrls | | [] | string \| string[] | string or array of global connection URIs to OCPP-J servers | -| supervisionUrlDistribution | round-robin/random/sequential | round-robin | boolean | supervision urls distribution policy to simulated charging stations | +| supervisionUrlDistribution | round-robin/random/charging-station-affinity | charging-station-affinity | boolean | supervision urls distribution policy to simulated charging stations | | logStatisticsInterval | | 60 | integer | seconds between charging stations statistics output in the logs | | logConsole | true/false | false | boolean | output logs on the console | | logFormat | | simple | string | winston log format | @@ -201,6 +201,25 @@ But the modifications to test have to be done to the files in the build target d #### AutomaticTransactionGenerator section +Section type definition: + +```ts +type AutomaticTransactionGeneratorConfiguration = { + enable: boolean; + minDuration: number; + maxDuration: number; + minDelayBetweenTwoTransactions: number; + maxDelayBetweenTwoTransactions: number; + probabilityOfStart: number; + stopAfterHours: number; + stopOnConnectionFailure: boolean; + requireAuthorize?: boolean; + idTagDistribution?: 'random' | 'round-robin' | 'connector-affinity'; +}; +``` + +Section example: + ```json "AutomaticTransactionGenerator": { "enable": false, @@ -211,7 +230,8 @@ But the modifications to test have to be done to the files in the build target d "probabilityOfStart": 1, "stopAfterHours": 0.3, "stopOnConnectionFailure": true, - "requireAuthorize": true + "requireAuthorize": true, + "idTagDistribution": "random" } ``` diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index 3afb86d4..9b196ed9 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -2,9 +2,10 @@ import BaseError from '../exception/BaseError'; import PerformanceStatistics from '../performance/PerformanceStatistics'; -import type { - AutomaticTransactionGeneratorConfiguration, - Status, +import { + type AutomaticTransactionGeneratorConfiguration, + IdTagDistribution, + type Status, } from '../types/AutomaticTransactionGenerator'; import { RequestCommand } from '../types/ocpp/Requests'; import { @@ -20,6 +21,7 @@ import Constants from '../utils/Constants'; import logger from '../utils/Logger'; import Utils from '../utils/Utils'; import type ChargingStation from './ChargingStation'; +import { ChargingStationUtils } from './ChargingStationUtils'; export default class AutomaticTransactionGenerator { private static readonly instances: Map = new Map< @@ -31,6 +33,7 @@ export default class AutomaticTransactionGenerator { public readonly configuration: AutomaticTransactionGeneratorConfiguration; public started: boolean; private readonly chargingStation: ChargingStation; + private idTagIndex: number; private constructor( automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration, @@ -39,6 +42,7 @@ export default class AutomaticTransactionGenerator { this.started = false; this.configuration = automaticTransactionGeneratorConfiguration; this.chargingStation = chargingStation; + this.idTagIndex = 0; this.connectorsStatus = new Map(); this.initializeConnectorsStatus(); } @@ -313,7 +317,7 @@ export default class AutomaticTransactionGenerator { const beginId = PerformanceStatistics.beginMeasure(measureId); let startResponse: StartTransactionResponse; if (this.chargingStation.hasAuthorizedTags()) { - const idTag = this.chargingStation.getRandomIdTag(); + const idTag = this.getIdTag(connectorId); const startTransactionLogMsg = `${this.logPrefix( connectorId )} start transaction with an idTag '${idTag}'`; @@ -401,6 +405,41 @@ export default class AutomaticTransactionGenerator { return this.configuration?.requireAuthorize ?? true; } + private getRandomIdTag(authorizationFile: string): string { + const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile); + this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length); + return tags[this.idTagIndex]; + } + + private getRoundRobinIdTag(authorizationFile: string): string { + const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile); + const idTag = tags[this.idTagIndex]; + this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1; + return idTag; + } + + private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string { + const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile); + this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length; + return tags[this.idTagIndex]; + } + + private getIdTag(connectorId: number): string { + const authorizationFile = ChargingStationUtils.getAuthorizationFile( + this.chargingStation.stationInfo + ); + switch (this.configuration?.idTagDistribution) { + case IdTagDistribution.RANDOM: + return this.getRandomIdTag(authorizationFile); + case IdTagDistribution.ROUND_ROBIN: + return this.getRoundRobinIdTag(authorizationFile); + case IdTagDistribution.CONNECTOR_AFFINITY: + return this.getConnectorAffinityIdTag(authorizationFile, connectorId); + default: + return this.getRoundRobinIdTag(authorizationFile); + } + } + private logPrefix(connectorId?: number): string { return Utils.logPrefix( ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${ diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index 7c80067c..cfb4eed6 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -2,7 +2,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; -import { isMainThread } from 'worker_threads'; +import { type Worker, isMainThread } from 'worker_threads'; import chalk from 'chalk'; @@ -177,6 +177,7 @@ export class Bootstrap { workerChoiceStrategy: Configuration.getWorker().poolStrategy, }, messageHandler: this.messageHandler.bind(this) as ( + this: Worker, msg: ChargingStationWorkerMessage ) => void, } diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 90bba2ef..9fabd2a5 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -85,6 +85,7 @@ import type OCPPRequestService from './ocpp/OCPPRequestService'; import SharedLRUCache from './SharedLRUCache'; export default class ChargingStation { + public readonly index: number; public readonly templateFile: string; public stationInfo!: ChargingStationInfo; public started: boolean; @@ -102,13 +103,13 @@ export default class ChargingStation { public powerDivider!: number; private starting: boolean; private stopping: boolean; - private readonly index: number; private configurationFile!: string; private configurationFileHash!: string; private connectorsConfigurationHash!: string; private ocppIncomingRequestService!: OCPPIncomingRequestService; private readonly messageBuffer: Set; private configuredSupervisionUrl!: URL; + private configuredSupervisionUrlIndex!: number; private wsConnectionRestarted: boolean; private autoReconnectRetryCount: number; private templateFileWatcher!: fs.FSWatcher; @@ -156,14 +157,6 @@ export default class ChargingStation { ); } - public getRandomIdTag(): string { - 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.authorizedTagsCache.getAuthorizedTags( @@ -880,10 +873,8 @@ export default class ChargingStation { stationInfo.resetTime = stationTemplate.resetTime ? stationTemplate.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME; - const configuredMaxConnectors = ChargingStationUtils.getConfiguredNumberOfConnectors( - this.index, - stationTemplate - ); + const configuredMaxConnectors = + ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate); ChargingStationUtils.checkConfiguredMaxConnectors( configuredMaxConnectors, this.templateFile, @@ -1645,7 +1636,7 @@ export default class ChargingStation { } // 0 for disabling - private getConnectionTimeout(): number | undefined { + private getConnectionTimeout(): number { if ( ChargingStationConfigurationUtils.getConfigurationKey( this, @@ -1665,7 +1656,7 @@ export default class ChargingStation { } // -1 for unlimited, 0 for disabling - private getAutoReconnectMaxRetries(): number | undefined { + private getAutoReconnectMaxRetries(): number { if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) { return this.stationInfo.autoReconnectMaxRetries; } @@ -1676,7 +1667,7 @@ export default class ChargingStation { } // 0 for disabling - private getRegistrationMaxRetries(): number | undefined { + private getRegistrationMaxRetries(): number { if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) { return this.stationInfo.registrationMaxRetries; } @@ -1928,39 +1919,34 @@ export default class ChargingStation { this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls() ); if (!Utils.isEmptyArray(supervisionUrls)) { - let urlIndex = 0; switch (Configuration.getSupervisionUrlDistribution()) { case SupervisionUrlDistribution.ROUND_ROBIN: - urlIndex = (this.index - 1) % supervisionUrls.length; + // FIXME + this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length; break; case SupervisionUrlDistribution.RANDOM: - // Get a random url - urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length); + this.configuredSupervisionUrlIndex = Math.floor( + Utils.secureRandom() * supervisionUrls.length + ); break; - case SupervisionUrlDistribution.SEQUENTIAL: - if (this.index <= supervisionUrls.length) { - urlIndex = this.index - 1; - } else { - logger.warn( - `${this.logPrefix()} No more configured supervision urls available, using the first one` - ); - } + case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY: + this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length; break; default: logger.error( `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${ - SupervisionUrlDistribution.ROUND_ROBIN + SupervisionUrlDistribution.CHARGING_STATION_AFFINITY }` ); - urlIndex = (this.index - 1) % supervisionUrls.length; + this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length; break; } - return new URL(supervisionUrls[urlIndex]); + return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]); } return new URL(supervisionUrls as string); } - private getHeartbeatInterval(): number | undefined { + private getHeartbeatInterval(): number { const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey( this, StandardParametersKey.HeartbeatInterval @@ -2003,7 +1989,7 @@ export default class ChargingStation { } } - private getReconnectExponentialDelay(): boolean | undefined { + private getReconnectExponentialDelay(): boolean { return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay) ? this.stationInfo.reconnectExponentialDelay : false; diff --git a/src/charging-station/ChargingStationUtils.ts b/src/charging-station/ChargingStationUtils.ts index 370e030c..24556120 100644 --- a/src/charging-station/ChargingStationUtils.ts +++ b/src/charging-station/ChargingStationUtils.ts @@ -109,16 +109,13 @@ export class ChargingStationUtils { } } - public static getConfiguredNumberOfConnectors( - index: number, - stationTemplate: ChargingStationTemplate - ): number { + public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number { let configuredMaxConnectors: number; - if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) { + if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) { const numberOfConnectors = stationTemplate.numberOfConnectors as number[]; - // Distribute evenly the number of connectors - configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length]; - } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) { + configuredMaxConnectors = + numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)]; + } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) { configuredMaxConnectors = stationTemplate.numberOfConnectors as number; } else { configuredMaxConnectors = stationTemplate?.Connectors[0] diff --git a/src/charging-station/ChargingStationWorker.ts b/src/charging-station/ChargingStationWorker.ts index 4f109bf5..58f1327a 100644 --- a/src/charging-station/ChargingStationWorker.ts +++ b/src/charging-station/ChargingStationWorker.ts @@ -4,11 +4,8 @@ import { parentPort, workerData } from 'worker_threads'; import { ThreadWorker } from 'poolifier'; -import { - ChargingStationWorkerData, - ChargingStationWorkerMessage, - ChargingStationWorkerMessageEvents, -} from '../types/ChargingStationWorker'; +import type { ChargingStationWorkerData } from '../types/ChargingStationWorker'; +import { WorkerMessage, WorkerMessageEvents } from '../types/Worker'; import Utils from '../utils/Utils'; import WorkerConstants from '../worker/WorkerConstants'; import ChargingStation from './ChargingStation'; @@ -33,8 +30,8 @@ if (ChargingStationUtils.workerPoolInUse()) { * Listen messages send by the main thread */ function addMessageListener(): void { - parentPort?.on('message', (message: ChargingStationWorkerMessage) => { - if (message.id === ChargingStationWorkerMessageEvents.START_WORKER_ELEMENT) { + parentPort?.on('message', (message: WorkerMessage) => { + if (message.id === WorkerMessageEvents.START_WORKER_ELEMENT) { startChargingStation(message.data); } }); diff --git a/src/charging-station/MessageChannelUtils.ts b/src/charging-station/MessageChannelUtils.ts index fe11041a..9bf283d5 100644 --- a/src/charging-station/MessageChannelUtils.ts +++ b/src/charging-station/MessageChannelUtils.ts @@ -60,9 +60,13 @@ export class MessageChannelUtils { ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest ), ...(chargingStation.automaticTransactionGenerator && { - automaticTransactionGeneratorStatuses: [ - ...chargingStation.automaticTransactionGenerator.connectorsStatus.values(), - ], + automaticTransactionGenerator: { + automaticTransactionGenerator: + chargingStation.automaticTransactionGenerator.configuration, + automaticTransactionGeneratorStatuses: [ + ...chargingStation.automaticTransactionGenerator.connectorsStatus.values(), + ], + }, }), }; } diff --git a/src/types/AutomaticTransactionGenerator.ts b/src/types/AutomaticTransactionGenerator.ts index c8ce3c9a..a91c9067 100644 --- a/src/types/AutomaticTransactionGenerator.ts +++ b/src/types/AutomaticTransactionGenerator.ts @@ -1,3 +1,9 @@ +export enum IdTagDistribution { + RANDOM = 'random', + ROUND_ROBIN = 'round-robin', + CONNECTOR_AFFINITY = 'connector-affinity', +} + export type AutomaticTransactionGeneratorConfiguration = { enable: boolean; minDuration: number; @@ -8,6 +14,7 @@ export type AutomaticTransactionGeneratorConfiguration = { stopAfterHours: number; stopOnConnectionFailure: boolean; requireAuthorize?: boolean; + idTagDistribution?: IdTagDistribution; }; export type Status = { @@ -31,5 +38,5 @@ export type Status = { export type ChargingStationAutomaticTransactionGeneratorConfiguration = { automaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration; - automaticTransactionGeneratorStatus?: Status; + automaticTransactionGeneratorStatuses?: Status[]; }; diff --git a/src/types/ChargingStationWorker.ts b/src/types/ChargingStationWorker.ts index ec6e92b5..ac13f441 100644 --- a/src/types/ChargingStationWorker.ts +++ b/src/types/ChargingStationWorker.ts @@ -1,6 +1,6 @@ import type { WebSocket } from 'ws'; -import type { Status } from './AutomaticTransactionGenerator'; +import type { ChargingStationAutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator'; import type { ChargingStationInfo } from './ChargingStationInfo'; import type { ConnectorStatus } from './ConnectorStatus'; import type { JsonObject } from './JsonType'; @@ -28,7 +28,7 @@ export interface ChargingStationData extends WorkerData { | typeof WebSocket.CLOSED; bootNotificationResponse: BootNotificationResponse; connectors: ConnectorStatus[]; - automaticTransactionGeneratorStatuses?: Status[]; + automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration; } enum ChargingStationMessageEvents { @@ -47,6 +47,9 @@ export const ChargingStationWorkerMessageEvents = { export type ChargingStationWorkerMessageData = ChargingStationData | Statistics; -export type ChargingStationWorkerMessage = Omit, 'id'> & { +export type ChargingStationWorkerMessage = Omit< + WorkerMessage, + 'id' +> & { id: ChargingStationWorkerMessageEvents; }; diff --git a/src/types/ConfigurationData.ts b/src/types/ConfigurationData.ts index ba9fb14c..58372a38 100644 --- a/src/types/ConfigurationData.ts +++ b/src/types/ConfigurationData.ts @@ -11,7 +11,7 @@ export type ServerOptions = ListenOptions; export enum SupervisionUrlDistribution { ROUND_ROBIN = 'round-robin', RANDOM = 'random', - SEQUENTIAL = 'sequential', + CHARGING_STATION_AFFINITY = 'charging-station-affinity', } export type StationTemplateUrl = { diff --git a/src/types/Worker.ts b/src/types/Worker.ts index c2594b9a..29f70aa4 100644 --- a/src/types/Worker.ts +++ b/src/types/Worker.ts @@ -15,7 +15,7 @@ export type WorkerOptions = { poolMinSize?: number; elementsPerWorker?: number; poolOptions?: PoolOptions; - messageHandler?: (message: unknown) => void | Promise; + messageHandler?: (this: Worker, message: unknown) => void | Promise; }; export type WorkerData = Record; -- 2.34.1