fix(simulator): remove unneeded main thread check at simulator stop
[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';
10687422 5import { exit } from 'node:process';
130783a7 6import { fileURLToPath } from 'node:url';
be245fda 7import { isMainThread } from 'node:worker_threads';
8114d10e
JB
8
9import chalk from 'chalk';
8603c1ca 10import { availableParallelism } from 'poolifier';
8114d10e 11
08b58f00 12import { waitChargingStationEvents } from './Helpers';
4c3c0d59
JB
13import type { AbstractUIServer } from './ui-server/AbstractUIServer';
14import { UIServerFactory } from './ui-server/UIServerFactory';
44ebef4c 15import { version } from '../../package.json';
268a74bb 16import { BaseError } from '../exception';
17bc43d7 17import { type Storage, StorageFactory } from '../performance';
e7aeea18 18import {
bbe10d5f
JB
19 type ChargingStationData,
20 type ChargingStationWorkerData,
21 type ChargingStationWorkerMessage,
22 type ChargingStationWorkerMessageData,
e7aeea18 23 ChargingStationWorkerMessageEvents,
5d049829 24 ConfigurationSection,
6bd808fd 25 ProcedureName,
268a74bb
JB
26 type StationTemplateUrl,
27 type Statistics,
5d049829
JB
28 type StorageConfiguration,
29 type UIServerConfiguration,
30 type WorkerConfiguration,
268a74bb 31} from '../types';
fa5995d6
JB
32import {
33 Configuration,
34 Constants,
9bf0ef23
JB
35 formatDurationMilliSeconds,
36 generateUUID,
fa5995d6
JB
37 handleUncaughtException,
38 handleUnhandledRejection,
9bf0ef23
JB
39 isNotEmptyArray,
40 isNullOrUndefined,
41 logPrefix,
fa5995d6
JB
42 logger,
43} from '../utils';
eda9c451 44import { type WorkerAbstract, WorkerFactory } from '../worker';
ded13d97 45
32de5a57
LM
46const moduleName = 'Bootstrap';
47
a307349b 48enum exitCodes {
a51a4ead 49 succeeded = 0,
a307349b
JB
50 missingChargingStationsConfiguration = 1,
51 noChargingStationTemplates = 2,
a51a4ead 52 gracefulShutdownError = 3,
a307349b 53}
e4cb2c14 54
f130b8e6 55export class Bootstrap extends EventEmitter {
535aaa27 56 private static instance: Bootstrap | null = null;
d1c99c59
JB
57 public numberOfChargingStations!: number;
58 public numberOfChargingStationTemplates!: number;
aa428a31 59 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
551e477c 60 private readonly uiServer!: AbstractUIServer | null;
6a49ad23 61 private readonly storage!: Storage;
89b7a234 62 private numberOfStartedChargingStations!: number;
628c30e5 63 private readonly version: string = version;
a596d200 64 private initializedCounters: boolean;
eb87fe87 65 private started: boolean;
82e9c15a
JB
66 private starting: boolean;
67 private stopping: boolean;
9e23580d 68 private readonly workerScript: string;
ded13d97
JB
69
70 private constructor() {
f130b8e6 71 super();
6bd808fd 72 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
f130b8e6 73 process.on(signal, this.gracefulShutdown);
6bd808fd 74 }
4724a293 75 // Enable unconditionally for now
fa5995d6
JB
76 handleUnhandledRejection();
77 handleUncaughtException();
af8e02ca 78 this.started = false;
82e9c15a
JB
79 this.starting = false;
80 this.stopping = false;
0f040ac0 81 this.initializedCounters = false;
a596d200 82 this.initializeCounters();
af8e02ca 83 this.workerImplementation = null;
d972af76
JB
84 this.workerScript = join(
85 dirname(fileURLToPath(import.meta.url)),
5edd8ba0 86 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
e7aeea18 87 );
864e5f8d
JB
88 const uiServerConfiguration = Configuration.getConfigurationSection<UIServerConfiguration>(
89 ConfigurationSection.uiServer,
90 );
91 uiServerConfiguration.enabled === true &&
92 (this.uiServer = UIServerFactory.getUIServerImplementation(uiServerConfiguration));
93 const performanceStorageConfiguration =
94 Configuration.getConfigurationSection<StorageConfiguration>(
95 ConfigurationSection.performanceStorage,
96 );
97 performanceStorageConfiguration.enabled === true &&
e7aeea18 98 (this.storage = StorageFactory.getStorage(
864e5f8d
JB
99 performanceStorageConfiguration.type!,
100 performanceStorageConfiguration.uri!,
5edd8ba0 101 this.logPrefix(),
e7aeea18 102 ));
9f139e99 103 Configuration.configurationChangeCallback = async () => Bootstrap.getInstance().restart();
ded13d97
JB
104 }
105
106 public static getInstance(): Bootstrap {
1ca780f9 107 if (Bootstrap.instance === null) {
ded13d97
JB
108 Bootstrap.instance = new Bootstrap();
109 }
110 return Bootstrap.instance;
111 }
112
113 public async start(): Promise<void> {
ee60150f 114 if (!isMainThread) {
a6080904 115 throw new BaseError('Cannot start charging stations simulator from worker thread');
ee60150f
JB
116 }
117 if (this.started === false) {
82e9c15a
JB
118 if (this.starting === false) {
119 this.starting = true;
120 this.initializeCounters();
864e5f8d
JB
121 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
122 ConfigurationSection.worker,
123 );
124 this.initializeWorkerImplementation(workerConfiguration);
82e9c15a
JB
125 await this.workerImplementation?.start();
126 await this.storage?.open();
127 this.uiServer?.start();
128 // Start ChargingStation object instance in worker thread
e1d9a0f4 129 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
82e9c15a
JB
130 try {
131 const nbStations = stationTemplateUrl.numberOfStations ?? 0;
132 for (let index = 1; index <= nbStations; index++) {
133 await this.startChargingStation(index, stationTemplateUrl);
134 }
135 } catch (error) {
136 console.error(
137 chalk.red(
5edd8ba0 138 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
82e9c15a 139 ),
5edd8ba0 140 error,
82e9c15a 141 );
ded13d97 142 }
ded13d97 143 }
82e9c15a
JB
144 console.info(
145 chalk.green(
146 `Charging stations simulator ${
147 this.version
148 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
149 Configuration.workerDynamicPoolInUse()
864e5f8d 150 ? `${workerConfiguration.poolMinSize?.toString()}/`
82e9c15a
JB
151 : ''
152 }${this.workerImplementation?.size}${
153 Configuration.workerPoolInUse()
864e5f8d 154 ? `/${workerConfiguration.poolMaxSize?.toString()}`
82e9c15a 155 : ''
864e5f8d 156 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
9bf0ef23 157 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
82e9c15a
JB
158 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
159 : ''
5edd8ba0
JB
160 }`,
161 ),
82e9c15a 162 );
56e2e1ab
JB
163 Configuration.workerDynamicPoolInUse() &&
164 console.warn(
165 chalk.yellow(
1d8f226b 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',
5edd8ba0 167 ),
56e2e1ab 168 );
0bde1ea1 169 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info);
82e9c15a
JB
170 this.started = true;
171 this.starting = false;
172 } else {
173 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
ded13d97 174 }
b322b8b4
JB
175 } else {
176 console.error(chalk.red('Cannot start an already started charging stations simulator'));
ded13d97
JB
177 }
178 }
179
180 public async stop(): Promise<void> {
ee60150f 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,
2035255d 188 Constants.EMPTY_FROZEN_OBJECT,
5edd8ba0 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(() => {
6ebeab4b 199 const message = `Timeout ${formatDurationMilliSeconds(
5edd8ba0 200 Constants.STOP_SIMULATOR_TIMEOUT,
6ebeab4b 201 )} reached at stopping charging stations simulator`;
1832a986
JB
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()
411f6bb4 233 ? Math.round(this.numberOfChargingStations / (availableParallelism() * 1.5))
34c200d5 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,
4ed03b6e 259 // undefined,
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(
f93dda6a
JB
295 `Unknown charging station worker event: '${
296 msg.event
4ed03b6e 297 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
32de5a57
LM
298 );
299 }
300 } catch (error) {
301 logger.error(
302 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 303 msg.event
32de5a57 304 }' event:`,
5edd8ba0 305 error,
32de5a57
LM
306 );
307 }
308 }
309
e2c77f10 310 private workerEventStarted = (data: ChargingStationData) => {
51c83d6f 311 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 312 ++this.numberOfStartedChargingStations;
56eb297e 313 logger.info(
e6159ce8 314 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 315 data.stationInfo.chargingStationId
e6159ce8 316 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 317 this.numberOfStartedChargingStations
5edd8ba0 318 } started from ${this.numberOfChargingStations})`,
56eb297e 319 );
e2c77f10 320 };
32de5a57 321
e2c77f10 322 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 323 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 324 --this.numberOfStartedChargingStations;
56eb297e 325 logger.info(
e6159ce8 326 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 327 data.stationInfo.chargingStationId
e6159ce8 328 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 329 this.numberOfStartedChargingStations
5edd8ba0 330 } started from ${this.numberOfChargingStations})`,
56eb297e 331 );
e2c77f10 332 };
32de5a57 333
e2c77f10 334 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 335 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 336 };
32de5a57
LM
337
338 private workerEventPerformanceStatistics = (data: Statistics) => {
339 this.storage.storePerformanceStatistics(data) as void;
340 };
341
326cec2d 342 private initializeCounters() {
a596d200 343 if (this.initializedCounters === false) {
0f040ac0 344 this.resetCounters();
e1d9a0f4 345 const stationTemplateUrls = Configuration.getStationTemplateUrls()!;
9bf0ef23 346 if (isNotEmptyArray(stationTemplateUrls)) {
41bda658 347 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 348 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 349 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 350 }
a596d200
JB
351 } else {
352 console.warn(
5edd8ba0 353 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
a596d200 354 );
10687422 355 exit(exitCodes.missingChargingStationsConfiguration);
a596d200
JB
356 }
357 if (this.numberOfChargingStations === 0) {
358 console.warn(
5edd8ba0 359 chalk.yellow('No charging station template enabled in configuration, exiting'),
a596d200 360 );
10687422 361 exit(exitCodes.noChargingStationTemplates);
a596d200 362 }
a596d200 363 this.initializedCounters = true;
846d2851 364 }
7c72977b
JB
365 }
366
0f040ac0
JB
367 private resetCounters(): void {
368 this.numberOfChargingStationTemplates = 0;
369 this.numberOfChargingStations = 0;
370 this.numberOfStartedChargingStations = 0;
371 }
372
e7aeea18
JB
373 private async startChargingStation(
374 index: number,
5edd8ba0 375 stationTemplateUrl: StationTemplateUrl,
e7aeea18 376 ): Promise<void> {
6ed3c845 377 await this.workerImplementation?.addElement({
717c1e56 378 index,
d972af76
JB
379 templateFile: join(
380 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
381 'assets',
382 'station-templates',
5edd8ba0 383 stationTemplateUrl.file,
e7aeea18 384 ),
6ed3c845 385 });
717c1e56
JB
386 }
387
f130b8e6 388 private gracefulShutdown = (): void => {
f130b8e6
JB
389 this.stop()
390 .then(() => {
83a36f14 391 console.info(`${chalk.green('Graceful shutdown')}`);
10687422 392 exit(exitCodes.succeeded);
f130b8e6
JB
393 })
394 .catch((error) => {
fca8bc64 395 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
10687422 396 exit(exitCodes.gracefulShutdownError);
f130b8e6
JB
397 });
398 };
399
8b7072dc 400 private logPrefix = (): string => {
9bf0ef23 401 return logPrefix(' Bootstrap |');
8b7072dc 402 };
ded13d97 403}