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
'./ChargingStationUtils';
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
,
24 type StationTemplateUrl
,
30 formatDurationMilliSeconds
,
32 handleUncaughtException
,
33 handleUnhandledRejection
,
39 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
41 const moduleName
= 'Bootstrap';
44 missingChargingStationsConfiguration
= 1,
45 noChargingStationTemplates
= 2,
48 export class Bootstrap
extends EventEmitter
{
49 private static instance
: Bootstrap
| null = null;
50 public numberOfChargingStations
!: number;
51 public numberOfChargingStationTemplates
!: number;
52 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
53 private readonly uiServer
!: AbstractUIServer
| null;
54 private readonly storage
!: Storage
;
55 private numberOfStartedChargingStations
!: number;
56 private readonly version
: string = version
;
57 private initializedCounters
: boolean;
58 private started
: boolean;
59 private starting
: boolean;
60 private stopping
: boolean;
61 private readonly workerScript
: string;
63 private constructor() {
65 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
66 process
.on(signal
, this.gracefulShutdown
);
68 // Enable unconditionally for now
69 handleUnhandledRejection();
70 handleUncaughtException();
72 this.starting
= false;
73 this.stopping
= false;
74 this.initializedCounters
= false;
75 this.initializeCounters();
76 this.workerImplementation
= null;
77 this.workerScript
= join(
78 dirname(fileURLToPath(import.meta
.url
)),
79 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
81 Configuration
.getUIServer().enabled
=== true &&
82 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
83 Configuration
.getPerformanceStorage().enabled
=== true &&
84 (this.storage
= StorageFactory
.getStorage(
85 Configuration
.getPerformanceStorage().type,
86 Configuration
.getPerformanceStorage().uri
,
89 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
92 public static getInstance(): Bootstrap
{
93 if (Bootstrap
.instance
=== null) {
94 Bootstrap
.instance
= new Bootstrap();
96 return Bootstrap
.instance
;
99 public async start(): Promise
<void> {
101 throw new Error('Cannot start charging stations simulator from worker thread');
103 if (this.started
=== false) {
104 if (this.starting
=== false) {
105 this.starting
= true;
106 this.initializeCounters();
107 this.initializeWorkerImplementation();
108 await this.workerImplementation
?.start();
109 await this.storage
?.open();
110 this.uiServer
?.start();
111 // Start ChargingStation object instance in worker thread
112 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
114 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
115 for (let index
= 1; index
<= nbStations
; index
++) {
116 await this.startChargingStation(index
, stationTemplateUrl
);
121 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
129 `Charging stations simulator ${
131 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
132 Configuration.workerDynamicPoolInUse()
133 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
135 }${this.workerImplementation?.size}${
136 Configuration.workerPoolInUse()
137 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
139 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
140 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
141 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
146 Configuration
.workerDynamicPoolInUse() &&
149 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using static pool or worker set mode instead',
152 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
154 this.starting
= false;
156 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
159 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
163 public async stop(): Promise
<void> {
165 throw new Error('Cannot stop charging stations simulator from worker thread');
167 if (this.started
=== true) {
168 if (this.stopping
=== false) {
169 this.stopping
= true;
170 await this.uiServer
?.sendInternalRequest(
171 this.uiServer
.buildProtocolRequest(
173 ProcedureName
.STOP_CHARGING_STATION
,
174 Constants
.EMPTY_FREEZED_OBJECT
,
178 waitChargingStationEvents(
180 ChargingStationWorkerMessageEvents
.stopped
,
181 this.numberOfChargingStations
,
183 new Promise
<string>((resolve
) => {
185 const message
= `Timeout reached ${formatDurationMilliSeconds(
186 Constants.STOP_SIMULATOR_TIMEOUT,
187 )} at stopping charging stations simulator`;
188 console
.warn(chalk
.yellow(message
));
190 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
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(): Promise
<void> {
214 private initializeWorkerImplementation(): void {
215 let elementsPerWorker
: number;
216 if (Configuration
.getWorker()?.elementsPerWorker
=== 'auto') {
218 this.numberOfChargingStations
> availableParallelism()
219 ? Math.round(this.numberOfChargingStations
/ availableParallelism())
222 this.workerImplementation
=== null &&
223 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
225 Configuration
.getWorker().processType
,
227 workerStartDelay
: Configuration
.getWorker().startDelay
,
228 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
229 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
230 poolMinSize
: Configuration
.getWorker().poolMinSize
,
232 elementsPerWorker
?? (Configuration
.getWorker().elementsPerWorker
as number),
234 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
240 private messageHandler(
241 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
244 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
252 case ChargingStationWorkerMessageEvents
.started
:
253 this.workerEventStarted(msg
.data
as ChargingStationData
);
254 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
256 case ChargingStationWorkerMessageEvents
.stopped
:
257 this.workerEventStopped(msg
.data
as ChargingStationData
);
258 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
260 case ChargingStationWorkerMessageEvents
.updated
:
261 this.workerEventUpdated(msg
.data
as ChargingStationData
);
262 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
264 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
265 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
267 ChargingStationWorkerMessageEvents
.performanceStatistics
,
268 msg
.data
as Statistics
,
273 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`,
278 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
286 private workerEventStarted
= (data
: ChargingStationData
) => {
287 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
288 ++this.numberOfStartedChargingStations
;
290 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
291 data.stationInfo.chargingStationId
292 } (hashId: ${data.stationInfo.hashId}) started (${
293 this.numberOfStartedChargingStations
294 } started from ${this.numberOfChargingStations})`,
298 private workerEventStopped
= (data
: ChargingStationData
) => {
299 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
300 --this.numberOfStartedChargingStations
;
302 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
303 data.stationInfo.chargingStationId
304 } (hashId: ${data.stationInfo.hashId}) stopped (${
305 this.numberOfStartedChargingStations
306 } started from ${this.numberOfChargingStations})`,
310 private workerEventUpdated
= (data
: ChargingStationData
) => {
311 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
314 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
315 this.storage
.storePerformanceStatistics(data
) as void;
318 private initializeCounters() {
319 if (this.initializedCounters
=== false) {
320 this.resetCounters();
321 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
322 if (isNotEmptyArray(stationTemplateUrls
)) {
323 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
324 for (const stationTemplateUrl
of stationTemplateUrls
) {
325 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
329 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
331 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
333 if (this.numberOfChargingStations
=== 0) {
335 chalk
.yellow('No charging station template enabled in configuration, exiting'),
337 process
.exit(exitCodes
.noChargingStationTemplates
);
339 this.initializedCounters
= true;
343 private resetCounters(): void {
344 this.numberOfChargingStationTemplates
= 0;
345 this.numberOfChargingStations
= 0;
346 this.numberOfStartedChargingStations
= 0;
349 private async startChargingStation(
351 stationTemplateUrl
: StationTemplateUrl
,
353 await this.workerImplementation
?.addElement({
356 dirname(fileURLToPath(import.meta
.url
)),
359 stationTemplateUrl
.file
,
364 private gracefulShutdown
= (): void => {
365 console
.info(`${chalk.green('Graceful shutdown')}`);
371 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
376 private logPrefix
= (): string => {
377 return logPrefix(' Bootstrap |');