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