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 { fileURLToPath
} from
'node:url';
6 import { isMainThread
} from
'node:worker_threads';
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' assert
{ type: '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
);
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 const uiServerConfiguration
= Configuration
.getConfigurationSection
<UIServerConfiguration
>(
88 ConfigurationSection
.uiServer
,
90 uiServerConfiguration
.enabled
=== true &&
91 (this.uiServer
= UIServerFactory
.getUIServerImplementation(uiServerConfiguration
));
92 const performanceStorageConfiguration
=
93 Configuration
.getConfigurationSection
<StorageConfiguration
>(
94 ConfigurationSection
.performanceStorage
,
96 performanceStorageConfiguration
.enabled
=== true &&
97 (this.storage
= StorageFactory
.getStorage(
98 performanceStorageConfiguration
.type!,
99 performanceStorageConfiguration
.uri
!,
102 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart();
105 public static getInstance(): Bootstrap
{
106 if (Bootstrap
.instance
=== null) {
107 Bootstrap
.instance
= new Bootstrap();
109 return Bootstrap
.instance
;
112 public async start(): Promise
<void> {
114 throw new BaseError('Cannot start charging stations simulator from worker thread');
116 if (this.started
=== false) {
117 if (this.starting
=== false) {
118 this.starting
= true;
119 this.initializeCounters();
120 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
121 ConfigurationSection
.worker
,
123 this.initializeWorkerImplementation(workerConfiguration
);
124 await this.workerImplementation
?.start();
125 await this.storage
?.open();
126 this.uiServer
?.start();
127 // Start ChargingStation object instance in worker thread
128 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
130 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
131 for (let index
= 1; index
<= nbStations
; index
++) {
132 await this.startChargingStation(index
, stationTemplateUrl
);
137 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
145 `Charging stations simulator ${
147 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
148 Configuration.workerDynamicPoolInUse()
149 ? `${workerConfiguration.poolMinSize?.toString()}
/`
151 }${this.workerImplementation?.size}${
152 Configuration.workerPoolInUse()
153 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
155 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
156 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
157 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
162 Configuration
.workerDynamicPoolInUse() &&
165 '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',
168 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
170 this.starting
= false;
172 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
175 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
179 public async stop(): Promise
<void> {
181 throw new BaseError('Cannot stop charging stations simulator from worker thread');
183 if (this.started
=== true) {
184 if (this.stopping
=== false) {
185 this.stopping
= true;
186 await this.uiServer
?.sendInternalRequest(
187 this.uiServer
.buildProtocolRequest(
189 ProcedureName
.STOP_CHARGING_STATION
,
190 Constants
.EMPTY_FROZEN_OBJECT
,
194 waitChargingStationEvents(
196 ChargingStationWorkerMessageEvents
.stopped
,
197 this.numberOfChargingStations
,
199 new Promise
<string>((resolve
) => {
201 const message
= `Timeout reached ${formatDurationMilliSeconds(
202 Constants.STOP_SIMULATOR_TIMEOUT,
203 )} at stopping charging stations simulator`;
204 console
.warn(chalk
.yellow(message
));
206 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
209 await this.workerImplementation
?.stop();
210 this.workerImplementation
= null;
211 this.uiServer
?.stop();
212 await this.storage
?.close();
213 this.resetCounters();
214 this.initializedCounters
= false;
215 this.started
= false;
216 this.stopping
= false;
218 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
221 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
225 public async restart(): Promise
<void> {
230 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
231 let elementsPerWorker
: number | undefined;
232 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
234 this.numberOfChargingStations
> availableParallelism()
235 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
238 this.workerImplementation
=== null &&
239 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
241 workerConfiguration
.processType
!,
243 workerStartDelay
: workerConfiguration
.startDelay
,
244 elementStartDelay
: workerConfiguration
.elementStartDelay
,
245 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
246 poolMinSize
: workerConfiguration
.poolMinSize
!,
247 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
249 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
255 private messageHandler(
256 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
259 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
267 case ChargingStationWorkerMessageEvents
.started
:
268 this.workerEventStarted(msg
.data
as ChargingStationData
);
269 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
271 case ChargingStationWorkerMessageEvents
.stopped
:
272 this.workerEventStopped(msg
.data
as ChargingStationData
);
273 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
275 case ChargingStationWorkerMessageEvents
.updated
:
276 this.workerEventUpdated(msg
.data
as ChargingStationData
);
277 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
279 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
280 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
282 ChargingStationWorkerMessageEvents
.performanceStatistics
,
283 msg
.data
as Statistics
,
286 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
288 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
291 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
293 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
297 `Unknown charging station worker event: '${
299 }' received with data: ${JSON.stringify(msg.data, null, 2)}`,
304 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
312 private workerEventStarted
= (data
: ChargingStationData
) => {
313 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
314 ++this.numberOfStartedChargingStations
;
316 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
317 data.stationInfo.chargingStationId
318 } (hashId: ${data.stationInfo.hashId}) started (${
319 this.numberOfStartedChargingStations
320 } started from ${this.numberOfChargingStations})`,
324 private workerEventStopped
= (data
: ChargingStationData
) => {
325 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
326 --this.numberOfStartedChargingStations
;
328 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
329 data.stationInfo.chargingStationId
330 } (hashId: ${data.stationInfo.hashId}) stopped (${
331 this.numberOfStartedChargingStations
332 } started from ${this.numberOfChargingStations})`,
336 private workerEventUpdated
= (data
: ChargingStationData
) => {
337 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
340 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
341 this.storage
.storePerformanceStatistics(data
) as void;
344 private initializeCounters() {
345 if (this.initializedCounters
=== false) {
346 this.resetCounters();
347 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
348 if (isNotEmptyArray(stationTemplateUrls
)) {
349 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
350 for (const stationTemplateUrl
of stationTemplateUrls
) {
351 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
355 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
357 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
359 if (this.numberOfChargingStations
=== 0) {
361 chalk
.yellow('No charging station template enabled in configuration, exiting'),
363 process
.exit(exitCodes
.noChargingStationTemplates
);
365 this.initializedCounters
= true;
369 private resetCounters(): void {
370 this.numberOfChargingStationTemplates
= 0;
371 this.numberOfChargingStations
= 0;
372 this.numberOfStartedChargingStations
= 0;
375 private async startChargingStation(
377 stationTemplateUrl
: StationTemplateUrl
,
379 await this.workerImplementation
?.addElement({
382 dirname(fileURLToPath(import.meta
.url
)),
385 stationTemplateUrl
.file
,
390 private gracefulShutdown
= (): void => {
391 console
.info(`${chalk.green('Graceful shutdown')}`);
394 process
.exit(exitCodes
.succeeded
);
397 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
398 process
.exit(exitCodes
.gracefulShutdownError
);
402 private logPrefix
= (): string => {
403 return logPrefix(' Bootstrap |');