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