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