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