1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { EventEmitter
} from
'node:events';
4 import { dirname
, extname
, join
} from
'node:path';
5 import { exit
} from
'node:process';
6 import { fileURLToPath
} from
'node:url';
8 import chalk from
'chalk';
9 import { availableParallelism
} from
'poolifier';
11 import { waitChargingStationEvents
} from
'./Helpers';
12 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
13 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
14 import { version
} from
'../../package.json';
15 import { BaseError
} from
'../exception';
16 import { type Storage
, StorageFactory
} from
'../performance';
18 type ChargingStationData
,
19 type ChargingStationWorkerData
,
20 type ChargingStationWorkerMessage
,
21 type ChargingStationWorkerMessageData
,
22 ChargingStationWorkerMessageEvents
,
25 type StationTemplateUrl
,
27 type StorageConfiguration
,
28 type UIServerConfiguration
,
29 type WorkerConfiguration
,
34 formatDurationMilliSeconds
,
36 handleUncaughtException
,
37 handleUnhandledRejection
,
43 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
45 const moduleName
= 'Bootstrap';
49 missingChargingStationsConfiguration
= 1,
50 noChargingStationTemplates
= 2,
51 gracefulShutdownError
= 3,
54 export class Bootstrap
extends EventEmitter
{
55 private static instance
: Bootstrap
| null = null;
56 public numberOfChargingStations
!: number;
57 public numberOfChargingStationTemplates
!: number;
58 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
59 private readonly uiServer
!: AbstractUIServer
| null;
60 private readonly storage
!: Storage
;
61 private numberOfStartedChargingStations
!: number;
62 private readonly version
: string = version
;
63 private initializedCounters
: boolean;
64 private started
: boolean;
65 private starting
: boolean;
66 private stopping
: boolean;
67 private readonly workerScript
: string;
69 private constructor() {
71 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
72 process
.on(signal
, this.gracefulShutdown
);
74 // Enable unconditionally for now
75 handleUnhandledRejection();
76 handleUncaughtException();
78 this.starting
= false;
79 this.stopping
= false;
80 this.initializedCounters
= false;
81 this.initializeCounters();
82 this.workerImplementation
= null;
83 this.workerScript
= join(
84 dirname(fileURLToPath(import.meta
.url
)),
85 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
87 const uiServerConfiguration
= Configuration
.getConfigurationSection
<UIServerConfiguration
>(
88 ConfigurationSection
.uiServer
,
90 uiServerConfiguration
.enabled
=== true &&
91 (this.uiServer
= UIServerFactory
.getUIServerImplementation(uiServerConfiguration
));
92 const performanceStorageConfiguration
=
93 Configuration
.getConfigurationSection
<StorageConfiguration
>(
94 ConfigurationSection
.performanceStorage
,
96 performanceStorageConfiguration
.enabled
=== true &&
97 (this.storage
= StorageFactory
.getStorage(
98 performanceStorageConfiguration
.type!,
99 performanceStorageConfiguration
.uri
!,
102 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart(false);
105 public static getInstance(): Bootstrap
{
106 if (Bootstrap
.instance
=== null) {
107 Bootstrap
.instance
= new Bootstrap();
109 return Bootstrap
.instance
;
112 public async start(): Promise
<void> {
113 if (this.started
=== false) {
114 if (this.starting
=== false) {
115 this.starting
= true;
116 this.initializeCounters();
117 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
118 ConfigurationSection
.worker
,
120 this.initializeWorkerImplementation(workerConfiguration
);
121 await this.workerImplementation
?.start();
122 await this.storage
?.open();
123 this.uiServer
?.start();
124 // Start ChargingStation object instance in worker thread
125 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
127 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
128 for (let index
= 1; index
<= nbStations
; index
++) {
129 await this.startChargingStation(index
, stationTemplateUrl
);
134 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
142 `Charging stations simulator ${
144 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
145 Configuration.workerDynamicPoolInUse()
146 ? `${workerConfiguration.poolMinSize?.toString()}
/`
148 }${this.workerImplementation?.size}${
149 Configuration.workerPoolInUse()
150 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
152 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
153 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
154 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
159 Configuration
.workerDynamicPoolInUse() &&
162 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead',
165 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
167 this.starting
= false;
169 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
172 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
176 public async stop(waitChargingStationsStopped
= true): Promise
<void> {
177 if (this.started
=== true) {
178 if (this.stopping
=== false) {
179 this.stopping
= true;
180 await this.uiServer
?.sendInternalRequest(
181 this.uiServer
.buildProtocolRequest(
183 ProcedureName
.STOP_CHARGING_STATION
,
184 Constants
.EMPTY_FROZEN_OBJECT
,
187 if (waitChargingStationsStopped
=== true) {
189 waitChargingStationEvents(
191 ChargingStationWorkerMessageEvents
.stopped
,
192 this.numberOfChargingStations
,
194 new Promise
<string>((resolve
) => {
196 const message
= `Timeout ${formatDurationMilliSeconds(
197 Constants.STOP_SIMULATOR_TIMEOUT,
198 )} reached at stopping charging stations simulator`;
199 console
.warn(chalk
.yellow(message
));
201 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
205 await this.workerImplementation
?.stop();
206 this.workerImplementation
= null;
207 this.uiServer
?.stop();
208 await this.storage
?.close();
209 this.resetCounters();
210 this.initializedCounters
= false;
211 this.started
= false;
212 this.stopping
= false;
214 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
217 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
221 public async restart(waitChargingStationsStopped
?: boolean): Promise
<void> {
222 await this.stop(waitChargingStationsStopped
);
226 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
227 let elementsPerWorker
: number | undefined;
228 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
230 this.numberOfChargingStations
> availableParallelism()
231 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
234 this.workerImplementation
=== null &&
235 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
237 workerConfiguration
.processType
!,
239 workerStartDelay
: workerConfiguration
.startDelay
,
240 elementStartDelay
: workerConfiguration
.elementStartDelay
,
241 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
242 poolMinSize
: workerConfiguration
.poolMinSize
!,
243 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
245 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
251 private messageHandler(
252 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
255 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
263 case ChargingStationWorkerMessageEvents
.started
:
264 this.workerEventStarted(msg
.data
as ChargingStationData
);
265 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
267 case ChargingStationWorkerMessageEvents
.stopped
:
268 this.workerEventStopped(msg
.data
as ChargingStationData
);
269 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
271 case ChargingStationWorkerMessageEvents
.updated
:
272 this.workerEventUpdated(msg
.data
as ChargingStationData
);
273 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
275 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
276 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
278 ChargingStationWorkerMessageEvents
.performanceStatistics
,
279 msg
.data
as Statistics
,
282 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
284 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
287 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
289 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
293 `Unknown charging station worker event: '${
295 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
300 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
308 private workerEventStarted
= (data
: ChargingStationData
) => {
309 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
310 ++this.numberOfStartedChargingStations
;
312 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
313 data.stationInfo.chargingStationId
314 } (hashId: ${data.stationInfo.hashId}) started (${
315 this.numberOfStartedChargingStations
316 } started from ${this.numberOfChargingStations})`,
320 private workerEventStopped
= (data
: ChargingStationData
) => {
321 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
322 --this.numberOfStartedChargingStations
;
324 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
325 data.stationInfo.chargingStationId
326 } (hashId: ${data.stationInfo.hashId}) stopped (${
327 this.numberOfStartedChargingStations
328 } started from ${this.numberOfChargingStations})`,
332 private workerEventUpdated
= (data
: ChargingStationData
) => {
333 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
336 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
337 this.storage
.storePerformanceStatistics(data
) as void;
340 private initializeCounters() {
341 if (this.initializedCounters
=== false) {
342 this.resetCounters();
343 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
344 if (isNotEmptyArray(stationTemplateUrls
)) {
345 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
346 for (const stationTemplateUrl
of stationTemplateUrls
) {
347 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
351 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
353 exit(exitCodes
.missingChargingStationsConfiguration
);
355 if (this.numberOfChargingStations
=== 0) {
357 chalk
.yellow('No charging station template enabled in configuration, exiting'),
359 exit(exitCodes
.noChargingStationTemplates
);
361 this.initializedCounters
= true;
365 private resetCounters(): void {
366 this.numberOfChargingStationTemplates
= 0;
367 this.numberOfChargingStations
= 0;
368 this.numberOfStartedChargingStations
= 0;
371 private async startChargingStation(
373 stationTemplateUrl
: StationTemplateUrl
,
375 await this.workerImplementation
?.addElement({
378 dirname(fileURLToPath(import.meta
.url
)),
381 stationTemplateUrl
.file
,
386 private gracefulShutdown
= (): void => {
389 console
.info(`${chalk.green('Graceful shutdown')}`);
390 exit(exitCodes
.succeeded
);
393 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
394 exit(exitCodes
.gracefulShutdownError
);
398 private logPrefix
= (): string => {
399 return logPrefix(' Bootstrap |');