build(deps): 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
fba11dc6 10import { waitForChargingStationEvents } 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)),
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,
86 this.logPrefix()
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(
120 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
121 ),
122 error
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 : ''
142 }`
143 )
144 );
0bde1ea1 145 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info);
82e9c15a
JB
146 this.started = true;
147 this.starting = false;
148 } else {
149 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
ded13d97 150 }
b322b8b4
JB
151 } else {
152 console.error(chalk.red('Cannot start an already started charging stations simulator'));
ded13d97
JB
153 }
154 }
155
156 public async stop(): Promise<void> {
ee60150f
JB
157 if (!isMainThread) {
158 throw new Error('Cannot stop charging stations simulator from worker thread');
159 }
160 if (this.started === true) {
82e9c15a
JB
161 if (this.stopping === false) {
162 this.stopping = true;
163 await this.uiServer?.sendInternalRequest(
164 this.uiServer.buildProtocolRequest(
9bf0ef23 165 generateUUID(),
82e9c15a
JB
166 ProcedureName.STOP_CHARGING_STATION,
167 Constants.EMPTY_FREEZED_OBJECT
168 )
169 );
1832a986 170 await Promise.race([
fba11dc6 171 waitForChargingStationEvents(
1832a986
JB
172 this,
173 ChargingStationWorkerMessageEvents.stopped,
174 this.numberOfChargingStations
175 ),
176 new Promise<string>((resolve) => {
177 setTimeout(() => {
9bf0ef23 178 const message = `Timeout reached ${formatDurationMilliSeconds(
1832a986
JB
179 Constants.STOP_SIMULATOR_TIMEOUT
180 )} at stopping charging stations simulator`;
181 console.warn(chalk.yellow(message));
182 resolve(message);
183 }, Constants.STOP_SIMULATOR_TIMEOUT);
184 }),
185 ]);
82e9c15a
JB
186 await this.workerImplementation?.stop();
187 this.workerImplementation = null;
188 this.uiServer?.stop();
189 await this.storage?.close();
190 this.resetCounters();
191 this.initializedCounters = false;
192 this.started = false;
193 this.stopping = false;
194 } else {
195 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
196 }
b322b8b4 197 } else {
82e9c15a 198 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
ded13d97 199 }
ded13d97
JB
200 }
201
202 public async restart(): Promise<void> {
203 await this.stop();
204 await this.start();
205 }
206
ec7f4dce 207 private initializeWorkerImplementation(): void {
e2c77f10 208 this.workerImplementation === null &&
ec7f4dce
JB
209 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
210 this.workerScript,
cf2a5d9b 211 Configuration.getWorker().processType,
ec7f4dce 212 {
cf2a5d9b
JB
213 workerStartDelay: Configuration.getWorker().startDelay,
214 elementStartDelay: Configuration.getWorker().elementStartDelay,
215 poolMaxSize: Configuration.getWorker().poolMaxSize,
216 poolMinSize: Configuration.getWorker().poolMinSize,
217 elementsPerWorker: Configuration.getWorker().elementsPerWorker,
ec7f4dce 218 poolOptions: {
cf2a5d9b 219 workerChoiceStrategy: Configuration.getWorker().poolStrategy,
be245fda 220 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
ec7f4dce 221 },
ec7f4dce
JB
222 }
223 ));
ded13d97 224 }
81797102 225
32de5a57 226 private messageHandler(
53e5fd67 227 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
228 ): void {
229 // logger.debug(
230 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
231 // msg,
232 // null,
233 // 2
234 // )}`
235 // );
236 try {
237 switch (msg.id) {
721646e9 238 case ChargingStationWorkerMessageEvents.started:
32de5a57 239 this.workerEventStarted(msg.data as ChargingStationData);
f130b8e6 240 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
32de5a57 241 break;
721646e9 242 case ChargingStationWorkerMessageEvents.stopped:
32de5a57 243 this.workerEventStopped(msg.data as ChargingStationData);
f130b8e6 244 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
32de5a57 245 break;
721646e9 246 case ChargingStationWorkerMessageEvents.updated:
32de5a57 247 this.workerEventUpdated(msg.data as ChargingStationData);
f130b8e6 248 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
32de5a57 249 break;
721646e9 250 case ChargingStationWorkerMessageEvents.performanceStatistics:
32de5a57 251 this.workerEventPerformanceStatistics(msg.data as Statistics);
f130b8e6
JB
252 this.emit(
253 ChargingStationWorkerMessageEvents.performanceStatistics,
254 msg.data as Statistics
255 );
32de5a57
LM
256 break;
257 default:
258 throw new BaseError(
259 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
260 );
261 }
262 } catch (error) {
263 logger.error(
264 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
265 msg.id
266 }' event:`,
267 error
268 );
269 }
270 }
271
e2c77f10 272 private workerEventStarted = (data: ChargingStationData) => {
51c83d6f 273 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 274 ++this.numberOfStartedChargingStations;
56eb297e 275 logger.info(
e6159ce8 276 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 277 data.stationInfo.chargingStationId
e6159ce8 278 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e
JB
279 this.numberOfStartedChargingStations
280 } started from ${this.numberOfChargingStations})`
281 );
e2c77f10 282 };
32de5a57 283
e2c77f10 284 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 285 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 286 --this.numberOfStartedChargingStations;
56eb297e 287 logger.info(
e6159ce8 288 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 289 data.stationInfo.chargingStationId
e6159ce8 290 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e
JB
291 this.numberOfStartedChargingStations
292 } started from ${this.numberOfChargingStations})`
293 );
e2c77f10 294 };
32de5a57 295
e2c77f10 296 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 297 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 298 };
32de5a57
LM
299
300 private workerEventPerformanceStatistics = (data: Statistics) => {
301 this.storage.storePerformanceStatistics(data) as void;
302 };
303
326cec2d 304 private initializeCounters() {
a596d200 305 if (this.initializedCounters === false) {
0f040ac0 306 this.resetCounters();
a596d200 307 const stationTemplateUrls = Configuration.getStationTemplateUrls();
9bf0ef23 308 if (isNotEmptyArray(stationTemplateUrls)) {
41bda658 309 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 310 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 311 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 312 }
a596d200
JB
313 } else {
314 console.warn(
315 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
316 );
317 process.exit(exitCodes.missingChargingStationsConfiguration);
318 }
319 if (this.numberOfChargingStations === 0) {
320 console.warn(
321 chalk.yellow('No charging station template enabled in configuration, exiting')
322 );
323 process.exit(exitCodes.noChargingStationTemplates);
324 }
a596d200 325 this.initializedCounters = true;
846d2851 326 }
7c72977b
JB
327 }
328
0f040ac0
JB
329 private resetCounters(): void {
330 this.numberOfChargingStationTemplates = 0;
331 this.numberOfChargingStations = 0;
332 this.numberOfStartedChargingStations = 0;
333 }
334
e7aeea18
JB
335 private async startChargingStation(
336 index: number,
337 stationTemplateUrl: StationTemplateUrl
338 ): Promise<void> {
6ed3c845 339 await this.workerImplementation?.addElement({
717c1e56 340 index,
d972af76
JB
341 templateFile: join(
342 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
343 'assets',
344 'station-templates',
ee5f26a2 345 stationTemplateUrl.file
e7aeea18 346 ),
6ed3c845 347 });
717c1e56
JB
348 }
349
f130b8e6 350 private gracefulShutdown = (): void => {
6bd808fd 351 console.info(`${chalk.green('Graceful shutdown')}`);
f130b8e6
JB
352 this.stop()
353 .then(() => {
354 process.exit(0);
355 })
356 .catch((error) => {
fca8bc64 357 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
f130b8e6
JB
358 process.exit(1);
359 });
360 };
361
8b7072dc 362 private logPrefix = (): string => {
9bf0ef23 363 return logPrefix(' Bootstrap |');
8b7072dc 364 };
ded13d97 365}