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 workerConfiguration
?: WorkerConfiguration
;
59 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>;
60 private readonly uiServer
?: AbstractUIServer
;
61 private storage
?: Storage
;
62 private numberOfStartedChargingStations
!: number;
63 private readonly version
: string = version
;
64 private initializedCounters
: boolean;
65 private started
: boolean;
66 private starting
: boolean;
67 private stopping
: boolean;
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.uiServer
= UIServerFactory
.getUIServerImplementation(
83 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
),
85 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
);
86 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
);
87 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
);
89 ChargingStationWorkerMessageEvents
.performanceStatistics
,
90 this.workerEventPerformanceStatistics
,
92 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart(false);
95 public static getInstance(): Bootstrap
{
96 if (Bootstrap
.instance
=== null) {
97 Bootstrap
.instance
= new Bootstrap();
99 return Bootstrap
.instance
;
102 public async start(): Promise
<void> {
103 if (this.started
=== false) {
104 if (this.starting
=== false) {
105 this.starting
= true;
106 this.initializeCounters();
107 this.workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
108 ConfigurationSection
.worker
,
110 this.initializeWorkerImplementation(this.workerConfiguration
);
111 await this.workerImplementation
?.start();
112 const performanceStorageConfiguration
=
113 Configuration
.getConfigurationSection
<StorageConfiguration
>(
114 ConfigurationSection
.performanceStorage
,
116 if (performanceStorageConfiguration
.enabled
=== true) {
117 this.storage
= StorageFactory
.getStorage(
118 performanceStorageConfiguration
.type!,
119 performanceStorageConfiguration
.uri
!,
122 await this.storage
?.open();
124 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
125 .enabled
=== true && this.uiServer
?.start();
126 // Start ChargingStation object instance in worker thread
127 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
129 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
130 for (let index
= 1; index
<= nbStations
; index
++) {
131 await this.startChargingStation(index
, stationTemplateUrl
);
136 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
144 `Charging stations simulator ${
146 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
147 Configuration.workerDynamicPoolInUse()
148 ? `${this.workerConfiguration.poolMinSize?.toString()}
/`
150 }${this.workerImplementation?.size}${
151 Configuration.workerPoolInUse()
152 ? `/${this.workerConfiguration.poolMaxSize?.toString()}
`
154 } worker(s) concurrently running in '${this.workerConfiguration.processType}' mode${
155 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
156 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
161 Configuration
.workerDynamicPoolInUse() &&
164 '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',
167 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
169 this.starting
= false;
171 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
174 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
178 public async stop(stopChargingStations
= true): Promise
<void> {
179 if (this.started
=== true) {
180 if (this.stopping
=== false) {
181 this.stopping
= true;
182 if (stopChargingStations
=== true) {
183 await this.uiServer
?.sendInternalRequest(
184 this.uiServer
.buildProtocolRequest(
186 ProcedureName
.STOP_CHARGING_STATION
,
187 Constants
.EMPTY_FROZEN_OBJECT
,
191 await this.waitChargingStationsStopped();
193 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
);
196 await this.workerImplementation
?.stop();
197 delete this.workerImplementation
;
198 delete this.workerConfiguration
;
199 this.uiServer
?.stop();
200 await this.storage
?.close();
202 this.resetCounters();
203 this.initializedCounters
= false;
204 this.started
= false;
205 this.stopping
= false;
207 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
210 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
214 public async restart(stopChargingStations
?: boolean): Promise
<void> {
215 await this.stop(stopChargingStations
);
219 private async waitChargingStationsStopped(): Promise
<string> {
220 return new Promise
<string>((resolve
, reject
) => {
221 const waitTimeout
= setTimeout(() => {
222 const message
= `Timeout ${formatDurationMilliSeconds(
223 Constants.STOP_CHARGING_STATIONS_TIMEOUT,
224 )} reached at stopping charging stations`;
225 console
.warn(chalk
.yellow(message
));
226 reject(new Error(message
));
227 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
);
228 waitChargingStationEvents(
230 ChargingStationWorkerMessageEvents
.stopped
,
231 this.numberOfChargingStations
,
234 resolve('Charging stations stopped');
238 clearTimeout(waitTimeout
);
243 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
244 let elementsPerWorker
: number | undefined;
245 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
247 this.numberOfChargingStations
> availableParallelism()
248 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
251 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
253 dirname(fileURLToPath(import.meta
.url
)),
254 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
256 workerConfiguration
.processType
!,
258 workerStartDelay
: workerConfiguration
.startDelay
,
259 elementStartDelay
: workerConfiguration
.elementStartDelay
,
260 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
261 poolMinSize
: workerConfiguration
.poolMinSize
!,
262 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
264 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
270 private messageHandler(
271 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
274 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
282 case ChargingStationWorkerMessageEvents
.started
:
283 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
285 case ChargingStationWorkerMessageEvents
.stopped
:
286 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
288 case ChargingStationWorkerMessageEvents
.updated
:
289 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
291 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
293 ChargingStationWorkerMessageEvents
.performanceStatistics
,
294 msg
.data
as Statistics
,
297 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
299 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
302 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
304 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
308 `Unknown charging station worker event: '${
310 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
315 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
323 private workerEventStarted
= (data
: ChargingStationData
) => {
324 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
325 ++this.numberOfStartedChargingStations
;
327 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
328 data.stationInfo.chargingStationId
329 } (hashId: ${data.stationInfo.hashId}) started (${
330 this.numberOfStartedChargingStations
331 } started from ${this.numberOfChargingStations})`,
335 private workerEventStopped
= (data
: ChargingStationData
) => {
336 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
337 --this.numberOfStartedChargingStations
;
339 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
340 data.stationInfo.chargingStationId
341 } (hashId: ${data.stationInfo.hashId}) stopped (${
342 this.numberOfStartedChargingStations
343 } started from ${this.numberOfChargingStations})`,
347 private workerEventUpdated
= (data
: ChargingStationData
) => {
348 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
351 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
352 this.storage
?.storePerformanceStatistics(data
) as void;
355 private initializeCounters() {
356 if (this.initializedCounters
=== false) {
357 this.resetCounters();
358 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
359 if (isNotEmptyArray(stationTemplateUrls
)) {
360 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
361 for (const stationTemplateUrl
of stationTemplateUrls
) {
362 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
366 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
368 exit(exitCodes
.missingChargingStationsConfiguration
);
370 if (this.numberOfChargingStations
=== 0) {
372 chalk
.yellow('No charging station template enabled in configuration, exiting'),
374 exit(exitCodes
.noChargingStationTemplates
);
376 this.initializedCounters
= true;
380 private resetCounters(): void {
381 this.numberOfChargingStationTemplates
= 0;
382 this.numberOfChargingStations
= 0;
383 this.numberOfStartedChargingStations
= 0;
386 private async startChargingStation(
388 stationTemplateUrl
: StationTemplateUrl
,
390 await this.workerImplementation
?.addElement({
393 dirname(fileURLToPath(import.meta
.url
)),
396 stationTemplateUrl
.file
,
401 private gracefulShutdown(): void {
404 console
.info(`${chalk.green('Graceful shutdown')}`);
405 // stop() asks for charging stations to stop by default
406 this.waitChargingStationsStopped()
408 exit(exitCodes
.succeeded
);
411 exit(exitCodes
.gracefulShutdownError
);
415 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
416 exit(exitCodes
.gracefulShutdownError
);
420 private logPrefix
= (): string => {
421 return logPrefix(' Bootstrap |');