feat: add error handling to worker set worker
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
b4d34251 2
f130b8e6 3import { EventEmitter } from 'node:events';
d972af76 4import { dirname, extname, join } from 'node:path';
130783a7 5import { fileURLToPath } from 'node:url';
be245fda 6import { isMainThread } from 'node:worker_threads';
8114d10e
JB
7
8import chalk from 'chalk';
8603c1ca 9import { availableParallelism } from 'poolifier';
8114d10e 10
b2b60626 11import { waitChargingStationEvents } from './ChargingStationUtils';
4c3c0d59
JB
12import type { AbstractUIServer } from './ui-server/AbstractUIServer';
13import { UIServerFactory } from './ui-server/UIServerFactory';
628c30e5 14import { version } from '../../package.json' assert { type: 'json' };
268a74bb 15import { BaseError } from '../exception';
17bc43d7 16import { type Storage, StorageFactory } from '../performance';
e7aeea18 17import {
bbe10d5f
JB
18 type ChargingStationData,
19 type ChargingStationWorkerData,
20 type ChargingStationWorkerMessage,
21 type ChargingStationWorkerMessageData,
e7aeea18 22 ChargingStationWorkerMessageEvents,
5d049829 23 ConfigurationSection,
6bd808fd 24 ProcedureName,
268a74bb
JB
25 type StationTemplateUrl,
26 type Statistics,
5d049829
JB
27 type StorageConfiguration,
28 type UIServerConfiguration,
29 type WorkerConfiguration,
268a74bb 30} from '../types';
fa5995d6
JB
31import {
32 Configuration,
33 Constants,
9bf0ef23
JB
34 formatDurationMilliSeconds,
35 generateUUID,
fa5995d6
JB
36 handleUncaughtException,
37 handleUnhandledRejection,
9bf0ef23
JB
38 isNotEmptyArray,
39 isNullOrUndefined,
40 logPrefix,
fa5995d6
JB
41 logger,
42} from '../utils';
eda9c451 43import { type WorkerAbstract, WorkerFactory } from '../worker';
ded13d97 44
32de5a57
LM
45const moduleName = 'Bootstrap';
46
a307349b
JB
47enum exitCodes {
48 missingChargingStationsConfiguration = 1,
49 noChargingStationTemplates = 2,
50}
e4cb2c14 51
f130b8e6 52export class Bootstrap extends EventEmitter {
535aaa27 53 private static instance: Bootstrap | null = null;
d1c99c59
JB
54 public numberOfChargingStations!: number;
55 public numberOfChargingStationTemplates!: number;
aa428a31 56 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
551e477c 57 private readonly uiServer!: AbstractUIServer | null;
6a49ad23 58 private readonly storage!: Storage;
89b7a234 59 private numberOfStartedChargingStations!: number;
628c30e5 60 private readonly version: string = version;
a596d200 61 private initializedCounters: boolean;
eb87fe87 62 private started: boolean;
82e9c15a
JB
63 private starting: boolean;
64 private stopping: boolean;
9e23580d 65 private readonly workerScript: string;
ded13d97
JB
66
67 private constructor() {
f130b8e6 68 super();
6bd808fd 69 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
f130b8e6 70 process.on(signal, this.gracefulShutdown);
6bd808fd 71 }
4724a293 72 // Enable unconditionally for now
fa5995d6
JB
73 handleUnhandledRejection();
74 handleUncaughtException();
af8e02ca 75 this.started = false;
82e9c15a
JB
76 this.starting = false;
77 this.stopping = false;
0f040ac0 78 this.initializedCounters = false;
a596d200 79 this.initializeCounters();
af8e02ca 80 this.workerImplementation = null;
d972af76
JB
81 this.workerScript = join(
82 dirname(fileURLToPath(import.meta.url)),
5edd8ba0 83 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
e7aeea18 84 );
864e5f8d
JB
85 const uiServerConfiguration = Configuration.getConfigurationSection<UIServerConfiguration>(
86 ConfigurationSection.uiServer,
87 );
88 uiServerConfiguration.enabled === true &&
89 (this.uiServer = UIServerFactory.getUIServerImplementation(uiServerConfiguration));
90 const performanceStorageConfiguration =
91 Configuration.getConfigurationSection<StorageConfiguration>(
92 ConfigurationSection.performanceStorage,
93 );
94 performanceStorageConfiguration.enabled === true &&
e7aeea18 95 (this.storage = StorageFactory.getStorage(
864e5f8d
JB
96 performanceStorageConfiguration.type!,
97 performanceStorageConfiguration.uri!,
5edd8ba0 98 this.logPrefix(),
e7aeea18 99 ));
7874b0b1 100 Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart());
ded13d97
JB
101 }
102
103 public static getInstance(): Bootstrap {
1ca780f9 104 if (Bootstrap.instance === null) {
ded13d97
JB
105 Bootstrap.instance = new Bootstrap();
106 }
107 return Bootstrap.instance;
108 }
109
110 public async start(): Promise<void> {
ee60150f
JB
111 if (!isMainThread) {
112 throw new Error('Cannot start charging stations simulator from worker thread');
113 }
114 if (this.started === false) {
82e9c15a
JB
115 if (this.starting === false) {
116 this.starting = true;
117 this.initializeCounters();
864e5f8d
JB
118 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
119 ConfigurationSection.worker,
120 );
121 this.initializeWorkerImplementation(workerConfiguration);
82e9c15a
JB
122 await this.workerImplementation?.start();
123 await this.storage?.open();
124 this.uiServer?.start();
125 // Start ChargingStation object instance in worker thread
e1d9a0f4 126 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
82e9c15a
JB
127 try {
128 const nbStations = stationTemplateUrl.numberOfStations ?? 0;
129 for (let index = 1; index <= nbStations; index++) {
130 await this.startChargingStation(index, stationTemplateUrl);
131 }
132 } catch (error) {
133 console.error(
134 chalk.red(
5edd8ba0 135 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
82e9c15a 136 ),
5edd8ba0 137 error,
82e9c15a 138 );
ded13d97 139 }
ded13d97 140 }
82e9c15a
JB
141 console.info(
142 chalk.green(
143 `Charging stations simulator ${
144 this.version
145 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
146 Configuration.workerDynamicPoolInUse()
864e5f8d 147 ? `${workerConfiguration.poolMinSize?.toString()}/`
82e9c15a
JB
148 : ''
149 }${this.workerImplementation?.size}${
150 Configuration.workerPoolInUse()
864e5f8d 151 ? `/${workerConfiguration.poolMaxSize?.toString()}`
82e9c15a 152 : ''
864e5f8d 153 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
9bf0ef23 154 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
82e9c15a
JB
155 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
156 : ''
5edd8ba0
JB
157 }`,
158 ),
82e9c15a 159 );
56e2e1ab
JB
160 Configuration.workerDynamicPoolInUse() &&
161 console.warn(
162 chalk.yellow(
5edd8ba0
JB
163 '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',
164 ),
56e2e1ab 165 );
0bde1ea1 166 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info);
82e9c15a
JB
167 this.started = true;
168 this.starting = false;
169 } else {
170 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
ded13d97 171 }
b322b8b4
JB
172 } else {
173 console.error(chalk.red('Cannot start an already started charging stations simulator'));
ded13d97
JB
174 }
175 }
176
177 public async stop(): Promise<void> {
ee60150f
JB
178 if (!isMainThread) {
179 throw new Error('Cannot stop charging stations simulator from worker thread');
180 }
181 if (this.started === true) {
82e9c15a
JB
182 if (this.stopping === false) {
183 this.stopping = true;
184 await this.uiServer?.sendInternalRequest(
185 this.uiServer.buildProtocolRequest(
9bf0ef23 186 generateUUID(),
82e9c15a 187 ProcedureName.STOP_CHARGING_STATION,
5edd8ba0
JB
188 Constants.EMPTY_FREEZED_OBJECT,
189 ),
82e9c15a 190 );
1832a986 191 await Promise.race([
b2b60626 192 waitChargingStationEvents(
1832a986
JB
193 this,
194 ChargingStationWorkerMessageEvents.stopped,
5edd8ba0 195 this.numberOfChargingStations,
1832a986
JB
196 ),
197 new Promise<string>((resolve) => {
198 setTimeout(() => {
9bf0ef23 199 const message = `Timeout reached ${formatDurationMilliSeconds(
5edd8ba0 200 Constants.STOP_SIMULATOR_TIMEOUT,
1832a986
JB
201 )} at stopping charging stations simulator`;
202 console.warn(chalk.yellow(message));
203 resolve(message);
204 }, Constants.STOP_SIMULATOR_TIMEOUT);
205 }),
206 ]);
82e9c15a
JB
207 await this.workerImplementation?.stop();
208 this.workerImplementation = null;
209 this.uiServer?.stop();
210 await this.storage?.close();
211 this.resetCounters();
212 this.initializedCounters = false;
213 this.started = false;
214 this.stopping = false;
215 } else {
216 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
217 }
b322b8b4 218 } else {
82e9c15a 219 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
ded13d97 220 }
ded13d97
JB
221 }
222
223 public async restart(): Promise<void> {
224 await this.stop();
225 await this.start();
226 }
227
864e5f8d 228 private initializeWorkerImplementation(workerConfiguration: WorkerConfiguration): void {
e1d9a0f4 229 let elementsPerWorker: number | undefined;
864e5f8d 230 if (workerConfiguration?.elementsPerWorker === 'auto') {
34c200d5
JB
231 elementsPerWorker =
232 this.numberOfChargingStations > availableParallelism()
233 ? Math.round(this.numberOfChargingStations / availableParallelism())
234 : 1;
8603c1ca 235 }
e2c77f10 236 this.workerImplementation === null &&
ec7f4dce
JB
237 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
238 this.workerScript,
864e5f8d 239 workerConfiguration.processType!,
ec7f4dce 240 {
864e5f8d
JB
241 workerStartDelay: workerConfiguration.startDelay,
242 elementStartDelay: workerConfiguration.elementStartDelay,
243 poolMaxSize: workerConfiguration.poolMaxSize!,
244 poolMinSize: workerConfiguration.poolMinSize!,
245 elementsPerWorker: elementsPerWorker ?? (workerConfiguration.elementsPerWorker as number),
ec7f4dce 246 poolOptions: {
be245fda 247 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
ec7f4dce 248 },
5edd8ba0 249 },
ec7f4dce 250 ));
ded13d97 251 }
81797102 252
32de5a57 253 private messageHandler(
5edd8ba0 254 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>,
32de5a57
LM
255 ): void {
256 // logger.debug(
257 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
258 // msg,
259 // null,
e1d9a0f4
JB
260 // 2,
261 // )}`,
32de5a57
LM
262 // );
263 try {
8cc482a9 264 switch (msg.event) {
721646e9 265 case ChargingStationWorkerMessageEvents.started:
32de5a57 266 this.workerEventStarted(msg.data as ChargingStationData);
f130b8e6 267 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
32de5a57 268 break;
721646e9 269 case ChargingStationWorkerMessageEvents.stopped:
32de5a57 270 this.workerEventStopped(msg.data as ChargingStationData);
f130b8e6 271 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
32de5a57 272 break;
721646e9 273 case ChargingStationWorkerMessageEvents.updated:
32de5a57 274 this.workerEventUpdated(msg.data as ChargingStationData);
f130b8e6 275 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
32de5a57 276 break;
721646e9 277 case ChargingStationWorkerMessageEvents.performanceStatistics:
32de5a57 278 this.workerEventPerformanceStatistics(msg.data as Statistics);
f130b8e6
JB
279 this.emit(
280 ChargingStationWorkerMessageEvents.performanceStatistics,
5edd8ba0 281 msg.data as Statistics,
f130b8e6 282 );
32de5a57 283 break;
2bb7a73e
JB
284 case ChargingStationWorkerMessageEvents.startWorkerElementError:
285 logger.error(
286 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
287 msg.data,
288 );
289 this.emit(ChargingStationWorkerMessageEvents.startWorkerElementError, msg.data);
290 break;
291 case ChargingStationWorkerMessageEvents.startedWorkerElement:
292 break;
32de5a57
LM
293 default:
294 throw new BaseError(
8cc482a9 295 `Unknown event type: '${msg.event}' for data: ${JSON.stringify(msg.data, null, 2)}`,
32de5a57
LM
296 );
297 }
298 } catch (error) {
299 logger.error(
300 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 301 msg.event
32de5a57 302 }' event:`,
5edd8ba0 303 error,
32de5a57
LM
304 );
305 }
306 }
307
e2c77f10 308 private workerEventStarted = (data: ChargingStationData) => {
51c83d6f 309 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 310 ++this.numberOfStartedChargingStations;
56eb297e 311 logger.info(
e6159ce8 312 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 313 data.stationInfo.chargingStationId
e6159ce8 314 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 315 this.numberOfStartedChargingStations
5edd8ba0 316 } started from ${this.numberOfChargingStations})`,
56eb297e 317 );
e2c77f10 318 };
32de5a57 319
e2c77f10 320 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 321 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 322 --this.numberOfStartedChargingStations;
56eb297e 323 logger.info(
e6159ce8 324 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 325 data.stationInfo.chargingStationId
e6159ce8 326 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 327 this.numberOfStartedChargingStations
5edd8ba0 328 } started from ${this.numberOfChargingStations})`,
56eb297e 329 );
e2c77f10 330 };
32de5a57 331
e2c77f10 332 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 333 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 334 };
32de5a57
LM
335
336 private workerEventPerformanceStatistics = (data: Statistics) => {
337 this.storage.storePerformanceStatistics(data) as void;
338 };
339
326cec2d 340 private initializeCounters() {
a596d200 341 if (this.initializedCounters === false) {
0f040ac0 342 this.resetCounters();
e1d9a0f4 343 const stationTemplateUrls = Configuration.getStationTemplateUrls()!;
9bf0ef23 344 if (isNotEmptyArray(stationTemplateUrls)) {
41bda658 345 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 346 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 347 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 348 }
a596d200
JB
349 } else {
350 console.warn(
5edd8ba0 351 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
a596d200
JB
352 );
353 process.exit(exitCodes.missingChargingStationsConfiguration);
354 }
355 if (this.numberOfChargingStations === 0) {
356 console.warn(
5edd8ba0 357 chalk.yellow('No charging station template enabled in configuration, exiting'),
a596d200
JB
358 );
359 process.exit(exitCodes.noChargingStationTemplates);
360 }
a596d200 361 this.initializedCounters = true;
846d2851 362 }
7c72977b
JB
363 }
364
0f040ac0
JB
365 private resetCounters(): void {
366 this.numberOfChargingStationTemplates = 0;
367 this.numberOfChargingStations = 0;
368 this.numberOfStartedChargingStations = 0;
369 }
370
e7aeea18
JB
371 private async startChargingStation(
372 index: number,
5edd8ba0 373 stationTemplateUrl: StationTemplateUrl,
e7aeea18 374 ): Promise<void> {
6ed3c845 375 await this.workerImplementation?.addElement({
717c1e56 376 index,
d972af76
JB
377 templateFile: join(
378 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
379 'assets',
380 'station-templates',
5edd8ba0 381 stationTemplateUrl.file,
e7aeea18 382 ),
6ed3c845 383 });
717c1e56
JB
384 }
385
f130b8e6 386 private gracefulShutdown = (): void => {
6bd808fd 387 console.info(`${chalk.green('Graceful shutdown')}`);
f130b8e6
JB
388 this.stop()
389 .then(() => {
390 process.exit(0);
391 })
392 .catch((error) => {
fca8bc64 393 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
f130b8e6
JB
394 process.exit(1);
395 });
396 };
397
8b7072dc 398 private logPrefix = (): string => {
9bf0ef23 399 return logPrefix(' Bootstrap |');
8b7072dc 400 };
ded13d97 401}