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