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