docs: add FIXME
[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 path 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 packageJson 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 = packageJson.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 = path.join(
73 path.dirname(fileURLToPath(import.meta.url)),
74 `ChargingStationWorker${path.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 this.started = true;
142 this.starting = false;
143 } else {
144 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
145 }
146 } else {
147 console.error(chalk.red('Cannot start an already started charging stations simulator'));
148 }
149 }
150
151 public async stop(): Promise<void> {
152 if (!isMainThread) {
153 throw new Error('Cannot stop charging stations simulator from worker thread');
154 }
155 if (this.started === true) {
156 if (this.stopping === false) {
157 this.stopping = true;
158 await this.uiServer?.sendInternalRequest(
159 this.uiServer.buildProtocolRequest(
160 Utils.generateUUID(),
161 ProcedureName.STOP_CHARGING_STATION,
162 Constants.EMPTY_FREEZED_OBJECT
163 )
164 );
165 await Promise.race([
166 ChargingStationUtils.waitForChargingStationEvents(
167 this,
168 ChargingStationWorkerMessageEvents.stopped,
169 this.numberOfChargingStations
170 ),
171 new Promise<string>((resolve) => {
172 setTimeout(() => {
173 const message = `Timeout reached ${Utils.formatDurationMilliSeconds(
174 Constants.STOP_SIMULATOR_TIMEOUT
175 )} at stopping charging stations simulator`;
176 console.warn(chalk.yellow(message));
177 resolve(message);
178 }, Constants.STOP_SIMULATOR_TIMEOUT);
179 }),
180 ]);
181 await this.workerImplementation?.stop();
182 this.workerImplementation = null;
183 this.uiServer?.stop();
184 await this.storage?.close();
185 this.resetCounters();
186 this.initializedCounters = false;
187 this.started = false;
188 this.stopping = false;
189 } else {
190 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
191 }
192 } else {
193 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
194 }
195 }
196
197 public async restart(): Promise<void> {
198 await this.stop();
199 await this.start();
200 }
201
202 private initializeWorkerImplementation(): void {
203 this.workerImplementation === null &&
204 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
205 this.workerScript,
206 Configuration.getWorker().processType,
207 {
208 workerStartDelay: Configuration.getWorker().startDelay,
209 elementStartDelay: Configuration.getWorker().elementStartDelay,
210 poolMaxSize: Configuration.getWorker().poolMaxSize,
211 poolMinSize: Configuration.getWorker().poolMinSize,
212 elementsPerWorker: Configuration.getWorker().elementsPerWorker,
213 poolOptions: {
214 workerChoiceStrategy: Configuration.getWorker().poolStrategy,
215 messageHandler: this.messageHandler.bind(this) as (message: unknown) => void,
216 },
217 }
218 ));
219 }
220
221 private messageHandler(
222 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
223 ): void {
224 // logger.debug(
225 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
226 // msg,
227 // null,
228 // 2
229 // )}`
230 // );
231 try {
232 switch (msg.id) {
233 case ChargingStationWorkerMessageEvents.started:
234 this.workerEventStarted(msg.data as ChargingStationData);
235 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
236 break;
237 case ChargingStationWorkerMessageEvents.stopped:
238 this.workerEventStopped(msg.data as ChargingStationData);
239 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
240 break;
241 case ChargingStationWorkerMessageEvents.updated:
242 this.workerEventUpdated(msg.data as ChargingStationData);
243 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
244 break;
245 case ChargingStationWorkerMessageEvents.performanceStatistics:
246 this.workerEventPerformanceStatistics(msg.data as Statistics);
247 this.emit(
248 ChargingStationWorkerMessageEvents.performanceStatistics,
249 msg.data as Statistics
250 );
251 break;
252 default:
253 throw new BaseError(
254 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
255 );
256 }
257 } catch (error) {
258 logger.error(
259 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
260 msg.id
261 }' event:`,
262 error
263 );
264 }
265 }
266
267 private workerEventStarted = (data: ChargingStationData) => {
268 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
269 ++this.numberOfStartedChargingStations;
270 logger.info(
271 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
272 data.stationInfo.chargingStationId
273 } (hashId: ${data.stationInfo.hashId}) started (${
274 this.numberOfStartedChargingStations
275 } started from ${this.numberOfChargingStations})`
276 );
277 };
278
279 private workerEventStopped = (data: ChargingStationData) => {
280 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
281 --this.numberOfStartedChargingStations;
282 logger.info(
283 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
284 data.stationInfo.chargingStationId
285 } (hashId: ${data.stationInfo.hashId}) stopped (${
286 this.numberOfStartedChargingStations
287 } started from ${this.numberOfChargingStations})`
288 );
289 };
290
291 private workerEventUpdated = (data: ChargingStationData) => {
292 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
293 };
294
295 private workerEventPerformanceStatistics = (data: Statistics) => {
296 this.storage.storePerformanceStatistics(data) as void;
297 };
298
299 private initializeCounters() {
300 if (this.initializedCounters === false) {
301 this.resetCounters();
302 const stationTemplateUrls = Configuration.getStationTemplateUrls();
303 if (Utils.isNotEmptyArray(stationTemplateUrls)) {
304 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
305 for (const stationTemplateUrl of stationTemplateUrls) {
306 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
307 }
308 } else {
309 console.warn(
310 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
311 );
312 process.exit(exitCodes.missingChargingStationsConfiguration);
313 }
314 if (this.numberOfChargingStations === 0) {
315 console.warn(
316 chalk.yellow('No charging station template enabled in configuration, exiting')
317 );
318 process.exit(exitCodes.noChargingStationTemplates);
319 }
320 this.initializedCounters = true;
321 }
322 }
323
324 private resetCounters(): void {
325 this.numberOfChargingStationTemplates = 0;
326 this.numberOfChargingStations = 0;
327 this.numberOfStartedChargingStations = 0;
328 }
329
330 private async startChargingStation(
331 index: number,
332 stationTemplateUrl: StationTemplateUrl
333 ): Promise<void> {
334 await this.workerImplementation?.addElement({
335 index,
336 templateFile: path.join(
337 path.dirname(fileURLToPath(import.meta.url)),
338 'assets',
339 'station-templates',
340 stationTemplateUrl.file
341 ),
342 });
343 }
344
345 private gracefulShutdown = (): void => {
346 console.info(`${chalk.green('Graceful shutdown')}`);
347 this.stop()
348 .then(() => {
349 process.exit(0);
350 })
351 .catch((error) => {
352 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
353 process.exit(1);
354 });
355 };
356
357 private logPrefix = (): string => {
358 return Utils.logPrefix(' Bootstrap |');
359 };
360 }