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