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