fix: fix cf push by conditionnaly importing husky
[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';
36adaf06 5import process, { exit } from 'node:process';
130783a7 6import { fileURLToPath } from 'node:url';
8114d10e
JB
7
8import chalk from 'chalk';
8603c1ca 9import { availableParallelism } from 'poolifier';
8114d10e 10
08b58f00 11import { waitChargingStationEvents } from './Helpers';
4c3c0d59
JB
12import type { AbstractUIServer } from './ui-server/AbstractUIServer';
13import { UIServerFactory } from './ui-server/UIServerFactory';
44ebef4c 14import { version } from '../../package.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 47enum exitCodes {
a51a4ead 48 succeeded = 0,
a307349b
JB
49 missingChargingStationsConfiguration = 1,
50 noChargingStationTemplates = 2,
a51a4ead 51 gracefulShutdownError = 3,
a307349b 52}
e4cb2c14 53
f130b8e6 54export class Bootstrap extends EventEmitter {
535aaa27 55 private static instance: Bootstrap | null = null;
d1c99c59
JB
56 public numberOfChargingStations!: number;
57 public numberOfChargingStationTemplates!: number;
aa428a31 58 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
36adaf06 59 private readonly uiServer: AbstractUIServer | null;
6a49ad23 60 private readonly storage!: Storage;
89b7a234 61 private numberOfStartedChargingStations!: number;
628c30e5 62 private readonly version: string = version;
a596d200 63 private initializedCounters: boolean;
eb87fe87 64 private started: boolean;
82e9c15a
JB
65 private starting: boolean;
66 private stopping: boolean;
9e23580d 67 private readonly workerScript: string;
ded13d97
JB
68
69 private constructor() {
f130b8e6 70 super();
6bd808fd 71 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
36adaf06 72 process.on(signal, this.gracefulShutdown.bind(this));
6bd808fd 73 }
4724a293 74 // Enable unconditionally for now
fa5995d6
JB
75 handleUnhandledRejection();
76 handleUncaughtException();
af8e02ca 77 this.started = false;
82e9c15a
JB
78 this.starting = false;
79 this.stopping = false;
0f040ac0 80 this.initializedCounters = false;
a596d200 81 this.initializeCounters();
af8e02ca 82 this.workerImplementation = null;
d972af76
JB
83 this.workerScript = join(
84 dirname(fileURLToPath(import.meta.url)),
5edd8ba0 85 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
e7aeea18 86 );
36adaf06
JB
87 this.uiServer = UIServerFactory.getUIServerImplementation(
88 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer),
864e5f8d 89 );
864e5f8d
JB
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 ));
b3b3f0eb 100 Configuration.configurationChangeCallback = async () => Bootstrap.getInstance().restart(false);
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 111 if (this.started === false) {
82e9c15a
JB
112 if (this.starting === false) {
113 this.starting = true;
114 this.initializeCounters();
864e5f8d
JB
115 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
116 ConfigurationSection.worker,
117 );
118 this.initializeWorkerImplementation(workerConfiguration);
82e9c15a
JB
119 await this.workerImplementation?.start();
120 await this.storage?.open();
36adaf06
JB
121 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
122 .enabled === true && this.uiServer?.start();
82e9c15a 123 // Start ChargingStation object instance in worker thread
e1d9a0f4 124 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
82e9c15a
JB
125 try {
126 const nbStations = stationTemplateUrl.numberOfStations ?? 0;
127 for (let index = 1; index <= nbStations; index++) {
128 await this.startChargingStation(index, stationTemplateUrl);
129 }
130 } catch (error) {
131 console.error(
132 chalk.red(
5edd8ba0 133 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
82e9c15a 134 ),
5edd8ba0 135 error,
82e9c15a 136 );
ded13d97 137 }
ded13d97 138 }
82e9c15a
JB
139 console.info(
140 chalk.green(
141 `Charging stations simulator ${
142 this.version
143 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
144 Configuration.workerDynamicPoolInUse()
864e5f8d 145 ? `${workerConfiguration.poolMinSize?.toString()}/`
82e9c15a
JB
146 : ''
147 }${this.workerImplementation?.size}${
148 Configuration.workerPoolInUse()
864e5f8d 149 ? `/${workerConfiguration.poolMaxSize?.toString()}`
82e9c15a 150 : ''
864e5f8d 151 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
9bf0ef23 152 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
82e9c15a
JB
153 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
154 : ''
5edd8ba0
JB
155 }`,
156 ),
82e9c15a 157 );
56e2e1ab
JB
158 Configuration.workerDynamicPoolInUse() &&
159 console.warn(
160 chalk.yellow(
1d8f226b 161 '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 162 ),
56e2e1ab 163 );
0bde1ea1 164 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info);
82e9c15a
JB
165 this.started = true;
166 this.starting = false;
167 } else {
168 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
ded13d97 169 }
b322b8b4
JB
170 } else {
171 console.error(chalk.red('Cannot start an already started charging stations simulator'));
ded13d97
JB
172 }
173 }
174
36adaf06 175 public async stop(stopChargingStations = true): Promise<void> {
ee60150f 176 if (this.started === true) {
82e9c15a
JB
177 if (this.stopping === false) {
178 this.stopping = true;
36adaf06
JB
179 if (stopChargingStations === true) {
180 await this.uiServer?.sendInternalRequest(
181 this.uiServer.buildProtocolRequest(
182 generateUUID(),
183 ProcedureName.STOP_CHARGING_STATION,
184 Constants.EMPTY_FROZEN_OBJECT,
ab7a96fa 185 ),
36adaf06 186 );
5b2721db
JB
187 try {
188 await this.waitChargingStationsStopped();
189 } catch (error) {
190 console.error(chalk.red('Error while waiting for charging stations to stop: '), error);
191 }
ab7a96fa 192 }
82e9c15a
JB
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;
201 } else {
202 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
203 }
b322b8b4 204 } else {
82e9c15a 205 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
ded13d97 206 }
ded13d97
JB
207 }
208
36adaf06
JB
209 public async restart(stopChargingStations?: boolean): Promise<void> {
210 await this.stop(stopChargingStations);
ded13d97
JB
211 await this.start();
212 }
213
5b2721db
JB
214 private async waitChargingStationsStopped(): Promise<string> {
215 return new Promise<string>((resolve, reject) => {
216 const waitTimeout = setTimeout(() => {
217 const message = `Timeout ${formatDurationMilliSeconds(
d81db081 218 Constants.STOP_CHARGING_STATIONS_TIMEOUT,
5b2721db
JB
219 )} reached at stopping charging stations`;
220 console.warn(chalk.yellow(message));
221 reject(new Error(message));
d81db081 222 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT);
36adaf06
JB
223 waitChargingStationEvents(
224 this,
225 ChargingStationWorkerMessageEvents.stopped,
226 this.numberOfChargingStations,
5b2721db
JB
227 )
228 .then(() => {
229 resolve('Charging stations stopped');
230 })
b7ee97c1 231 .catch(reject)
5b2721db
JB
232 .finally(() => {
233 clearTimeout(waitTimeout);
234 });
235 });
36adaf06
JB
236 }
237
864e5f8d 238 private initializeWorkerImplementation(workerConfiguration: WorkerConfiguration): void {
e1d9a0f4 239 let elementsPerWorker: number | undefined;
864e5f8d 240 if (workerConfiguration?.elementsPerWorker === 'auto') {
34c200d5
JB
241 elementsPerWorker =
242 this.numberOfChargingStations > availableParallelism()
411f6bb4 243 ? Math.round(this.numberOfChargingStations / (availableParallelism() * 1.5))
34c200d5 244 : 1;
8603c1ca 245 }
e2c77f10 246 this.workerImplementation === null &&
ec7f4dce
JB
247 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
248 this.workerScript,
864e5f8d 249 workerConfiguration.processType!,
ec7f4dce 250 {
864e5f8d
JB
251 workerStartDelay: workerConfiguration.startDelay,
252 elementStartDelay: workerConfiguration.elementStartDelay,
253 poolMaxSize: workerConfiguration.poolMaxSize!,
254 poolMinSize: workerConfiguration.poolMinSize!,
255 elementsPerWorker: elementsPerWorker ?? (workerConfiguration.elementsPerWorker as number),
ec7f4dce 256 poolOptions: {
be245fda 257 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
ec7f4dce 258 },
5edd8ba0 259 },
ec7f4dce 260 ));
ded13d97 261 }
81797102 262
32de5a57 263 private messageHandler(
5edd8ba0 264 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>,
32de5a57
LM
265 ): void {
266 // logger.debug(
267 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
268 // msg,
4ed03b6e 269 // undefined,
e1d9a0f4
JB
270 // 2,
271 // )}`,
32de5a57
LM
272 // );
273 try {
8cc482a9 274 switch (msg.event) {
721646e9 275 case ChargingStationWorkerMessageEvents.started:
32de5a57 276 this.workerEventStarted(msg.data as ChargingStationData);
f130b8e6 277 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
32de5a57 278 break;
721646e9 279 case ChargingStationWorkerMessageEvents.stopped:
32de5a57 280 this.workerEventStopped(msg.data as ChargingStationData);
f130b8e6 281 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
32de5a57 282 break;
721646e9 283 case ChargingStationWorkerMessageEvents.updated:
32de5a57 284 this.workerEventUpdated(msg.data as ChargingStationData);
f130b8e6 285 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
32de5a57 286 break;
721646e9 287 case ChargingStationWorkerMessageEvents.performanceStatistics:
32de5a57 288 this.workerEventPerformanceStatistics(msg.data as Statistics);
f130b8e6
JB
289 this.emit(
290 ChargingStationWorkerMessageEvents.performanceStatistics,
5edd8ba0 291 msg.data as Statistics,
f130b8e6 292 );
32de5a57 293 break;
2bb7a73e
JB
294 case ChargingStationWorkerMessageEvents.startWorkerElementError:
295 logger.error(
296 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
297 msg.data,
298 );
299 this.emit(ChargingStationWorkerMessageEvents.startWorkerElementError, msg.data);
300 break;
301 case ChargingStationWorkerMessageEvents.startedWorkerElement:
302 break;
32de5a57
LM
303 default:
304 throw new BaseError(
f93dda6a
JB
305 `Unknown charging station worker event: '${
306 msg.event
4ed03b6e 307 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
32de5a57
LM
308 );
309 }
310 } catch (error) {
311 logger.error(
312 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 313 msg.event
32de5a57 314 }' event:`,
5edd8ba0 315 error,
32de5a57
LM
316 );
317 }
318 }
319
e2c77f10 320 private workerEventStarted = (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}.workerEventStarted: Charging station ${
56eb297e 325 data.stationInfo.chargingStationId
e6159ce8 326 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 327 this.numberOfStartedChargingStations
5edd8ba0 328 } started from ${this.numberOfChargingStations})`,
56eb297e 329 );
e2c77f10 330 };
32de5a57 331
e2c77f10 332 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 333 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 334 --this.numberOfStartedChargingStations;
56eb297e 335 logger.info(
e6159ce8 336 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 337 data.stationInfo.chargingStationId
e6159ce8 338 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 339 this.numberOfStartedChargingStations
5edd8ba0 340 } started from ${this.numberOfChargingStations})`,
56eb297e 341 );
e2c77f10 342 };
32de5a57 343
e2c77f10 344 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 345 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 346 };
32de5a57
LM
347
348 private workerEventPerformanceStatistics = (data: Statistics) => {
349 this.storage.storePerformanceStatistics(data) as void;
350 };
351
326cec2d 352 private initializeCounters() {
a596d200 353 if (this.initializedCounters === false) {
0f040ac0 354 this.resetCounters();
e1d9a0f4 355 const stationTemplateUrls = Configuration.getStationTemplateUrls()!;
9bf0ef23 356 if (isNotEmptyArray(stationTemplateUrls)) {
41bda658 357 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 358 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 359 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 360 }
a596d200
JB
361 } else {
362 console.warn(
5edd8ba0 363 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
a596d200 364 );
10687422 365 exit(exitCodes.missingChargingStationsConfiguration);
a596d200
JB
366 }
367 if (this.numberOfChargingStations === 0) {
368 console.warn(
5edd8ba0 369 chalk.yellow('No charging station template enabled in configuration, exiting'),
a596d200 370 );
10687422 371 exit(exitCodes.noChargingStationTemplates);
a596d200 372 }
a596d200 373 this.initializedCounters = true;
846d2851 374 }
7c72977b
JB
375 }
376
0f040ac0
JB
377 private resetCounters(): void {
378 this.numberOfChargingStationTemplates = 0;
379 this.numberOfChargingStations = 0;
380 this.numberOfStartedChargingStations = 0;
381 }
382
e7aeea18
JB
383 private async startChargingStation(
384 index: number,
5edd8ba0 385 stationTemplateUrl: StationTemplateUrl,
e7aeea18 386 ): Promise<void> {
6ed3c845 387 await this.workerImplementation?.addElement({
717c1e56 388 index,
d972af76
JB
389 templateFile: join(
390 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
391 'assets',
392 'station-templates',
5edd8ba0 393 stationTemplateUrl.file,
e7aeea18 394 ),
6ed3c845 395 });
717c1e56
JB
396 }
397
36adaf06 398 private gracefulShutdown(): void {
f130b8e6
JB
399 this.stop()
400 .then(() => {
83a36f14 401 console.info(`${chalk.green('Graceful shutdown')}`);
36adaf06
JB
402 // stop() asks for charging stations to stop by default
403 this.waitChargingStationsStopped()
404 .then(() => {
405 exit(exitCodes.succeeded);
406 })
5b2721db 407 .catch(() => {
36adaf06
JB
408 exit(exitCodes.gracefulShutdownError);
409 });
f130b8e6
JB
410 })
411 .catch((error) => {
fca8bc64 412 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
10687422 413 exit(exitCodes.gracefulShutdownError);
f130b8e6 414 });
36adaf06 415 }
f130b8e6 416
8b7072dc 417 private logPrefix = (): string => {
9bf0ef23 418 return logPrefix(' Bootstrap |');
8b7072dc 419 };
ded13d97 420}