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