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