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 process
, { 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
.bind(this));
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 this.uiServer
= UIServerFactory
.getUIServerImplementation(
88 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
),
90 const performanceStorageConfiguration
=
91 Configuration
.getConfigurationSection
<StorageConfiguration
>(
92 ConfigurationSection
.performanceStorage
,
94 performanceStorageConfiguration
.enabled
=== true &&
95 (this.storage
= StorageFactory
.getStorage(
96 performanceStorageConfiguration
.type!,
97 performanceStorageConfiguration
.uri
!,
100 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart(false);
103 public static getInstance(): Bootstrap
{
104 if (Bootstrap
.instance
=== null) {
105 Bootstrap
.instance
= new Bootstrap();
107 return Bootstrap
.instance
;
110 public async start(): Promise
<void> {
111 if (this.started
=== false) {
112 if (this.starting
=== false) {
113 this.starting
= true;
114 this.initializeCounters();
115 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
116 ConfigurationSection
.worker
,
118 this.initializeWorkerImplementation(workerConfiguration
);
119 await this.workerImplementation
?.start();
120 await this.storage
?.open();
121 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
122 .enabled
=== true && this.uiServer
?.start();
123 // Start ChargingStation object instance in worker thread
124 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
126 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
127 for (let index
= 1; index
<= nbStations
; index
++) {
128 await this.startChargingStation(index
, stationTemplateUrl
);
133 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
141 `Charging stations simulator ${
143 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
144 Configuration.workerDynamicPoolInUse()
145 ? `${workerConfiguration.poolMinSize?.toString()}
/`
147 }${this.workerImplementation?.size}${
148 Configuration.workerPoolInUse()
149 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
151 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
152 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
153 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
158 Configuration
.workerDynamicPoolInUse() &&
161 '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',
164 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
166 this.starting
= false;
168 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
171 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
175 public async stop(stopChargingStations
= true): Promise
<void> {
176 if (this.started
=== true) {
177 if (this.stopping
=== false) {
178 this.stopping
= true;
179 if (stopChargingStations
=== true) {
180 await this.uiServer
?.sendInternalRequest(
181 this.uiServer
.buildProtocolRequest(
183 ProcedureName
.STOP_CHARGING_STATION
,
184 Constants
.EMPTY_FROZEN_OBJECT
,
188 await this.waitChargingStationsStopped();
190 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
);
193 await this.workerImplementation
?.stop();
194 this.workerImplementation
= null;
195 this.uiServer
?.stop();
196 await this.storage
?.close();
197 this.resetCounters();
198 this.initializedCounters
= false;
199 this.started
= false;
200 this.stopping
= false;
202 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
205 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
209 public async restart(stopChargingStations
?: boolean): Promise
<void> {
210 await this.stop(stopChargingStations
);
214 private async waitChargingStationsStopped(): Promise
<string> {
215 return new Promise
<string>((resolve
, reject
) => {
216 const waitTimeout
= setTimeout(() => {
217 const message
= `Timeout ${formatDurationMilliSeconds(
218 Constants.STOP_CHARGING_STATIONS_TIMEOUT,
219 )} reached at stopping charging stations`;
220 console
.warn(chalk
.yellow(message
));
221 reject(new Error(message
));
222 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
);
223 waitChargingStationEvents(
225 ChargingStationWorkerMessageEvents
.stopped
,
226 this.numberOfChargingStations
,
229 resolve('Charging stations stopped');
233 clearTimeout(waitTimeout
);
238 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
239 let elementsPerWorker
: number | undefined;
240 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
242 this.numberOfChargingStations
> availableParallelism()
243 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
246 this.workerImplementation
=== null &&
247 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
249 workerConfiguration
.processType
!,
251 workerStartDelay
: workerConfiguration
.startDelay
,
252 elementStartDelay
: workerConfiguration
.elementStartDelay
,
253 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
254 poolMinSize
: workerConfiguration
.poolMinSize
!,
255 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
257 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
263 private messageHandler(
264 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
267 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
275 case ChargingStationWorkerMessageEvents
.started
:
276 this.workerEventStarted(msg
.data
as ChargingStationData
);
277 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
279 case ChargingStationWorkerMessageEvents
.stopped
:
280 this.workerEventStopped(msg
.data
as ChargingStationData
);
281 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
283 case ChargingStationWorkerMessageEvents
.updated
:
284 this.workerEventUpdated(msg
.data
as ChargingStationData
);
285 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
287 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
288 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
290 ChargingStationWorkerMessageEvents
.performanceStatistics
,
291 msg
.data
as Statistics
,
294 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
296 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
299 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
301 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
305 `Unknown charging station worker event: '${
307 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
312 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
320 private workerEventStarted
= (data
: ChargingStationData
) => {
321 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
322 ++this.numberOfStartedChargingStations
;
324 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
325 data.stationInfo.chargingStationId
326 } (hashId: ${data.stationInfo.hashId}) started (${
327 this.numberOfStartedChargingStations
328 } started from ${this.numberOfChargingStations})`,
332 private workerEventStopped
= (data
: ChargingStationData
) => {
333 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
334 --this.numberOfStartedChargingStations
;
336 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
337 data.stationInfo.chargingStationId
338 } (hashId: ${data.stationInfo.hashId}) stopped (${
339 this.numberOfStartedChargingStations
340 } started from ${this.numberOfChargingStations})`,
344 private workerEventUpdated
= (data
: ChargingStationData
) => {
345 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
348 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
349 this.storage
.storePerformanceStatistics(data
) as void;
352 private initializeCounters() {
353 if (this.initializedCounters
=== false) {
354 this.resetCounters();
355 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
356 if (isNotEmptyArray(stationTemplateUrls
)) {
357 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
358 for (const stationTemplateUrl
of stationTemplateUrls
) {
359 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
363 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
365 exit(exitCodes
.missingChargingStationsConfiguration
);
367 if (this.numberOfChargingStations
=== 0) {
369 chalk
.yellow('No charging station template enabled in configuration, exiting'),
371 exit(exitCodes
.noChargingStationTemplates
);
373 this.initializedCounters
= true;
377 private resetCounters(): void {
378 this.numberOfChargingStationTemplates
= 0;
379 this.numberOfChargingStations
= 0;
380 this.numberOfStartedChargingStations
= 0;
383 private async startChargingStation(
385 stationTemplateUrl
: StationTemplateUrl
,
387 await this.workerImplementation
?.addElement({
390 dirname(fileURLToPath(import.meta
.url
)),
393 stationTemplateUrl
.file
,
398 private gracefulShutdown(): void {
401 console
.info(`${chalk.green('Graceful shutdown')}`);
402 // stop() asks for charging stations to stop by default
403 this.waitChargingStationsStopped()
405 exit(exitCodes
.succeeded
);
408 exit(exitCodes
.gracefulShutdownError
);
412 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
413 exit(exitCodes
.gracefulShutdownError
);
417 private logPrefix
= (): string => {
418 return logPrefix(' Bootstrap |');