feat: add graceful shutdown
[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 type { AbstractUIServer } from './ui-server/AbstractUIServer';
10 import { UIServerFactory } from './ui-server/UIServerFactory';
11 import packageJson from '../../package.json' assert { type: 'json' };
12 import { BaseError } from '../exception';
13 import { type Storage, StorageFactory } from '../performance';
14 import {
15 type ChargingStationData,
16 type ChargingStationWorkerData,
17 type ChargingStationWorkerMessage,
18 type ChargingStationWorkerMessageData,
19 ChargingStationWorkerMessageEvents,
20 ProcedureName,
21 type StationTemplateUrl,
22 type Statistics,
23 } from '../types';
24 import { Configuration, Constants, 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 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
49 process.on(signal, () => {
50 this.gracefulShutdown().catch(Constants.EMPTY_FUNCTION);
51 });
52 }
53 // Enable unconditionally for now
54 ErrorUtils.handleUnhandledRejection();
55 ErrorUtils.handleUncaughtException();
56 this.initializedCounters = false;
57 this.started = false;
58 this.initializeCounters();
59 this.workerImplementation = null;
60 this.workerScript = path.join(
61 path.dirname(fileURLToPath(import.meta.url)),
62 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
63 );
64 Configuration.getUIServer().enabled === true &&
65 (this.uiServer = UIServerFactory.getUIServerImplementation(Configuration.getUIServer()));
66 Configuration.getPerformanceStorage().enabled === true &&
67 (this.storage = StorageFactory.getStorage(
68 Configuration.getPerformanceStorage().type,
69 Configuration.getPerformanceStorage().uri,
70 this.logPrefix()
71 ));
72 Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart());
73 }
74
75 public static getInstance(): Bootstrap {
76 if (Bootstrap.instance === null) {
77 Bootstrap.instance = new Bootstrap();
78 }
79 return Bootstrap.instance;
80 }
81
82 public async start(): Promise<void> {
83 if (isMainThread && this.started === false) {
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);
95 }
96 } catch (error) {
97 console.error(
98 chalk.red(
99 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
100 ),
101 error
102 );
103 }
104 }
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 ${
110 Configuration.workerDynamicPoolInUse()
111 ? `${Configuration.getWorker().poolMinSize?.toString()}/`
112 : ''
113 }${this.workerImplementation?.size}${
114 Configuration.workerPoolInUse()
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;
125 } else {
126 console.error(chalk.red('Cannot start an already started charging stations simulator'));
127 }
128 }
129
130 public async stop(): Promise<void> {
131 if (isMainThread && this.started === true) {
132 await this.uiServer?.sendBroadcastChannelRequest(
133 Utils.generateUUID(),
134 ProcedureName.STOP_CHARGING_STATION,
135 Constants.EMPTY_FREEZED_OBJECT
136 );
137 await this.workerImplementation?.stop();
138 this.workerImplementation = null;
139 this.uiServer?.stop();
140 await this.storage?.close();
141 this.initializedCounters = false;
142 this.started = false;
143 } else {
144 console.error(chalk.red('Cannot stop a not started charging stations simulator'));
145 }
146 }
147
148 public async restart(): Promise<void> {
149 await this.stop();
150 await this.start();
151 }
152
153 private initializeWorkerImplementation(): void {
154 this.workerImplementation === null &&
155 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
156 this.workerScript,
157 Configuration.getWorker().processType,
158 {
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,
164 poolOptions: {
165 workerChoiceStrategy: Configuration.getWorker().poolStrategy,
166 },
167 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
168 }
169 ));
170 }
171
172 private messageHandler(
173 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
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) {
184 case ChargingStationWorkerMessageEvents.started:
185 this.workerEventStarted(msg.data as ChargingStationData);
186 break;
187 case ChargingStationWorkerMessageEvents.stopped:
188 this.workerEventStopped(msg.data as ChargingStationData);
189 break;
190 case ChargingStationWorkerMessageEvents.updated:
191 this.workerEventUpdated(msg.data as ChargingStationData);
192 break;
193 case ChargingStationWorkerMessageEvents.performanceStatistics:
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
211 private workerEventStarted = (data: ChargingStationData) => {
212 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
213 ++this.numberOfStartedChargingStations;
214 logger.info(
215 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
216 data.stationInfo.chargingStationId
217 } (hashId: ${data.stationInfo.hashId}) started (${
218 this.numberOfStartedChargingStations
219 } started from ${this.numberOfChargingStations})`
220 );
221 };
222
223 private workerEventStopped = (data: ChargingStationData) => {
224 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
225 --this.numberOfStartedChargingStations;
226 logger.info(
227 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
228 data.stationInfo.chargingStationId
229 } (hashId: ${data.stationInfo.hashId}) stopped (${
230 this.numberOfStartedChargingStations
231 } started from ${this.numberOfChargingStations})`
232 );
233 };
234
235 private workerEventUpdated = (data: ChargingStationData) => {
236 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
237 };
238
239 private workerEventPerformanceStatistics = (data: Statistics) => {
240 this.storage.storePerformanceStatistics(data) as void;
241 };
242
243 private initializeCounters() {
244 if (this.initializedCounters === false) {
245 this.numberOfChargingStationTemplates = 0;
246 this.numberOfChargingStations = 0;
247 const stationTemplateUrls = Configuration.getStationTemplateUrls();
248 if (Utils.isNotEmptyArray(stationTemplateUrls)) {
249 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
250 for (const stationTemplateUrl of stationTemplateUrls) {
251 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
252 }
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;
267 }
268 }
269
270 private async startChargingStation(
271 index: number,
272 stationTemplateUrl: StationTemplateUrl
273 ): Promise<void> {
274 await this.workerImplementation?.addElement({
275 index,
276 templateFile: path.join(
277 path.dirname(fileURLToPath(import.meta.url)),
278 'assets',
279 'station-templates',
280 stationTemplateUrl.file
281 ),
282 });
283 }
284
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
295 private logPrefix = (): string => {
296 return Utils.logPrefix(' Bootstrap |');
297 };
298 }