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