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