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
, WorkerConstants
, 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 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
235 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
241 private messageHandler(
242 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
245 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
253 case ChargingStationWorkerMessageEvents
.started
:
254 this.workerEventStarted(msg
.data
as ChargingStationData
);
255 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
257 case ChargingStationWorkerMessageEvents
.stopped
:
258 this.workerEventStopped(msg
.data
as ChargingStationData
);
259 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
261 case ChargingStationWorkerMessageEvents
.updated
:
262 this.workerEventUpdated(msg
.data
as ChargingStationData
);
263 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
265 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
266 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
268 ChargingStationWorkerMessageEvents
.performanceStatistics
,
269 msg
.data
as Statistics
,
274 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`,
279 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
287 private workerEventStarted
= (data
: ChargingStationData
) => {
288 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
289 ++this.numberOfStartedChargingStations
;
291 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
292 data.stationInfo.chargingStationId
293 } (hashId: ${data.stationInfo.hashId}) started (${
294 this.numberOfStartedChargingStations
295 } started from ${this.numberOfChargingStations})`,
299 private workerEventStopped
= (data
: ChargingStationData
) => {
300 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
301 --this.numberOfStartedChargingStations
;
303 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
304 data.stationInfo.chargingStationId
305 } (hashId: ${data.stationInfo.hashId}) stopped (${
306 this.numberOfStartedChargingStations
307 } started from ${this.numberOfChargingStations})`,
311 private workerEventUpdated
= (data
: ChargingStationData
) => {
312 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
315 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
316 this.storage
.storePerformanceStatistics(data
) as void;
319 private initializeCounters() {
320 if (this.initializedCounters
=== false) {
321 this.resetCounters();
322 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
323 if (isNotEmptyArray(stationTemplateUrls
)) {
324 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
325 for (const stationTemplateUrl
of stationTemplateUrls
) {
326 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
330 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
332 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
334 if (this.numberOfChargingStations
=== 0) {
336 chalk
.yellow('No charging station template enabled in configuration, exiting'),
338 process
.exit(exitCodes
.noChargingStationTemplates
);
340 this.initializedCounters
= true;
344 private resetCounters(): void {
345 this.numberOfChargingStationTemplates
= 0;
346 this.numberOfChargingStations
= 0;
347 this.numberOfStartedChargingStations
= 0;
350 private async startChargingStation(
352 stationTemplateUrl
: StationTemplateUrl
,
354 await this.workerImplementation
?.addElement({
357 dirname(fileURLToPath(import.meta
.url
)),
360 stationTemplateUrl
.file
,
365 private gracefulShutdown
= (): void => {
366 console
.info(`${chalk.green('Graceful shutdown')}`);
372 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
377 private logPrefix
= (): string => {
378 return logPrefix(' Bootstrap |');