fix(simulator): remove unneeded main thread check at simulator start
[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 { exit } from 'node:process';
6 import { fileURLToPath } from 'node:url';
7
8 import chalk from 'chalk';
9 import { availableParallelism } from 'poolifier';
10
11 import { waitChargingStationEvents } from './Helpers';
12 import type { AbstractUIServer } from './ui-server/AbstractUIServer';
13 import { UIServerFactory } from './ui-server/UIServerFactory';
14 import { version } from '../../package.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 succeeded = 0,
49 missingChargingStationsConfiguration = 1,
50 noChargingStationTemplates = 2,
51 gracefulShutdownError = 3,
52 }
53
54 export class Bootstrap extends EventEmitter {
55 private static instance: Bootstrap | null = null;
56 public numberOfChargingStations!: number;
57 public numberOfChargingStationTemplates!: number;
58 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
59 private readonly uiServer!: AbstractUIServer | null;
60 private readonly storage!: Storage;
61 private numberOfStartedChargingStations!: number;
62 private readonly version: string = version;
63 private initializedCounters: boolean;
64 private started: boolean;
65 private starting: boolean;
66 private stopping: boolean;
67 private readonly workerScript: string;
68
69 private constructor() {
70 super();
71 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
72 process.on(signal, this.gracefulShutdown);
73 }
74 // Enable unconditionally for now
75 handleUnhandledRejection();
76 handleUncaughtException();
77 this.started = false;
78 this.starting = false;
79 this.stopping = false;
80 this.initializedCounters = false;
81 this.initializeCounters();
82 this.workerImplementation = null;
83 this.workerScript = join(
84 dirname(fileURLToPath(import.meta.url)),
85 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
86 );
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 &&
97 (this.storage = StorageFactory.getStorage(
98 performanceStorageConfiguration.type!,
99 performanceStorageConfiguration.uri!,
100 this.logPrefix(),
101 ));
102 Configuration.configurationChangeCallback = 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 (this.started === false) {
114 if (this.starting === false) {
115 this.starting = true;
116 this.initializeCounters();
117 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
118 ConfigurationSection.worker,
119 );
120 this.initializeWorkerImplementation(workerConfiguration);
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 ? `${workerConfiguration.poolMinSize?.toString()}/`
147 : ''
148 }${this.workerImplementation?.size}${
149 Configuration.workerPoolInUse()
150 ? `/${workerConfiguration.poolMaxSize?.toString()}`
151 : ''
152 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
153 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
154 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
155 : ''
156 }`,
157 ),
158 );
159 Configuration.workerDynamicPoolInUse() &&
160 console.warn(
161 chalk.yellow(
162 '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',
163 ),
164 );
165 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info);
166 this.started = true;
167 this.starting = false;
168 } else {
169 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
170 }
171 } else {
172 console.error(chalk.red('Cannot start an already started charging stations simulator'));
173 }
174 }
175
176 public async stop(): Promise<void> {
177 if (this.started === true) {
178 if (this.stopping === false) {
179 this.stopping = true;
180 await this.uiServer?.sendInternalRequest(
181 this.uiServer.buildProtocolRequest(
182 generateUUID(),
183 ProcedureName.STOP_CHARGING_STATION,
184 Constants.EMPTY_FROZEN_OBJECT,
185 ),
186 );
187 await Promise.race([
188 waitChargingStationEvents(
189 this,
190 ChargingStationWorkerMessageEvents.stopped,
191 this.numberOfChargingStations,
192 ),
193 new Promise<string>((resolve) => {
194 setTimeout(() => {
195 const message = `Timeout ${formatDurationMilliSeconds(
196 Constants.STOP_SIMULATOR_TIMEOUT,
197 )} reached at stopping charging stations simulator`;
198 console.warn(chalk.yellow(message));
199 resolve(message);
200 }, Constants.STOP_SIMULATOR_TIMEOUT);
201 }),
202 ]);
203 await this.workerImplementation?.stop();
204 this.workerImplementation = null;
205 this.uiServer?.stop();
206 await this.storage?.close();
207 this.resetCounters();
208 this.initializedCounters = false;
209 this.started = false;
210 this.stopping = false;
211 } else {
212 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
213 }
214 } else {
215 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
216 }
217 }
218
219 public async restart(): Promise<void> {
220 await this.stop();
221 await this.start();
222 }
223
224 private initializeWorkerImplementation(workerConfiguration: WorkerConfiguration): void {
225 let elementsPerWorker: number | undefined;
226 if (workerConfiguration?.elementsPerWorker === 'auto') {
227 elementsPerWorker =
228 this.numberOfChargingStations > availableParallelism()
229 ? Math.round(this.numberOfChargingStations / (availableParallelism() * 1.5))
230 : 1;
231 }
232 this.workerImplementation === null &&
233 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
234 this.workerScript,
235 workerConfiguration.processType!,
236 {
237 workerStartDelay: workerConfiguration.startDelay,
238 elementStartDelay: workerConfiguration.elementStartDelay,
239 poolMaxSize: workerConfiguration.poolMaxSize!,
240 poolMinSize: workerConfiguration.poolMinSize!,
241 elementsPerWorker: elementsPerWorker ?? (workerConfiguration.elementsPerWorker as number),
242 poolOptions: {
243 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
244 },
245 },
246 ));
247 }
248
249 private messageHandler(
250 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>,
251 ): void {
252 // logger.debug(
253 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
254 // msg,
255 // undefined,
256 // 2,
257 // )}`,
258 // );
259 try {
260 switch (msg.event) {
261 case ChargingStationWorkerMessageEvents.started:
262 this.workerEventStarted(msg.data as ChargingStationData);
263 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
264 break;
265 case ChargingStationWorkerMessageEvents.stopped:
266 this.workerEventStopped(msg.data as ChargingStationData);
267 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
268 break;
269 case ChargingStationWorkerMessageEvents.updated:
270 this.workerEventUpdated(msg.data as ChargingStationData);
271 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
272 break;
273 case ChargingStationWorkerMessageEvents.performanceStatistics:
274 this.workerEventPerformanceStatistics(msg.data as Statistics);
275 this.emit(
276 ChargingStationWorkerMessageEvents.performanceStatistics,
277 msg.data as Statistics,
278 );
279 break;
280 case ChargingStationWorkerMessageEvents.startWorkerElementError:
281 logger.error(
282 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
283 msg.data,
284 );
285 this.emit(ChargingStationWorkerMessageEvents.startWorkerElementError, msg.data);
286 break;
287 case ChargingStationWorkerMessageEvents.startedWorkerElement:
288 break;
289 default:
290 throw new BaseError(
291 `Unknown charging station worker event: '${
292 msg.event
293 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
294 );
295 }
296 } catch (error) {
297 logger.error(
298 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
299 msg.event
300 }' event:`,
301 error,
302 );
303 }
304 }
305
306 private workerEventStarted = (data: ChargingStationData) => {
307 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
308 ++this.numberOfStartedChargingStations;
309 logger.info(
310 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
311 data.stationInfo.chargingStationId
312 } (hashId: ${data.stationInfo.hashId}) started (${
313 this.numberOfStartedChargingStations
314 } started from ${this.numberOfChargingStations})`,
315 );
316 };
317
318 private workerEventStopped = (data: ChargingStationData) => {
319 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
320 --this.numberOfStartedChargingStations;
321 logger.info(
322 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
323 data.stationInfo.chargingStationId
324 } (hashId: ${data.stationInfo.hashId}) stopped (${
325 this.numberOfStartedChargingStations
326 } started from ${this.numberOfChargingStations})`,
327 );
328 };
329
330 private workerEventUpdated = (data: ChargingStationData) => {
331 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
332 };
333
334 private workerEventPerformanceStatistics = (data: Statistics) => {
335 this.storage.storePerformanceStatistics(data) as void;
336 };
337
338 private initializeCounters() {
339 if (this.initializedCounters === false) {
340 this.resetCounters();
341 const stationTemplateUrls = Configuration.getStationTemplateUrls()!;
342 if (isNotEmptyArray(stationTemplateUrls)) {
343 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
344 for (const stationTemplateUrl of stationTemplateUrls) {
345 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
346 }
347 } else {
348 console.warn(
349 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
350 );
351 exit(exitCodes.missingChargingStationsConfiguration);
352 }
353 if (this.numberOfChargingStations === 0) {
354 console.warn(
355 chalk.yellow('No charging station template enabled in configuration, exiting'),
356 );
357 exit(exitCodes.noChargingStationTemplates);
358 }
359 this.initializedCounters = true;
360 }
361 }
362
363 private resetCounters(): void {
364 this.numberOfChargingStationTemplates = 0;
365 this.numberOfChargingStations = 0;
366 this.numberOfStartedChargingStations = 0;
367 }
368
369 private async startChargingStation(
370 index: number,
371 stationTemplateUrl: StationTemplateUrl,
372 ): Promise<void> {
373 await this.workerImplementation?.addElement({
374 index,
375 templateFile: join(
376 dirname(fileURLToPath(import.meta.url)),
377 'assets',
378 'station-templates',
379 stationTemplateUrl.file,
380 ),
381 });
382 }
383
384 private gracefulShutdown = (): void => {
385 this.stop()
386 .then(() => {
387 console.info(`${chalk.green('Graceful shutdown')}`);
388 exit(exitCodes.succeeded);
389 })
390 .catch((error) => {
391 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
392 exit(exitCodes.gracefulShutdownError);
393 });
394 };
395
396 private logPrefix = (): string => {
397 return logPrefix(' Bootstrap |');
398 };
399 }