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