refactor(simulator): factor out data transfer response
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
b4d34251 2
130783a7
JB
3import path from 'node:path';
4import { fileURLToPath } from 'node:url';
01f4001e 5import { type Worker, isMainThread } from 'node:worker_threads';
8114d10e
JB
6
7import chalk from 'chalk';
8
4c3c0d59
JB
9import type { AbstractUIServer } from './ui-server/AbstractUIServer';
10import { UIServerFactory } from './ui-server/UIServerFactory';
21ece8a1 11import packageJson from '../../package.json' assert { type: 'json' };
268a74bb 12import { BaseError } from '../exception';
17bc43d7 13import { type Storage, StorageFactory } from '../performance';
e7aeea18 14import {
bbe10d5f
JB
15 type ChargingStationData,
16 type ChargingStationWorkerData,
17 type ChargingStationWorkerMessage,
18 type ChargingStationWorkerMessageData,
e7aeea18 19 ChargingStationWorkerMessageEvents,
268a74bb
JB
20 type StationTemplateUrl,
21 type Statistics,
22} from '../types';
7671fa0b 23import { Configuration, ErrorUtils, Utils, logger } from '../utils';
268a74bb 24import { type MessageHandler, type WorkerAbstract, WorkerFactory } from '../worker';
ded13d97 25
32de5a57
LM
26const moduleName = 'Bootstrap';
27
a307349b
JB
28enum exitCodes {
29 missingChargingStationsConfiguration = 1,
30 noChargingStationTemplates = 2,
31}
e4cb2c14 32
5a010bf0 33export class Bootstrap {
535aaa27 34 private static instance: Bootstrap | null = null;
d1c99c59
JB
35 public numberOfChargingStations!: number;
36 public numberOfChargingStationTemplates!: number;
aa428a31 37 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
551e477c 38 private readonly uiServer!: AbstractUIServer | null;
6a49ad23 39 private readonly storage!: Storage;
89b7a234 40 private numberOfStartedChargingStations!: number;
21ece8a1 41 private readonly version: string = packageJson.version;
a596d200 42 private initializedCounters: boolean;
eb87fe87 43 private started: boolean;
9e23580d 44 private readonly workerScript: string;
ded13d97
JB
45
46 private constructor() {
4724a293 47 // Enable unconditionally for now
7671fa0b
JB
48 ErrorUtils.handleUnhandledRejection();
49 ErrorUtils.handleUncaughtException();
a596d200 50 this.initializedCounters = false;
af8e02ca 51 this.started = false;
a596d200 52 this.initializeCounters();
af8e02ca 53 this.workerImplementation = null;
e7aeea18 54 this.workerScript = path.join(
51022aa0 55 path.dirname(fileURLToPath(import.meta.url)),
44eb6026 56 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
e7aeea18 57 );
5af9aa8a 58 Configuration.getUIServer().enabled === true &&
976d11ec 59 (this.uiServer = UIServerFactory.getUIServerImplementation(Configuration.getUIServer()));
eb3abc4f 60 Configuration.getPerformanceStorage().enabled === true &&
e7aeea18
JB
61 (this.storage = StorageFactory.getStorage(
62 Configuration.getPerformanceStorage().type,
63 Configuration.getPerformanceStorage().uri,
64 this.logPrefix()
65 ));
7874b0b1 66 Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart());
ded13d97
JB
67 }
68
69 public static getInstance(): Bootstrap {
1ca780f9 70 if (Bootstrap.instance === null) {
ded13d97
JB
71 Bootstrap.instance = new Bootstrap();
72 }
73 return Bootstrap.instance;
74 }
75
76 public async start(): Promise<void> {
452a82ca 77 if (isMainThread && this.started === false) {
4ec634b7
JB
78 this.initializeCounters();
79 this.initializeWorkerImplementation();
80 await this.workerImplementation?.start();
81 await this.storage?.open();
82 this.uiServer?.start();
83 // Start ChargingStation object instance in worker thread
84 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()) {
85 try {
86 const nbStations = stationTemplateUrl.numberOfStations ?? 0;
87 for (let index = 1; index <= nbStations; index++) {
88 await this.startChargingStation(index, stationTemplateUrl);
ded13d97 89 }
4ec634b7
JB
90 } catch (error) {
91 console.error(
92 chalk.red(
93 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
94 ),
95 error
96 );
ded13d97 97 }
ded13d97 98 }
4ec634b7
JB
99 console.info(
100 chalk.green(
101 `Charging stations simulator ${
102 this.version
103 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
aa7d6d95 104 Configuration.workerDynamicPoolInUse()
4ec634b7
JB
105 ? `${Configuration.getWorker().poolMinSize?.toString()}/`
106 : ''
107 }${this.workerImplementation?.size}${
aa7d6d95 108 Configuration.workerPoolInUse()
4ec634b7
JB
109 ? `/${Configuration.getWorker().poolMaxSize?.toString()}`
110 : ''
111 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
112 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
113 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
114 : ''
115 }`
116 )
117 );
118 this.started = true;
b322b8b4
JB
119 } else {
120 console.error(chalk.red('Cannot start an already started charging stations simulator'));
ded13d97
JB
121 }
122 }
123
124 public async stop(): Promise<void> {
452a82ca 125 if (isMainThread && this.started === true) {
1895299d 126 await this.workerImplementation?.stop();
b19021e2 127 this.workerImplementation = null;
675fa8e3 128 this.uiServer?.stop();
6a49ad23 129 await this.storage?.close();
b6a45d9a 130 this.initializedCounters = false;
ba7965c4 131 this.started = false;
b322b8b4 132 } else {
ba7965c4 133 console.error(chalk.red('Cannot stop a not started charging stations simulator'));
ded13d97 134 }
ded13d97
JB
135 }
136
137 public async restart(): Promise<void> {
138 await this.stop();
139 await this.start();
140 }
141
ec7f4dce 142 private initializeWorkerImplementation(): void {
e2c77f10 143 this.workerImplementation === null &&
ec7f4dce
JB
144 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
145 this.workerScript,
cf2a5d9b 146 Configuration.getWorker().processType,
ec7f4dce 147 {
cf2a5d9b
JB
148 workerStartDelay: Configuration.getWorker().startDelay,
149 elementStartDelay: Configuration.getWorker().elementStartDelay,
150 poolMaxSize: Configuration.getWorker().poolMaxSize,
151 poolMinSize: Configuration.getWorker().poolMinSize,
152 elementsPerWorker: Configuration.getWorker().elementsPerWorker,
ec7f4dce 153 poolOptions: {
cf2a5d9b 154 workerChoiceStrategy: Configuration.getWorker().poolStrategy,
ec7f4dce 155 },
0e4fa348 156 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
ec7f4dce
JB
157 }
158 ));
ded13d97 159 }
81797102 160
32de5a57 161 private messageHandler(
53e5fd67 162 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
163 ): void {
164 // logger.debug(
165 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
166 // msg,
167 // null,
168 // 2
169 // )}`
170 // );
171 try {
172 switch (msg.id) {
721646e9 173 case ChargingStationWorkerMessageEvents.started:
32de5a57
LM
174 this.workerEventStarted(msg.data as ChargingStationData);
175 break;
721646e9 176 case ChargingStationWorkerMessageEvents.stopped:
32de5a57
LM
177 this.workerEventStopped(msg.data as ChargingStationData);
178 break;
721646e9 179 case ChargingStationWorkerMessageEvents.updated:
32de5a57
LM
180 this.workerEventUpdated(msg.data as ChargingStationData);
181 break;
721646e9 182 case ChargingStationWorkerMessageEvents.performanceStatistics:
32de5a57
LM
183 this.workerEventPerformanceStatistics(msg.data as Statistics);
184 break;
185 default:
186 throw new BaseError(
187 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
188 );
189 }
190 } catch (error) {
191 logger.error(
192 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
193 msg.id
194 }' event:`,
195 error
196 );
197 }
198 }
199
e2c77f10 200 private workerEventStarted = (data: ChargingStationData) => {
51c83d6f 201 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 202 ++this.numberOfStartedChargingStations;
56eb297e 203 logger.info(
e6159ce8 204 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 205 data.stationInfo.chargingStationId
e6159ce8 206 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e
JB
207 this.numberOfStartedChargingStations
208 } started from ${this.numberOfChargingStations})`
209 );
e2c77f10 210 };
32de5a57 211
e2c77f10 212 private workerEventStopped = (data: ChargingStationData) => {
51c83d6f 213 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
89b7a234 214 --this.numberOfStartedChargingStations;
56eb297e 215 logger.info(
e6159ce8 216 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 217 data.stationInfo.chargingStationId
e6159ce8 218 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e
JB
219 this.numberOfStartedChargingStations
220 } started from ${this.numberOfChargingStations})`
221 );
e2c77f10 222 };
32de5a57 223
e2c77f10 224 private workerEventUpdated = (data: ChargingStationData) => {
51c83d6f 225 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
e2c77f10 226 };
32de5a57
LM
227
228 private workerEventPerformanceStatistics = (data: Statistics) => {
229 this.storage.storePerformanceStatistics(data) as void;
230 };
231
326cec2d 232 private initializeCounters() {
a596d200
JB
233 if (this.initializedCounters === false) {
234 this.numberOfChargingStationTemplates = 0;
235 this.numberOfChargingStations = 0;
236 const stationTemplateUrls = Configuration.getStationTemplateUrls();
53ac516c 237 if (Utils.isNotEmptyArray(stationTemplateUrls)) {
41bda658 238 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
7436ee0d 239 for (const stationTemplateUrl of stationTemplateUrls) {
a596d200 240 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
7436ee0d 241 }
a596d200
JB
242 } else {
243 console.warn(
244 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
245 );
246 process.exit(exitCodes.missingChargingStationsConfiguration);
247 }
248 if (this.numberOfChargingStations === 0) {
249 console.warn(
250 chalk.yellow('No charging station template enabled in configuration, exiting')
251 );
252 process.exit(exitCodes.noChargingStationTemplates);
253 }
254 this.numberOfStartedChargingStations = 0;
255 this.initializedCounters = true;
846d2851 256 }
7c72977b
JB
257 }
258
e7aeea18
JB
259 private async startChargingStation(
260 index: number,
261 stationTemplateUrl: StationTemplateUrl
262 ): Promise<void> {
6ed3c845 263 await this.workerImplementation?.addElement({
717c1e56 264 index,
e7aeea18 265 templateFile: path.join(
51022aa0 266 path.dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
267 'assets',
268 'station-templates',
ee5f26a2 269 stationTemplateUrl.file
e7aeea18 270 ),
6ed3c845 271 });
717c1e56
JB
272 }
273
8b7072dc 274 private logPrefix = (): string => {
689dca78 275 return Utils.logPrefix(' Bootstrap |');
8b7072dc 276 };
ded13d97 277}