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
>;
59 private readonly uiServer
?: AbstractUIServer
;
60 private 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;
68 private constructor() {
70 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
71 process
.on(signal
, this.gracefulShutdown
.bind(this));
73 // Enable unconditionally for now
74 handleUnhandledRejection();
75 handleUncaughtException();
77 this.starting
= false;
78 this.stopping
= false;
79 this.initializedCounters
= false;
80 this.initializeCounters();
81 this.uiServer
= UIServerFactory
.getUIServerImplementation(
82 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
),
84 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
);
85 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
);
86 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
);
88 ChargingStationWorkerMessageEvents
.performanceStatistics
,
89 this.workerEventPerformanceStatistics
,
91 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart(false);
94 public static getInstance(): Bootstrap
{
95 if (Bootstrap
.instance
=== null) {
96 Bootstrap
.instance
= new Bootstrap();
98 return Bootstrap
.instance
;
101 public async start(): Promise
<void> {
102 if (this.started
=== false) {
103 if (this.starting
=== false) {
104 this.starting
= true;
105 this.initializeCounters();
106 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
107 ConfigurationSection
.worker
,
109 this.initializeWorkerImplementation(workerConfiguration
);
110 await this.workerImplementation
?.start();
111 const performanceStorageConfiguration
=
112 Configuration
.getConfigurationSection
<StorageConfiguration
>(
113 ConfigurationSection
.performanceStorage
,
115 if (performanceStorageConfiguration
.enabled
=== true) {
116 this.storage
= StorageFactory
.getStorage(
117 performanceStorageConfiguration
.type!,
118 performanceStorageConfiguration
.uri
!,
121 await this.storage
?.open();
123 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
124 .enabled
=== true && this.uiServer
?.start();
125 // Start ChargingStation object instance in worker thread
126 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
128 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
129 for (let index
= 1; index
<= nbStations
; index
++) {
130 await this.startChargingStation(index
, stationTemplateUrl
);
135 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
143 `Charging stations simulator ${
145 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
146 Configuration.workerDynamicPoolInUse()
147 ? `${workerConfiguration.poolMinSize?.toString()}
/`
149 }${this.workerImplementation?.size}${
150 Configuration.workerPoolInUse()
151 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
153 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
154 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
155 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
160 Configuration
.workerDynamicPoolInUse() &&
163 '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',
166 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
168 this.starting
= false;
170 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
173 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
177 public async stop(stopChargingStations
= true): Promise
<void> {
178 if (this.started
=== true) {
179 if (this.stopping
=== false) {
180 this.stopping
= true;
181 if (stopChargingStations
=== true) {
182 await this.uiServer
?.sendInternalRequest(
183 this.uiServer
.buildProtocolRequest(
185 ProcedureName
.STOP_CHARGING_STATION
,
186 Constants
.EMPTY_FROZEN_OBJECT
,
190 await this.waitChargingStationsStopped();
192 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
);
195 await this.workerImplementation
?.stop();
196 delete this.workerImplementation
;
197 this.uiServer
?.stop();
198 await this.storage
?.close();
200 this.resetCounters();
201 this.initializedCounters
= false;
202 this.started
= false;
203 this.stopping
= false;
205 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
208 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
212 public async restart(stopChargingStations
?: boolean): Promise
<void> {
213 await this.stop(stopChargingStations
);
217 private async waitChargingStationsStopped(): Promise
<string> {
218 return new Promise
<string>((resolve
, reject
) => {
219 const waitTimeout
= setTimeout(() => {
220 const message
= `Timeout ${formatDurationMilliSeconds(
221 Constants.STOP_CHARGING_STATIONS_TIMEOUT,
222 )} reached at stopping charging stations`;
223 console
.warn(chalk
.yellow(message
));
224 reject(new Error(message
));
225 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
);
226 waitChargingStationEvents(
228 ChargingStationWorkerMessageEvents
.stopped
,
229 this.numberOfChargingStations
,
232 resolve('Charging stations stopped');
236 clearTimeout(waitTimeout
);
241 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
242 let elementsPerWorker
: number | undefined;
243 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
245 this.numberOfChargingStations
> availableParallelism()
246 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
249 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
251 dirname(fileURLToPath(import.meta
.url
)),
252 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
254 workerConfiguration
.processType
!,
256 workerStartDelay
: workerConfiguration
.startDelay
,
257 elementStartDelay
: workerConfiguration
.elementStartDelay
,
258 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
259 poolMinSize
: workerConfiguration
.poolMinSize
!,
260 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
262 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
268 private messageHandler(
269 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
272 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
280 case ChargingStationWorkerMessageEvents
.started
:
281 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
283 case ChargingStationWorkerMessageEvents
.stopped
:
284 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
286 case ChargingStationWorkerMessageEvents
.updated
:
287 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
289 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
291 ChargingStationWorkerMessageEvents
.performanceStatistics
,
292 msg
.data
as Statistics
,
295 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
297 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
300 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
302 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
306 `Unknown charging station worker event: '${
308 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
313 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
321 private workerEventStarted
= (data
: ChargingStationData
) => {
322 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
323 ++this.numberOfStartedChargingStations
;
325 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
326 data.stationInfo.chargingStationId
327 } (hashId: ${data.stationInfo.hashId}) started (${
328 this.numberOfStartedChargingStations
329 } started from ${this.numberOfChargingStations})`,
333 private workerEventStopped
= (data
: ChargingStationData
) => {
334 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
335 --this.numberOfStartedChargingStations
;
337 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
338 data.stationInfo.chargingStationId
339 } (hashId: ${data.stationInfo.hashId}) stopped (${
340 this.numberOfStartedChargingStations
341 } started from ${this.numberOfChargingStations})`,
345 private workerEventUpdated
= (data
: ChargingStationData
) => {
346 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
349 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
350 this.storage
?.storePerformanceStatistics(data
) as void;
353 private initializeCounters() {
354 if (this.initializedCounters
=== false) {
355 this.resetCounters();
356 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
357 if (isNotEmptyArray(stationTemplateUrls
)) {
358 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
359 for (const stationTemplateUrl
of stationTemplateUrls
) {
360 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
364 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
366 exit(exitCodes
.missingChargingStationsConfiguration
);
368 if (this.numberOfChargingStations
=== 0) {
370 chalk
.yellow('No charging station template enabled in configuration, exiting'),
372 exit(exitCodes
.noChargingStationTemplates
);
374 this.initializedCounters
= true;
378 private resetCounters(): void {
379 this.numberOfChargingStationTemplates
= 0;
380 this.numberOfChargingStations
= 0;
381 this.numberOfStartedChargingStations
= 0;
384 private async startChargingStation(
386 stationTemplateUrl
: StationTemplateUrl
,
388 await this.workerImplementation
?.addElement({
391 dirname(fileURLToPath(import.meta
.url
)),
394 stationTemplateUrl
.file
,
399 private gracefulShutdown(): void {
402 console
.info(`${chalk.green('Graceful shutdown')}`);
403 // stop() asks for charging stations to stop by default
404 this.waitChargingStationsStopped()
406 exit(exitCodes
.succeeded
);
409 exit(exitCodes
.gracefulShutdownError
);
413 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
414 exit(exitCodes
.gracefulShutdownError
);
418 private logPrefix
= (): string => {
419 return logPrefix(' Bootstrap |');