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