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