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';
130783a7 5import { fileURLToPath } from 'node:url';
be245fda 6import { isMainThread } from 'node:worker_threads';
8114d10e
JB
7
8import chalk from 'chalk';
9
b2b60626 10import { waitChargingStationEvents } from './ChargingStationUtils';
4c3c0d59
JB
11import type { AbstractUIServer } from './ui-server/AbstractUIServer';
12import { UIServerFactory } from './ui-server/UIServerFactory';
628c30e5 13import { version } from '../../package.json' assert { type: 'json' };
268a74bb 14import { BaseError } from '../exception';
17bc43d7 15import { type Storage, StorageFactory } from '../performance';
e7aeea18 16import {
bbe10d5f
JB
17 type ChargingStationData,
18 type ChargingStationWorkerData,
19 type ChargingStationWorkerMessage,
20 type ChargingStationWorkerMessageData,
e7aeea18 21 ChargingStationWorkerMessageEvents,
6bd808fd 22 ProcedureName,
268a74bb
JB
23 type StationTemplateUrl,
24 type Statistics,
25} from '../types';
fa5995d6
JB
26import {
27 Configuration,
28 Constants,
9bf0ef23
JB
29 formatDurationMilliSeconds,
30 generateUUID,
fa5995d6
JB
31 handleUncaughtException,
32 handleUnhandledRejection,
9bf0ef23
JB
33 isNotEmptyArray,
34 isNullOrUndefined,
35 logPrefix,
fa5995d6
JB
36 logger,
37} from '../utils';
be245fda 38import { type WorkerAbstract, WorkerFactory } from '../worker';
ded13d97 39
32de5a57
LM
40const moduleName = 'Bootstrap';
41
a307349b
JB
42enum exitCodes {
43 missingChargingStationsConfiguration = 1,
44 noChargingStationTemplates = 2,
45}
e4cb2c14 46
f130b8e6 47export class Bootstrap extends EventEmitter {
535aaa27 48 private static instance: Bootstrap | null = null;
d1c99c59
JB
49 public numberOfChargingStations!: number;
50 public numberOfChargingStationTemplates!: number;
aa428a31 51 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
551e477c 52 private readonly uiServer!: AbstractUIServer | null;
6a49ad23 53 private readonly storage!: Storage;
89b7a234 54 private numberOfStartedChargingStations!: number;
628c30e5 55 private readonly version: string = version;
a596d200 56 private initializedCounters: boolean;
eb87fe87 57 private started: boolean;
82e9c15a
JB
58 private starting: boolean;
59 private stopping: boolean;
9e23580d 60 private readonly workerScript: string;
ded13d97
JB
61
62 private constructor() {
f130b8e6 63 super();
6bd808fd 64 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
f130b8e6 65 process.on(signal, this.gracefulShutdown);
6bd808fd 66 }
4724a293 67 // Enable unconditionally for now
fa5995d6
JB
68 handleUnhandledRejection();
69 handleUncaughtException();
af8e02ca 70 this.started = false;
82e9c15a
JB
71 this.starting = false;
72 this.stopping = false;
0f040ac0 73 this.initializedCounters = false;
a596d200 74 this.initializeCounters();
af8e02ca 75 this.workerImplementation = null;
d972af76
JB
76 this.workerScript = join(
77 dirname(fileURLToPath(import.meta.url)),
5edd8ba0 78 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
e7aeea18 79 );
5af9aa8a 80 Configuration.getUIServer().enabled === true &&
976d11ec 81 (this.uiServer = UIServerFactory.getUIServerImplementation(Configuration.getUIServer()));
eb3abc4f 82 Configuration.getPerformanceStorage().enabled === true &&
e7aeea18
JB
83 (this.storage = StorageFactory.getStorage(
84 Configuration.getPerformanceStorage().type,
85 Configuration.getPerformanceStorage().uri,
5edd8ba0 86 this.logPrefix(),
e7aeea18 87 ));
7874b0b1 88 Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart());
ded13d97
JB
89 }
90
91 public static getInstance(): Bootstrap {
1ca780f9 92 if (Bootstrap.instance === null) {
ded13d97
JB
93 Bootstrap.instance = new Bootstrap();
94 }
95 return Bootstrap.instance;
96 }
97
98 public async start(): Promise<void> {
ee60150f
JB
99 if (!isMainThread) {
100 throw new Error('Cannot start charging stations simulator from worker thread');
101 }
102 if (this.started === false) {
82e9c15a
JB
103 if (this.starting === false) {
104 this.starting = true;
105 this.initializeCounters();
106 this.initializeWorkerImplementation();
107 await this.workerImplementation?.start();
108 await this.storage?.open();
109 this.uiServer?.start();
110 // Start ChargingStation object instance in worker thread
111 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()) {
112 try {
113 const nbStations = stationTemplateUrl.numberOfStations ?? 0;
114 for (let index = 1; index <= nbStations; index++) {
115 await this.startChargingStation(index, stationTemplateUrl);
116 }
117 } catch (error) {
118 console.error(
119 chalk.red(
5edd8ba0 120 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
82e9c15a 121 ),
5edd8ba0 122 error,
82e9c15a 123 );
ded13d97 124 }
ded13d97 125 }
82e9c15a
JB
126 console.info(
127 chalk.green(
128 `Charging stations simulator ${
129 this.version
130 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
131 Configuration.workerDynamicPoolInUse()
132 ? `${Configuration.getWorker().poolMinSize?.toString()}/`
133 : ''
134 }${this.workerImplementation?.size}${
135 Configuration.workerPoolInUse()
136 ? `/${Configuration.getWorker().poolMaxSize?.toString()}`
137 : ''
138 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
9bf0ef23 139 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
82e9c15a
JB
140 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
141 : ''
5edd8ba0
JB
142 }`,
143 ),
82e9c15a 144 );
56e2e1ab
JB
145 Configuration.workerDynamicPoolInUse() &&
146 console.warn(
147 chalk.yellow(
5edd8ba0
JB
148 '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',
149 ),
56e2e1ab 150 );
0bde1ea1 151 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info);
82e9c15a
JB
152 this.started = true;
153 this.starting = false;
154 } else {
155 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
ded13d97 156 }
b322b8b4
JB
157 } else {
158 console.error(chalk.red('Cannot start an already started charging stations simulator'));
ded13d97
JB
159 }
160 }
161
162 public async stop(): Promise<void> {
ee60150f
JB
163 if (!isMainThread) {
164 throw new Error('Cannot stop charging stations simulator from worker thread');
165 }
166 if (this.started === true) {
82e9c15a
JB
167 if (this.stopping === false) {
168 this.stopping = true;
169 await this.uiServer?.sendInternalRequest(
170 this.uiServer.buildProtocolRequest(
9bf0ef23 171 generateUUID(),
82e9c15a 172 ProcedureName.STOP_CHARGING_STATION,
5edd8ba0
JB
173 Constants.EMPTY_FREEZED_OBJECT,
174 ),
82e9c15a 175 );
1832a986 176 await Promise.race([
b2b60626 177 waitChargingStationEvents(
1832a986
JB
178 this,
179 ChargingStationWorkerMessageEvents.stopped,
5edd8ba0 180 this.numberOfChargingStations,
1832a986
JB
181 ),
182 new Promise<string>((resolve) => {
183 setTimeout(() => {
9bf0ef23 184 const message = `Timeout reached ${formatDurationMilliSeconds(
5edd8ba0 185 Constants.STOP_SIMULATOR_TIMEOUT,
1832a986
JB
186 )} at stopping charging stations simulator`;
187 console.warn(chalk.yellow(message));
188 resolve(message);
189 }, Constants.STOP_SIMULATOR_TIMEOUT);
190 }),
191 ]);
82e9c15a
JB
192 await this.workerImplementation?.stop();
193 this.workerImplementation = null;
194 this.uiServer?.stop();
195 await this.storage?.close();
196 this.resetCounters();
197 this.initializedCounters = false;
198 this.started = false;
199 this.stopping = false;
200 } else {
201 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
202 }
b322b8b4 203 } else {
82e9c15a 204 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
ded13d97 205 }
ded13d97
JB
206 }
207
208 public async restart(): Promise<void> {
209 await this.stop();
210 await this.start();
211 }
212
ec7f4dce 213 private initializeWorkerImplementation(): void {
e2c77f10 214 this.workerImplementation === null &&
ec7f4dce
JB
215 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
216 this.workerScript,
cf2a5d9b 217 Configuration.getWorker().processType,
ec7f4dce 218 {
cf2a5d9b
JB
219 workerStartDelay: Configuration.getWorker().startDelay,
220 elementStartDelay: Configuration.getWorker().elementStartDelay,
221 poolMaxSize: Configuration.getWorker().poolMaxSize,
222 poolMinSize: Configuration.getWorker().poolMinSize,
223 elementsPerWorker: Configuration.getWorker().elementsPerWorker,
ec7f4dce 224 poolOptions: {
cf2a5d9b 225 workerChoiceStrategy: Configuration.getWorker().poolStrategy,
be245fda 226 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
ec7f4dce 227 },
5edd8ba0 228 },
ec7f4dce 229 ));
ded13d97 230 }
81797102 231
32de5a57 232 private messageHandler(
5edd8ba0 233 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>,
32de5a57
LM
234 ): void {
235 // logger.debug(
236 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
237 // msg,
238 // null,
239 // 2
240 // )}`
241 // );
242 try {
243 switch (msg.id) {
721646e9 244 case ChargingStationWorkerMessageEvents.started:
32de5a57 245 this.workerEventStarted(msg.data as ChargingStationData);
f130b8e6 246 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
32de5a57 247 break;
721646e9 248 case ChargingStationWorkerMessageEvents.stopped:
32de5a57 249 this.workerEventStopped(msg.data as ChargingStationData);
f130b8e6 250 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
32de5a57 251 break;
721646e9 252 case ChargingStationWorkerMessageEvents.updated:
32de5a57 253 this.workerEventUpdated(msg.data as ChargingStationData);
f130b8e6 254 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
32de5a57 255 break;
721646e9 256 case ChargingStationWorkerMessageEvents.performanceStatistics:
32de5a57 257 this.workerEventPerformanceStatistics(msg.data as Statistics);
f130b8e6
JB
258 this.emit(
259 ChargingStationWorkerMessageEvents.performanceStatistics,
5edd8ba0 260 msg.data as Statistics,
f130b8e6 261 );
32de5a57
LM
262 break;
263 default:
264 throw new BaseError(
5edd8ba0 265 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`,
32de5a57
LM
266 );
267 }
268 } catch (error) {
269 logger.error(
270 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
271 msg.id
272 }' event:`,
5edd8ba0 273 error,
32de5a57
LM
274 );
275 }
276 }
277
e2c77f10 278 private workerEventStarted = (data: ChargingStationData) => {
51c83d6f 279 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 280 ++this.numberOfStartedChargingStations;
56eb297e 281 logger.info(
e6159ce8 282 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 283 data.stationInfo.chargingStationId
e6159ce8 284 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 285 this.numberOfStartedChargingStations
5edd8ba0 286 } started from ${this.numberOfChargingStations})`,
56eb297e 287 );
e2c77f10 288 };
32de5a57 289
e2c77f10 290 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 291 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 292 --this.numberOfStartedChargingStations;
56eb297e 293 logger.info(
e6159ce8 294 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 295 data.stationInfo.chargingStationId
e6159ce8 296 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 297 this.numberOfStartedChargingStations
5edd8ba0 298 } started from ${this.numberOfChargingStations})`,
56eb297e 299 );
e2c77f10 300 };
32de5a57 301
e2c77f10 302 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 303 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 304 };
32de5a57
LM
305
306 private workerEventPerformanceStatistics = (data: Statistics) => {
307 this.storage.storePerformanceStatistics(data) as void;
308 };
309
326cec2d 310 private initializeCounters() {
a596d200 311 if (this.initializedCounters === false) {
0f040ac0 312 this.resetCounters();
a596d200 313 const stationTemplateUrls = Configuration.getStationTemplateUrls();
9bf0ef23 314 if (isNotEmptyArray(stationTemplateUrls)) {
41bda658 315 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 316 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 317 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 318 }
a596d200
JB
319 } else {
320 console.warn(
5edd8ba0 321 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
a596d200
JB
322 );
323 process.exit(exitCodes.missingChargingStationsConfiguration);
324 }
325 if (this.numberOfChargingStations === 0) {
326 console.warn(
5edd8ba0 327 chalk.yellow('No charging station template enabled in configuration, exiting'),
a596d200
JB
328 );
329 process.exit(exitCodes.noChargingStationTemplates);
330 }
a596d200 331 this.initializedCounters = true;
846d2851 332 }
7c72977b
JB
333 }
334
0f040ac0
JB
335 private resetCounters(): void {
336 this.numberOfChargingStationTemplates = 0;
337 this.numberOfChargingStations = 0;
338 this.numberOfStartedChargingStations = 0;
339 }
340
e7aeea18
JB
341 private async startChargingStation(
342 index: number,
5edd8ba0 343 stationTemplateUrl: StationTemplateUrl,
e7aeea18 344 ): Promise<void> {
6ed3c845 345 await this.workerImplementation?.addElement({
717c1e56 346 index,
d972af76
JB
347 templateFile: join(
348 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
349 'assets',
350 'station-templates',
5edd8ba0 351 stationTemplateUrl.file,
e7aeea18 352 ),
6ed3c845 353 });
717c1e56
JB
354 }
355
f130b8e6 356 private gracefulShutdown = (): void => {
6bd808fd 357 console.info(`${chalk.green('Graceful shutdown')}`);
f130b8e6
JB
358 this.stop()
359 .then(() => {
360 process.exit(0);
361 })
362 .catch((error) => {
fca8bc64 363 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
f130b8e6
JB
364 process.exit(1);
365 });
366 };
367
8b7072dc 368 private logPrefix = (): string => {
9bf0ef23 369 return logPrefix(' Bootstrap |');
8b7072dc 370 };
ded13d97 371}