refactor: more coding style fixes
[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
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,
9bf0ef23
JB
38 isNotEmptyArray,
39 isNullOrUndefined,
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,
a807045b 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 {
66a7748d 133 const nbStations = stationTemplateUrl.numberOfStations ?? 0
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${
9bf0ef23 159 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
82e9c15a
JB
160 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
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
JB
226 const waitTimeout = setTimeout(() => {
227 const message = `Timeout ${formatDurationMilliSeconds(
66a7748d
JB
228 Constants.STOP_CHARGING_STATIONS_TIMEOUT
229 )} reached at stopping charging stations`
230 console.warn(chalk.yellow(message))
231 reject(new Error(message))
232 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
36adaf06
JB
233 waitChargingStationEvents(
234 this,
235 ChargingStationWorkerMessageEvents.stopped,
66a7748d 236 this.numberOfChargingStations
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
487f0dfd
JB
250 switch (workerConfiguration?.elementsPerWorker) {
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
JB
365 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
366 this.storage?.storePerformanceStatistics(data) as undefined
367 }
32de5a57 368
66a7748d
JB
369 private initializeCounters (): void {
370 if (!this.initializedCounters) {
371 this.resetCounters()
372 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 374 if (isNotEmptyArray(stationTemplateUrls)) {
66a7748d 375 this.numberOfChargingStationTemplates = stationTemplateUrls.length
7436ee0d 376 for (const stationTemplateUrl of stationTemplateUrls) {
66a7748d 377 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0
7436ee0d 378 }
a596d200
JB
379 } else {
380 console.warn(
66a7748d
JB
381 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
382 )
383 exit(exitCodes.missingChargingStationsConfiguration)
a596d200
JB
384 }
385 if (this.numberOfChargingStations === 0) {
66a7748d
JB
386 console.warn(chalk.yellow('No charging station template enabled in configuration, exiting'))
387 exit(exitCodes.noChargingStationTemplates)
a596d200 388 }
66a7748d 389 this.initializedCounters = true
846d2851 390 }
7c72977b
JB
391 }
392
66a7748d
JB
393 private resetCounters (): void {
394 this.numberOfChargingStationTemplates = 0
395 this.numberOfChargingStations = 0
396 this.numberOfStartedChargingStations = 0
0f040ac0
JB
397 }
398
66a7748d 399 private async startChargingStation (
e7aeea18 400 index: number,
66a7748d 401 stationTemplateUrl: StationTemplateUrl
e7aeea18 402 ): Promise<void> {
6ed3c845 403 await this.workerImplementation?.addElement({
717c1e56 404 index,
d972af76
JB
405 templateFile: join(
406 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
407 'assets',
408 'station-templates',
66a7748d
JB
409 stationTemplateUrl.file
410 )
411 })
717c1e56
JB
412 }
413
66a7748d 414 private gracefulShutdown (): void {
f130b8e6
JB
415 this.stop()
416 .then(() => {
66a7748d
JB
417 console.info(`${chalk.green('Graceful shutdown')}`)
418 this.uiServer?.stop()
36adaf06
JB
419 // stop() asks for charging stations to stop by default
420 this.waitChargingStationsStopped()
421 .then(() => {
66a7748d 422 exit(exitCodes.succeeded)
36adaf06 423 })
5b2721db 424 .catch(() => {
66a7748d
JB
425 exit(exitCodes.gracefulShutdownError)
426 })
f130b8e6
JB
427 })
428 .catch((error) => {
66a7748d
JB
429 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
430 exit(exitCodes.gracefulShutdownError)
431 })
36adaf06 432 }
f130b8e6 433
66a7748d
JB
434 private readonly logPrefix = (): string => {
435 return logPrefix(' Bootstrap |')
436 }
ded13d97 437}