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