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