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 { exit
} from
'node:process';
6 import { fileURLToPath
} from
'node:url';
7 import { isMainThread
} from
'node:worker_threads';
9 import chalk from
'chalk';
10 import { availableParallelism
} from
'poolifier';
12 import { waitChargingStationEvents
} from
'./Helpers';
13 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
14 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
15 import { version
} from
'../../package.json';
16 import { BaseError
} from
'../exception';
17 import { type Storage
, StorageFactory
} from
'../performance';
19 type ChargingStationData
,
20 type ChargingStationWorkerData
,
21 type ChargingStationWorkerMessage
,
22 type ChargingStationWorkerMessageData
,
23 ChargingStationWorkerMessageEvents
,
26 type StationTemplateUrl
,
28 type StorageConfiguration
,
29 type UIServerConfiguration
,
30 type WorkerConfiguration
,
35 formatDurationMilliSeconds
,
37 handleUncaughtException
,
38 handleUnhandledRejection
,
44 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
46 const moduleName
= 'Bootstrap';
50 missingChargingStationsConfiguration
= 1,
51 noChargingStationTemplates
= 2,
52 gracefulShutdownError
= 3,
55 export class Bootstrap
extends EventEmitter
{
56 private static instance
: Bootstrap
| null = null;
57 public numberOfChargingStations
!: number;
58 public numberOfChargingStationTemplates
!: number;
59 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
60 private readonly uiServer
!: AbstractUIServer
| null;
61 private readonly 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;
68 private readonly workerScript
: string;
70 private constructor() {
72 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
73 process
.on(signal
, this.gracefulShutdown
);
75 // Enable unconditionally for now
76 handleUnhandledRejection();
77 handleUncaughtException();
79 this.starting
= false;
80 this.stopping
= false;
81 this.initializedCounters
= false;
82 this.initializeCounters();
83 this.workerImplementation
= null;
84 this.workerScript
= join(
85 dirname(fileURLToPath(import.meta
.url
)),
86 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
88 const uiServerConfiguration
= Configuration
.getConfigurationSection
<UIServerConfiguration
>(
89 ConfigurationSection
.uiServer
,
91 uiServerConfiguration
.enabled
=== true &&
92 (this.uiServer
= UIServerFactory
.getUIServerImplementation(uiServerConfiguration
));
93 const performanceStorageConfiguration
=
94 Configuration
.getConfigurationSection
<StorageConfiguration
>(
95 ConfigurationSection
.performanceStorage
,
97 performanceStorageConfiguration
.enabled
=== true &&
98 (this.storage
= StorageFactory
.getStorage(
99 performanceStorageConfiguration
.type!,
100 performanceStorageConfiguration
.uri
!,
103 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart();
106 public static getInstance(): Bootstrap
{
107 if (Bootstrap
.instance
=== null) {
108 Bootstrap
.instance
= new Bootstrap();
110 return Bootstrap
.instance
;
113 public async start(): Promise
<void> {
115 throw new BaseError('Cannot start charging stations simulator from worker thread');
117 if (this.started
=== false) {
118 if (this.starting
=== false) {
119 this.starting
= true;
120 this.initializeCounters();
121 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
122 ConfigurationSection
.worker
,
124 this.initializeWorkerImplementation(workerConfiguration
);
125 await this.workerImplementation
?.start();
126 await this.storage
?.open();
127 this.uiServer
?.start();
128 // Start ChargingStation object instance in worker thread
129 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
131 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
132 for (let index
= 1; index
<= nbStations
; index
++) {
133 await this.startChargingStation(index
, stationTemplateUrl
);
138 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
146 `Charging stations simulator ${
148 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
149 Configuration.workerDynamicPoolInUse()
150 ? `${workerConfiguration.poolMinSize?.toString()}
/`
152 }${this.workerImplementation?.size}${
153 Configuration.workerPoolInUse()
154 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
156 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
157 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
158 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
163 Configuration
.workerDynamicPoolInUse() &&
166 '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',
169 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
171 this.starting
= false;
173 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
176 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
180 public async stop(): Promise
<void> {
182 throw new BaseError('Cannot stop charging stations simulator from worker thread');
184 if (this.started
=== true) {
185 if (this.stopping
=== false) {
186 this.stopping
= true;
187 await this.uiServer
?.sendInternalRequest(
188 this.uiServer
.buildProtocolRequest(
190 ProcedureName
.STOP_CHARGING_STATION
,
191 Constants
.EMPTY_FROZEN_OBJECT
,
195 waitChargingStationEvents(
197 ChargingStationWorkerMessageEvents
.stopped
,
198 this.numberOfChargingStations
,
200 new Promise
<string>((resolve
) => {
202 const message
= `Timeout ${formatDurationMilliSeconds(
203 Constants.STOP_SIMULATOR_TIMEOUT,
204 )} reached at stopping charging stations simulator`;
205 console
.warn(chalk
.yellow(message
));
207 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
210 await this.workerImplementation
?.stop();
211 this.workerImplementation
= null;
212 this.uiServer
?.stop();
213 await this.storage
?.close();
214 this.resetCounters();
215 this.initializedCounters
= false;
216 this.started
= false;
217 this.stopping
= false;
219 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
222 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
226 public async restart(): Promise
<void> {
231 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
232 let elementsPerWorker
: number | undefined;
233 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
235 this.numberOfChargingStations
> availableParallelism()
236 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
239 this.workerImplementation
=== null &&
240 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
242 workerConfiguration
.processType
!,
244 workerStartDelay
: workerConfiguration
.startDelay
,
245 elementStartDelay
: workerConfiguration
.elementStartDelay
,
246 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
247 poolMinSize
: workerConfiguration
.poolMinSize
!,
248 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
250 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
256 private messageHandler(
257 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
260 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
268 case ChargingStationWorkerMessageEvents
.started
:
269 this.workerEventStarted(msg
.data
as ChargingStationData
);
270 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
272 case ChargingStationWorkerMessageEvents
.stopped
:
273 this.workerEventStopped(msg
.data
as ChargingStationData
);
274 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
276 case ChargingStationWorkerMessageEvents
.updated
:
277 this.workerEventUpdated(msg
.data
as ChargingStationData
);
278 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
280 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
281 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
283 ChargingStationWorkerMessageEvents
.performanceStatistics
,
284 msg
.data
as Statistics
,
287 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
289 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
292 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
294 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
298 `Unknown charging station worker event: '${
300 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
305 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
313 private workerEventStarted
= (data
: ChargingStationData
) => {
314 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
315 ++this.numberOfStartedChargingStations
;
317 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
318 data.stationInfo.chargingStationId
319 } (hashId: ${data.stationInfo.hashId}) started (${
320 this.numberOfStartedChargingStations
321 } started from ${this.numberOfChargingStations})`,
325 private workerEventStopped
= (data
: ChargingStationData
) => {
326 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
327 --this.numberOfStartedChargingStations
;
329 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
330 data.stationInfo.chargingStationId
331 } (hashId: ${data.stationInfo.hashId}) stopped (${
332 this.numberOfStartedChargingStations
333 } started from ${this.numberOfChargingStations})`,
337 private workerEventUpdated
= (data
: ChargingStationData
) => {
338 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
341 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
342 this.storage
.storePerformanceStatistics(data
) as void;
345 private initializeCounters() {
346 if (this.initializedCounters
=== false) {
347 this.resetCounters();
348 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
349 if (isNotEmptyArray(stationTemplateUrls
)) {
350 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
351 for (const stationTemplateUrl
of stationTemplateUrls
) {
352 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
356 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
358 exit(exitCodes
.missingChargingStationsConfiguration
);
360 if (this.numberOfChargingStations
=== 0) {
362 chalk
.yellow('No charging station template enabled in configuration, exiting'),
364 exit(exitCodes
.noChargingStationTemplates
);
366 this.initializedCounters
= true;
370 private resetCounters(): void {
371 this.numberOfChargingStationTemplates
= 0;
372 this.numberOfChargingStations
= 0;
373 this.numberOfStartedChargingStations
= 0;
376 private async startChargingStation(
378 stationTemplateUrl
: StationTemplateUrl
,
380 await this.workerImplementation
?.addElement({
383 dirname(fileURLToPath(import.meta
.url
)),
386 stationTemplateUrl
.file
,
391 private gracefulShutdown
= (): void => {
394 console
.info(`${chalk.green('Graceful shutdown')}`);
395 exit(exitCodes
.succeeded
);
398 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
399 exit(exitCodes
.gracefulShutdownError
);
403 private logPrefix
= (): string => {
404 return logPrefix(' Bootstrap |');