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