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