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_SIMULATOR_TIMEOUT,
219 )} reached at stopping charging stations`;
220 console
.warn(chalk
.yellow(message
));
221 reject(new Error(message
));
222 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
223 waitChargingStationEvents(
225 ChargingStationWorkerMessageEvents
.stopped
,
226 this.numberOfChargingStations
,
229 resolve('Charging stations stopped');
235 clearTimeout(waitTimeout
);
240 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
241 let elementsPerWorker
: number | undefined;
242 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
244 this.numberOfChargingStations
> availableParallelism()
245 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
248 this.workerImplementation
=== null &&
249 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
251 workerConfiguration
.processType
!,
253 workerStartDelay
: workerConfiguration
.startDelay
,
254 elementStartDelay
: workerConfiguration
.elementStartDelay
,
255 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
256 poolMinSize
: workerConfiguration
.poolMinSize
!,
257 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
259 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
265 private messageHandler(
266 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
269 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
277 case ChargingStationWorkerMessageEvents
.started
:
278 this.workerEventStarted(msg
.data
as ChargingStationData
);
279 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
281 case ChargingStationWorkerMessageEvents
.stopped
:
282 this.workerEventStopped(msg
.data
as ChargingStationData
);
283 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
285 case ChargingStationWorkerMessageEvents
.updated
:
286 this.workerEventUpdated(msg
.data
as ChargingStationData
);
287 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
289 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
290 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
292 ChargingStationWorkerMessageEvents
.performanceStatistics
,
293 msg
.data
as Statistics
,
296 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
298 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
301 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
303 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
307 `Unknown charging station worker event: '${
309 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
314 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
322 private workerEventStarted
= (data
: ChargingStationData
) => {
323 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
324 ++this.numberOfStartedChargingStations
;
326 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
327 data.stationInfo.chargingStationId
328 } (hashId: ${data.stationInfo.hashId}) started (${
329 this.numberOfStartedChargingStations
330 } started from ${this.numberOfChargingStations})`,
334 private workerEventStopped
= (data
: ChargingStationData
) => {
335 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
336 --this.numberOfStartedChargingStations
;
338 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
339 data.stationInfo.chargingStationId
340 } (hashId: ${data.stationInfo.hashId}) stopped (${
341 this.numberOfStartedChargingStations
342 } started from ${this.numberOfChargingStations})`,
346 private workerEventUpdated
= (data
: ChargingStationData
) => {
347 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
350 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
351 this.storage
.storePerformanceStatistics(data
) as void;
354 private initializeCounters() {
355 if (this.initializedCounters
=== false) {
356 this.resetCounters();
357 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
358 if (isNotEmptyArray(stationTemplateUrls
)) {
359 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
360 for (const stationTemplateUrl
of stationTemplateUrls
) {
361 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
365 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
367 exit(exitCodes
.missingChargingStationsConfiguration
);
369 if (this.numberOfChargingStations
=== 0) {
371 chalk
.yellow('No charging station template enabled in configuration, exiting'),
373 exit(exitCodes
.noChargingStationTemplates
);
375 this.initializedCounters
= true;
379 private resetCounters(): void {
380 this.numberOfChargingStationTemplates
= 0;
381 this.numberOfChargingStations
= 0;
382 this.numberOfStartedChargingStations
= 0;
385 private async startChargingStation(
387 stationTemplateUrl
: StationTemplateUrl
,
389 await this.workerImplementation
?.addElement({
392 dirname(fileURLToPath(import.meta
.url
)),
395 stationTemplateUrl
.file
,
400 private gracefulShutdown(): void {
403 console
.info(`${chalk.green('Graceful shutdown')}`);
404 // stop() asks for charging stations to stop by default
405 this.waitChargingStationsStopped()
407 exit(exitCodes
.succeeded
);
410 exit(exitCodes
.gracefulShutdownError
);
414 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
415 exit(exitCodes
.gracefulShutdownError
);
419 private logPrefix
= (): string => {
420 return logPrefix(' Bootstrap |');