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
,
187 await this.waitChargingStationsStopped();
189 await this.workerImplementation
?.stop();
190 this.workerImplementation
= null;
191 this.uiServer
?.stop();
192 await this.storage
?.close();
193 this.resetCounters();
194 this.initializedCounters
= false;
195 this.started
= false;
196 this.stopping
= false;
198 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
201 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
205 public async restart(stopChargingStations
?: boolean): Promise
<void> {
206 await this.stop(stopChargingStations
);
210 private async waitChargingStationsStopped(): Promise
<void> {
212 waitChargingStationEvents(
214 ChargingStationWorkerMessageEvents
.stopped
,
215 this.numberOfChargingStations
,
217 new Promise
<string>((resolve
) => {
219 const message
= `Timeout ${formatDurationMilliSeconds(
220 Constants.STOP_SIMULATOR_TIMEOUT,
221 )} reached at stopping charging stations simulator`;
222 console
.warn(chalk
.yellow(message
));
224 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
229 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
230 let elementsPerWorker
: number | undefined;
231 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
233 this.numberOfChargingStations
> availableParallelism()
234 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
237 this.workerImplementation
=== null &&
238 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
240 workerConfiguration
.processType
!,
242 workerStartDelay
: workerConfiguration
.startDelay
,
243 elementStartDelay
: workerConfiguration
.elementStartDelay
,
244 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
245 poolMinSize
: workerConfiguration
.poolMinSize
!,
246 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
248 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
254 private messageHandler(
255 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
258 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
266 case ChargingStationWorkerMessageEvents
.started
:
267 this.workerEventStarted(msg
.data
as ChargingStationData
);
268 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
270 case ChargingStationWorkerMessageEvents
.stopped
:
271 this.workerEventStopped(msg
.data
as ChargingStationData
);
272 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
274 case ChargingStationWorkerMessageEvents
.updated
:
275 this.workerEventUpdated(msg
.data
as ChargingStationData
);
276 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
278 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
279 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
281 ChargingStationWorkerMessageEvents
.performanceStatistics
,
282 msg
.data
as Statistics
,
285 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
287 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
290 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
292 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
296 `Unknown charging station worker event: '${
298 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
303 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
311 private workerEventStarted
= (data
: ChargingStationData
) => {
312 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
313 ++this.numberOfStartedChargingStations
;
315 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
316 data.stationInfo.chargingStationId
317 } (hashId: ${data.stationInfo.hashId}) started (${
318 this.numberOfStartedChargingStations
319 } started from ${this.numberOfChargingStations})`,
323 private workerEventStopped
= (data
: ChargingStationData
) => {
324 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
325 --this.numberOfStartedChargingStations
;
327 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
328 data.stationInfo.chargingStationId
329 } (hashId: ${data.stationInfo.hashId}) stopped (${
330 this.numberOfStartedChargingStations
331 } started from ${this.numberOfChargingStations})`,
335 private workerEventUpdated
= (data
: ChargingStationData
) => {
336 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
339 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
340 this.storage
.storePerformanceStatistics(data
) as void;
343 private initializeCounters() {
344 if (this.initializedCounters
=== false) {
345 this.resetCounters();
346 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
347 if (isNotEmptyArray(stationTemplateUrls
)) {
348 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
349 for (const stationTemplateUrl
of stationTemplateUrls
) {
350 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
354 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
356 exit(exitCodes
.missingChargingStationsConfiguration
);
358 if (this.numberOfChargingStations
=== 0) {
360 chalk
.yellow('No charging station template enabled in configuration, exiting'),
362 exit(exitCodes
.noChargingStationTemplates
);
364 this.initializedCounters
= true;
368 private resetCounters(): void {
369 this.numberOfChargingStationTemplates
= 0;
370 this.numberOfChargingStations
= 0;
371 this.numberOfStartedChargingStations
= 0;
374 private async startChargingStation(
376 stationTemplateUrl
: StationTemplateUrl
,
378 await this.workerImplementation
?.addElement({
381 dirname(fileURLToPath(import.meta
.url
)),
384 stationTemplateUrl
.file
,
389 private gracefulShutdown(): void {
392 console
.info(`${chalk.green('Graceful shutdown')}`);
393 // stop() asks for charging stations to stop by default
394 this.waitChargingStationsStopped()
396 exit(exitCodes
.succeeded
);
399 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
);
400 exit(exitCodes
.gracefulShutdownError
);
404 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
405 exit(exitCodes
.gracefulShutdownError
);
409 private logPrefix
= (): string => {
410 return logPrefix(' Bootstrap |');