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