build: rollup -> esbuild
[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 (!isMainThread) {
a6080904 182 throw new BaseError('Cannot stop charging stations simulator from worker thread');
ee60150f
JB
183 }
184 if (this.started === true) {
82e9c15a
JB
185 if (this.stopping === false) {
186 this.stopping = true;
187 await this.uiServer?.sendInternalRequest(
188 this.uiServer.buildProtocolRequest(
9bf0ef23 189 generateUUID(),
82e9c15a 190 ProcedureName.STOP_CHARGING_STATION,
2035255d 191 Constants.EMPTY_FROZEN_OBJECT,
5edd8ba0 192 ),
82e9c15a 193 );
1832a986 194 await Promise.race([
b2b60626 195 waitChargingStationEvents(
1832a986
JB
196 this,
197 ChargingStationWorkerMessageEvents.stopped,
5edd8ba0 198 this.numberOfChargingStations,
1832a986
JB
199 ),
200 new Promise<string>((resolve) => {
201 setTimeout(() => {
9bf0ef23 202 const message = `Timeout reached ${formatDurationMilliSeconds(
5edd8ba0 203 Constants.STOP_SIMULATOR_TIMEOUT,
1832a986
JB
204 )} at stopping charging stations simulator`;
205 console.warn(chalk.yellow(message));
206 resolve(message);
207 }, Constants.STOP_SIMULATOR_TIMEOUT);
208 }),
209 ]);
82e9c15a
JB
210 await this.workerImplementation?.stop();
211 this.workerImplementation = null;
212 this.uiServer?.stop();
213 await this.storage?.close();
214 this.resetCounters();
215 this.initializedCounters = false;
216 this.started = false;
217 this.stopping = false;
218 } else {
219 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
220 }
b322b8b4 221 } else {
82e9c15a 222 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
ded13d97 223 }
ded13d97
JB
224 }
225
226 public async restart(): Promise<void> {
227 await this.stop();
228 await this.start();
229 }
230
864e5f8d 231 private initializeWorkerImplementation(workerConfiguration: WorkerConfiguration): void {
e1d9a0f4 232 let elementsPerWorker: number | undefined;
864e5f8d 233 if (workerConfiguration?.elementsPerWorker === 'auto') {
34c200d5
JB
234 elementsPerWorker =
235 this.numberOfChargingStations > availableParallelism()
411f6bb4 236 ? Math.round(this.numberOfChargingStations / (availableParallelism() * 1.5))
34c200d5 237 : 1;
8603c1ca 238 }
e2c77f10 239 this.workerImplementation === null &&
ec7f4dce
JB
240 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
241 this.workerScript,
864e5f8d 242 workerConfiguration.processType!,
ec7f4dce 243 {
864e5f8d
JB
244 workerStartDelay: workerConfiguration.startDelay,
245 elementStartDelay: workerConfiguration.elementStartDelay,
246 poolMaxSize: workerConfiguration.poolMaxSize!,
247 poolMinSize: workerConfiguration.poolMinSize!,
248 elementsPerWorker: elementsPerWorker ?? (workerConfiguration.elementsPerWorker as number),
ec7f4dce 249 poolOptions: {
be245fda 250 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
ec7f4dce 251 },
5edd8ba0 252 },
ec7f4dce 253 ));
ded13d97 254 }
81797102 255
32de5a57 256 private messageHandler(
5edd8ba0 257 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>,
32de5a57
LM
258 ): void {
259 // logger.debug(
260 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
261 // msg,
4ed03b6e 262 // undefined,
e1d9a0f4
JB
263 // 2,
264 // )}`,
32de5a57
LM
265 // );
266 try {
8cc482a9 267 switch (msg.event) {
721646e9 268 case ChargingStationWorkerMessageEvents.started:
32de5a57 269 this.workerEventStarted(msg.data as ChargingStationData);
f130b8e6 270 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
32de5a57 271 break;
721646e9 272 case ChargingStationWorkerMessageEvents.stopped:
32de5a57 273 this.workerEventStopped(msg.data as ChargingStationData);
f130b8e6 274 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
32de5a57 275 break;
721646e9 276 case ChargingStationWorkerMessageEvents.updated:
32de5a57 277 this.workerEventUpdated(msg.data as ChargingStationData);
f130b8e6 278 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
32de5a57 279 break;
721646e9 280 case ChargingStationWorkerMessageEvents.performanceStatistics:
32de5a57 281 this.workerEventPerformanceStatistics(msg.data as Statistics);
f130b8e6
JB
282 this.emit(
283 ChargingStationWorkerMessageEvents.performanceStatistics,
5edd8ba0 284 msg.data as Statistics,
f130b8e6 285 );
32de5a57 286 break;
2bb7a73e
JB
287 case ChargingStationWorkerMessageEvents.startWorkerElementError:
288 logger.error(
289 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
290 msg.data,
291 );
292 this.emit(ChargingStationWorkerMessageEvents.startWorkerElementError, msg.data);
293 break;
294 case ChargingStationWorkerMessageEvents.startedWorkerElement:
295 break;
32de5a57
LM
296 default:
297 throw new BaseError(
f93dda6a
JB
298 `Unknown charging station worker event: '${
299 msg.event
4ed03b6e 300 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
32de5a57
LM
301 );
302 }
303 } catch (error) {
304 logger.error(
305 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 306 msg.event
32de5a57 307 }' event:`,
5edd8ba0 308 error,
32de5a57
LM
309 );
310 }
311 }
312
e2c77f10 313 private workerEventStarted = (data: ChargingStationData) => {
51c83d6f 314 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 315 ++this.numberOfStartedChargingStations;
56eb297e 316 logger.info(
e6159ce8 317 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 318 data.stationInfo.chargingStationId
e6159ce8 319 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 320 this.numberOfStartedChargingStations
5edd8ba0 321 } started from ${this.numberOfChargingStations})`,
56eb297e 322 );
e2c77f10 323 };
32de5a57 324
e2c77f10 325 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 326 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 327 --this.numberOfStartedChargingStations;
56eb297e 328 logger.info(
e6159ce8 329 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 330 data.stationInfo.chargingStationId
e6159ce8 331 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 332 this.numberOfStartedChargingStations
5edd8ba0 333 } started from ${this.numberOfChargingStations})`,
56eb297e 334 );
e2c77f10 335 };
32de5a57 336
e2c77f10 337 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 338 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 339 };
32de5a57
LM
340
341 private workerEventPerformanceStatistics = (data: Statistics) => {
342 this.storage.storePerformanceStatistics(data) as void;
343 };
344
326cec2d 345 private initializeCounters() {
a596d200 346 if (this.initializedCounters === false) {
0f040ac0 347 this.resetCounters();
e1d9a0f4 348 const stationTemplateUrls = Configuration.getStationTemplateUrls()!;
9bf0ef23 349 if (isNotEmptyArray(stationTemplateUrls)) {
41bda658 350 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 351 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 352 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 353 }
a596d200
JB
354 } else {
355 console.warn(
5edd8ba0 356 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
a596d200 357 );
10687422 358 exit(exitCodes.missingChargingStationsConfiguration);
a596d200
JB
359 }
360 if (this.numberOfChargingStations === 0) {
361 console.warn(
5edd8ba0 362 chalk.yellow('No charging station template enabled in configuration, exiting'),
a596d200 363 );
10687422 364 exit(exitCodes.noChargingStationTemplates);
a596d200 365 }
a596d200 366 this.initializedCounters = true;
846d2851 367 }
7c72977b
JB
368 }
369
0f040ac0
JB
370 private resetCounters(): void {
371 this.numberOfChargingStationTemplates = 0;
372 this.numberOfChargingStations = 0;
373 this.numberOfStartedChargingStations = 0;
374 }
375
e7aeea18
JB
376 private async startChargingStation(
377 index: number,
5edd8ba0 378 stationTemplateUrl: StationTemplateUrl,
e7aeea18 379 ): Promise<void> {
6ed3c845 380 await this.workerImplementation?.addElement({
717c1e56 381 index,
d972af76
JB
382 templateFile: join(
383 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
384 'assets',
385 'station-templates',
5edd8ba0 386 stationTemplateUrl.file,
e7aeea18 387 ),
6ed3c845 388 });
717c1e56
JB
389 }
390
f130b8e6 391 private gracefulShutdown = (): void => {
f130b8e6
JB
392 this.stop()
393 .then(() => {
83a36f14 394 console.info(`${chalk.green('Graceful shutdown')}`);
10687422 395 exit(exitCodes.succeeded);
f130b8e6
JB
396 })
397 .catch((error) => {
fca8bc64 398 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
10687422 399 exit(exitCodes.gracefulShutdownError);
f130b8e6
JB
400 });
401 };
402
8b7072dc 403 private logPrefix = (): string => {
9bf0ef23 404 return logPrefix(' Bootstrap |');
8b7072dc 405 };
ded13d97 406}