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