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