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