refactor: cleanup unneeded type casting
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
CommitLineData
a19b897d 1// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
b4d34251 2
66a7748d
JB
3import { EventEmitter } from 'node:events'
4import { dirname, extname, join } from 'node:path'
5import process, { exit } from 'node:process'
6import { fileURLToPath } from 'node:url'
ba9a56a6 7import type { Worker } from 'worker_threads'
8114d10e 8
66a7748d 9import chalk from 'chalk'
ba9a56a6 10import { type MessageHandler, availableParallelism } from 'poolifier'
8114d10e 11
66a7748d
JB
12import { waitChargingStationEvents } from './Helpers.js'
13import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
14import { UIServerFactory } from './ui-server/UIServerFactory.js'
15import { version } from '../../package.json'
16import { BaseError } from '../exception/index.js'
17import { type Storage, StorageFactory } from '../performance/index.js'
e7aeea18 18import {
bbe10d5f
JB
19 type ChargingStationData,
20 type ChargingStationWorkerData,
21 type ChargingStationWorkerMessage,
22 type ChargingStationWorkerMessageData,
e7aeea18 23 ChargingStationWorkerMessageEvents,
5d049829 24 ConfigurationSection,
6bd808fd 25 ProcedureName,
268a74bb
JB
26 type StationTemplateUrl,
27 type Statistics,
5d049829
JB
28 type StorageConfiguration,
29 type UIServerConfiguration,
66a7748d
JB
30 type WorkerConfiguration
31} from '../types/index.js'
fa5995d6
JB
32import {
33 Configuration,
34 Constants,
9bf0ef23
JB
35 formatDurationMilliSeconds,
36 generateUUID,
fa5995d6
JB
37 handleUncaughtException,
38 handleUnhandledRejection,
be0a4d4d 39 isAsyncFunction,
9bf0ef23 40 isNotEmptyArray,
9bf0ef23 41 logPrefix,
66a7748d
JB
42 logger
43} from '../utils/index.js'
44import { type WorkerAbstract, WorkerFactory } from '../worker/index.js'
ded13d97 45
66a7748d 46const moduleName = 'Bootstrap'
32de5a57 47
a307349b 48enum exitCodes {
a51a4ead 49 succeeded = 0,
a307349b
JB
50 missingChargingStationsConfiguration = 1,
51 noChargingStationTemplates = 2,
a223d9be 52 gracefulShutdownError = 3
a307349b 53}
e4cb2c14 54
f130b8e6 55export class Bootstrap extends EventEmitter {
66a7748d
JB
56 private static instance: Bootstrap | null = null
57 public numberOfChargingStations!: number
58 public numberOfChargingStationTemplates!: number
59 private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
60 private readonly uiServer?: AbstractUIServer
61 private storage?: Storage
62 private numberOfStartedChargingStations!: number
63 private readonly version: string = version
64 private initializedCounters: boolean
65 private started: boolean
66 private starting: boolean
67 private stopping: boolean
ded13d97 68
66a7748d
JB
69 private constructor () {
70 super()
6bd808fd 71 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
66a7748d 72 process.on(signal, this.gracefulShutdown.bind(this))
6bd808fd 73 }
4724a293 74 // Enable unconditionally for now
66a7748d
JB
75 handleUnhandledRejection()
76 handleUncaughtException()
77 this.started = false
78 this.starting = false
79 this.stopping = false
80 this.initializedCounters = false
81 this.initializeCounters()
36adaf06 82 this.uiServer = UIServerFactory.getUIServerImplementation(
66a7748d
JB
83 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
84 )
85 Configuration.configurationChangeCallback = async () => {
86 await Bootstrap.getInstance().restart(false)
87 }
ded13d97
JB
88 }
89
66a7748d 90 public static getInstance (): Bootstrap {
1ca780f9 91 if (Bootstrap.instance === null) {
66a7748d 92 Bootstrap.instance = new Bootstrap()
ded13d97 93 }
66a7748d 94 return Bootstrap.instance
ded13d97
JB
95 }
96
66a7748d
JB
97 public async start (): Promise<void> {
98 if (!this.started) {
99 if (!this.starting) {
100 this.starting = true
101 this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted)
102 this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped)
103 this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated)
4354af5a
JB
104 this.on(
105 ChargingStationWorkerMessageEvents.performanceStatistics,
66a7748d
JB
106 this.workerEventPerformanceStatistics
107 )
108 this.initializeCounters()
5b373a23 109 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
66a7748d
JB
110 ConfigurationSection.worker
111 )
112 this.initializeWorkerImplementation(workerConfiguration)
113 await this.workerImplementation?.start()
6d2b7d01
JB
114 const performanceStorageConfiguration =
115 Configuration.getConfigurationSection<StorageConfiguration>(
66a7748d
JB
116 ConfigurationSection.performanceStorage
117 )
6d2b7d01
JB
118 if (performanceStorageConfiguration.enabled === true) {
119 this.storage = StorageFactory.getStorage(
66a7748d 120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 121 performanceStorageConfiguration.type!,
66a7748d 122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 123 performanceStorageConfiguration.uri!,
66a7748d
JB
124 this.logPrefix()
125 )
126 await this.storage?.open()
6d2b7d01 127 }
36adaf06 128 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
66a7748d 129 .enabled === true && this.uiServer?.start()
82e9c15a 130 // Start ChargingStation object instance in worker thread
66a7748d 131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 132 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
82e9c15a 133 try {
5199f9fd 134 const nbStations = stationTemplateUrl.numberOfStations
82e9c15a 135 for (let index = 1; index <= nbStations; index++) {
66a7748d 136 await this.startChargingStation(index, stationTemplateUrl)
82e9c15a
JB
137 }
138 } catch (error) {
139 console.error(
140 chalk.red(
66a7748d 141 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
82e9c15a 142 ),
66a7748d
JB
143 error
144 )
ded13d97 145 }
ded13d97 146 }
82e9c15a
JB
147 console.info(
148 chalk.green(
149 `Charging stations simulator ${
150 this.version
151 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
152 Configuration.workerDynamicPoolInUse()
5b373a23 153 ? `${workerConfiguration.poolMinSize?.toString()}/`
82e9c15a
JB
154 : ''
155 }${this.workerImplementation?.size}${
156 Configuration.workerPoolInUse()
5b373a23 157 ? `/${workerConfiguration.poolMaxSize?.toString()}`
82e9c15a 158 : ''
5b373a23 159 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
401fa922 160 this.workerImplementation?.maxElementsPerWorker != null
5199f9fd 161 ? ` (${this.workerImplementation.maxElementsPerWorker} charging station(s) per worker)`
82e9c15a 162 : ''
66a7748d
JB
163 }`
164 )
165 )
56e2e1ab
JB
166 Configuration.workerDynamicPoolInUse() &&
167 console.warn(
168 chalk.yellow(
66a7748d
JB
169 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead'
170 )
171 )
172 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info)
173 this.started = true
174 this.starting = false
82e9c15a 175 } else {
66a7748d 176 console.error(chalk.red('Cannot start an already starting charging stations simulator'))
ded13d97 177 }
b322b8b4 178 } else {
66a7748d 179 console.error(chalk.red('Cannot start an already started charging stations simulator'))
ded13d97
JB
180 }
181 }
182
66a7748d
JB
183 public async stop (stopChargingStations = true): Promise<void> {
184 if (this.started) {
185 if (!this.stopping) {
186 this.stopping = true
187 if (stopChargingStations) {
36adaf06
JB
188 await this.uiServer?.sendInternalRequest(
189 this.uiServer.buildProtocolRequest(
190 generateUUID(),
191 ProcedureName.STOP_CHARGING_STATION,
66a7748d
JB
192 Constants.EMPTY_FROZEN_OBJECT
193 )
194 )
5b2721db 195 try {
66a7748d 196 await this.waitChargingStationsStopped()
5b2721db 197 } catch (error) {
66a7748d 198 console.error(chalk.red('Error while waiting for charging stations to stop: '), error)
5b2721db 199 }
ab7a96fa 200 }
66a7748d
JB
201 await this.workerImplementation?.stop()
202 delete this.workerImplementation
203 this.removeAllListeners()
204 await this.storage?.close()
205 delete this.storage
206 this.resetCounters()
207 this.initializedCounters = false
208 this.started = false
209 this.stopping = false
82e9c15a 210 } else {
66a7748d 211 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'))
82e9c15a 212 }
b322b8b4 213 } else {
66a7748d 214 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'))
ded13d97 215 }
ded13d97
JB
216 }
217
66a7748d
JB
218 public async restart (stopChargingStations?: boolean): Promise<void> {
219 await this.stop(stopChargingStations)
73edcc94 220 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
66a7748d
JB
221 .enabled === false && this.uiServer?.stop()
222 await this.start()
ded13d97
JB
223 }
224
66a7748d
JB
225 private async waitChargingStationsStopped (): Promise<string> {
226 return await new Promise<string>((resolve, reject) => {
5b2721db 227 const waitTimeout = setTimeout(() => {
a01134ed 228 const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
66a7748d
JB
229 Constants.STOP_CHARGING_STATIONS_TIMEOUT
230 )} reached at stopping charging stations`
a01134ed
JB
231 console.warn(chalk.yellow(timeoutMessage))
232 reject(new Error(timeoutMessage))
66a7748d 233 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
36adaf06
JB
234 waitChargingStationEvents(
235 this,
236 ChargingStationWorkerMessageEvents.stopped,
a01134ed 237 this.numberOfStartedChargingStations
5b2721db
JB
238 )
239 .then(() => {
66a7748d 240 resolve('Charging stations stopped')
5b2721db 241 })
b7ee97c1 242 .catch(reject)
5b2721db 243 .finally(() => {
66a7748d
JB
244 clearTimeout(waitTimeout)
245 })
246 })
36adaf06
JB
247 }
248
66a7748d
JB
249 private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
250 let elementsPerWorker: number | undefined
5199f9fd 251 switch (workerConfiguration.elementsPerWorker) {
487f0dfd
JB
252 case 'auto':
253 elementsPerWorker =
254 this.numberOfChargingStations > availableParallelism()
255 ? Math.round(this.numberOfChargingStations / (availableParallelism() * 1.5))
66a7748d
JB
256 : 1
257 break
c20d5d72 258 case 'all':
66a7748d
JB
259 elementsPerWorker = this.numberOfChargingStations
260 break
8603c1ca 261 }
6d2b7d01
JB
262 this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
263 join(
264 dirname(fileURLToPath(import.meta.url)),
66a7748d 265 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
6d2b7d01 266 ),
66a7748d 267 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
268 workerConfiguration.processType!,
269 {
270 workerStartDelay: workerConfiguration.startDelay,
271 elementStartDelay: workerConfiguration.elementStartDelay,
66a7748d 272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 273 poolMaxSize: workerConfiguration.poolMaxSize!,
66a7748d 274 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
275 poolMinSize: workerConfiguration.poolMinSize!,
276 elementsPerWorker: elementsPerWorker ?? (workerConfiguration.elementsPerWorker as number),
277 poolOptions: {
ba9a56a6 278 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
66a7748d
JB
279 workerOptions: { resourceLimits: workerConfiguration.resourceLimits }
280 }
281 }
282 )
ded13d97 283 }
81797102 284
66a7748d
JB
285 private messageHandler (
286 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
287 ): void {
288 // logger.debug(
289 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
290 // msg,
4ed03b6e 291 // undefined,
66a7748d
JB
292 // 2
293 // )}`
294 // )
32de5a57 295 try {
8cc482a9 296 switch (msg.event) {
721646e9 297 case ChargingStationWorkerMessageEvents.started:
66a7748d
JB
298 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData)
299 break
721646e9 300 case ChargingStationWorkerMessageEvents.stopped:
66a7748d
JB
301 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData)
302 break
721646e9 303 case ChargingStationWorkerMessageEvents.updated:
66a7748d
JB
304 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData)
305 break
721646e9 306 case ChargingStationWorkerMessageEvents.performanceStatistics:
f130b8e6
JB
307 this.emit(
308 ChargingStationWorkerMessageEvents.performanceStatistics,
66a7748d
JB
309 msg.data as Statistics
310 )
311 break
2bb7a73e
JB
312 case ChargingStationWorkerMessageEvents.startWorkerElementError:
313 logger.error(
314 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
66a7748d
JB
315 msg.data
316 )
317 this.emit(ChargingStationWorkerMessageEvents.startWorkerElementError, msg.data)
318 break
2bb7a73e 319 case ChargingStationWorkerMessageEvents.startedWorkerElement:
66a7748d 320 break
32de5a57
LM
321 default:
322 throw new BaseError(
f93dda6a
JB
323 `Unknown charging station worker event: '${
324 msg.event
66a7748d
JB
325 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
326 )
32de5a57
LM
327 }
328 } catch (error) {
329 logger.error(
330 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 331 msg.event
32de5a57 332 }' event:`,
66a7748d
JB
333 error
334 )
32de5a57
LM
335 }
336 }
337
66a7748d
JB
338 private readonly workerEventStarted = (data: ChargingStationData): void => {
339 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
340 ++this.numberOfStartedChargingStations
56eb297e 341 logger.info(
e6159ce8 342 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 343 data.stationInfo.chargingStationId
e6159ce8 344 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 345 this.numberOfStartedChargingStations
66a7748d
JB
346 } started from ${this.numberOfChargingStations})`
347 )
348 }
32de5a57 349
66a7748d
JB
350 private readonly workerEventStopped = (data: ChargingStationData): void => {
351 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
352 --this.numberOfStartedChargingStations
56eb297e 353 logger.info(
e6159ce8 354 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 355 data.stationInfo.chargingStationId
e6159ce8 356 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 357 this.numberOfStartedChargingStations
66a7748d
JB
358 } started from ${this.numberOfChargingStations})`
359 )
360 }
32de5a57 361
66a7748d
JB
362 private readonly workerEventUpdated = (data: ChargingStationData): void => {
363 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
364 }
32de5a57 365
66a7748d 366 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
367 // eslint-disable-next-line @typescript-eslint/unbound-method
368 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
369 (
370 this.storage.storePerformanceStatistics as (
371 performanceStatistics: Statistics
372 ) => Promise<void>
373 )(data).catch(Constants.EMPTY_FUNCTION)
374 } else {
375 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
376 data
377 )
378 }
66a7748d 379 }
32de5a57 380
66a7748d
JB
381 private initializeCounters (): void {
382 if (!this.initializedCounters) {
383 this.resetCounters()
384 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
385 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 386 if (isNotEmptyArray(stationTemplateUrls)) {
66a7748d 387 this.numberOfChargingStationTemplates = stationTemplateUrls.length
7436ee0d 388 for (const stationTemplateUrl of stationTemplateUrls) {
5199f9fd 389 this.numberOfChargingStations += stationTemplateUrl.numberOfStations
7436ee0d 390 }
a596d200
JB
391 } else {
392 console.warn(
66a7748d
JB
393 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
394 )
395 exit(exitCodes.missingChargingStationsConfiguration)
a596d200
JB
396 }
397 if (this.numberOfChargingStations === 0) {
66a7748d
JB
398 console.warn(chalk.yellow('No charging station template enabled in configuration, exiting'))
399 exit(exitCodes.noChargingStationTemplates)
a596d200 400 }
66a7748d 401 this.initializedCounters = true
846d2851 402 }
7c72977b
JB
403 }
404
66a7748d
JB
405 private resetCounters (): void {
406 this.numberOfChargingStationTemplates = 0
407 this.numberOfChargingStations = 0
408 this.numberOfStartedChargingStations = 0
0f040ac0
JB
409 }
410
66a7748d 411 private async startChargingStation (
e7aeea18 412 index: number,
66a7748d 413 stationTemplateUrl: StationTemplateUrl
e7aeea18 414 ): Promise<void> {
6ed3c845 415 await this.workerImplementation?.addElement({
717c1e56 416 index,
d972af76
JB
417 templateFile: join(
418 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
419 'assets',
420 'station-templates',
66a7748d
JB
421 stationTemplateUrl.file
422 )
423 })
717c1e56
JB
424 }
425
66a7748d 426 private gracefulShutdown (): void {
f130b8e6
JB
427 this.stop()
428 .then(() => {
5199f9fd 429 console.info(chalk.green('Graceful shutdown'))
66a7748d 430 this.uiServer?.stop()
36adaf06
JB
431 // stop() asks for charging stations to stop by default
432 this.waitChargingStationsStopped()
433 .then(() => {
66a7748d 434 exit(exitCodes.succeeded)
36adaf06 435 })
5b2721db 436 .catch(() => {
66a7748d
JB
437 exit(exitCodes.gracefulShutdownError)
438 })
f130b8e6 439 })
a974c8e4 440 .catch(error => {
66a7748d
JB
441 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
442 exit(exitCodes.gracefulShutdownError)
443 })
36adaf06 444 }
f130b8e6 445
66a7748d
JB
446 private readonly logPrefix = (): string => {
447 return logPrefix(' Bootstrap |')
448 }
ded13d97 449}