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