1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { EventEmitter
} from
'node:events'
4 import { dirname
, extname
, join
, parse
} from
'node:path'
5 import process
, { exit
} from
'node:process'
6 import { fileURLToPath
} from
'node:url'
7 import { isMainThread
} from
'node:worker_threads'
8 import type { Worker
} from
'worker_threads'
10 import chalk from
'chalk'
11 import { type MessageHandler
, availableParallelism
} from
'poolifier'
13 import { waitChargingStationEvents
} from
'./Helpers.js'
14 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer.js'
15 import { UIServerFactory
} from
'./ui-server/UIServerFactory.js'
16 import { version
} from
'../../package.json'
17 import { BaseError
} from
'../exception/index.js'
18 import { type Storage
, StorageFactory
} from
'../performance/index.js'
20 type ChargingStationData
,
21 type ChargingStationOptions
,
22 type ChargingStationWorkerData
,
23 type ChargingStationWorkerEventError
,
24 type ChargingStationWorkerMessage
,
25 type ChargingStationWorkerMessageData
,
26 ChargingStationWorkerMessageEvents
,
30 type StorageConfiguration
,
31 type UIServerConfiguration
,
32 type WorkerConfiguration
33 } from
'../types/index.js'
37 formatDurationMilliSeconds
,
39 handleUncaughtException
,
40 handleUnhandledRejection
,
46 } from
'../utils/index.js'
47 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
49 const moduleName
= 'Bootstrap'
53 missingChargingStationsConfiguration
= 1,
54 duplicateChargingStationTemplateUrls
= 2,
55 noChargingStationTemplates
= 3,
56 gracefulShutdownError
= 4
59 interface TemplateChargingStations
{
66 export class Bootstrap
extends EventEmitter
{
67 private static instance
: Bootstrap
| null = null
68 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
69 private readonly uiServer
?: AbstractUIServer
70 private storage
?: Storage
71 private readonly chargingStationsByTemplate
: Map
<string, TemplateChargingStations
>
72 private readonly version
: string = version
73 private initializedCounters
: boolean
74 private started
: boolean
75 private starting
: boolean
76 private stopping
: boolean
78 private constructor () {
80 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
81 process
.on(signal
, this.gracefulShutdown
.bind(this))
83 // Enable unconditionally for now
84 handleUnhandledRejection()
85 handleUncaughtException()
89 this.chargingStationsByTemplate
= new Map
<string, TemplateChargingStations
>()
90 this.uiServer
= UIServerFactory
.getUIServerImplementation(
91 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
93 this.initializedCounters
= false
94 this.initializeCounters()
95 Configuration
.configurationChangeCallback
= async () => {
97 await Bootstrap
.getInstance().restart()
102 public static getInstance (): Bootstrap
{
103 if (Bootstrap
.instance
=== null) {
104 Bootstrap
.instance
= new Bootstrap()
106 return Bootstrap
.instance
109 public get
numberOfChargingStationTemplates (): number {
110 return this.chargingStationsByTemplate
.size
113 public get
numberOfConfiguredChargingStations (): number {
114 return [...this.chargingStationsByTemplate
.values()].reduce(
115 (accumulator
, value
) => accumulator
+ value
.configured
,
120 public getLastIndex (templateName
: string): number {
121 return this.chargingStationsByTemplate
.get(templateName
)?.lastIndex
?? 0
124 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
125 return this.storage
?.getPerformanceStatistics()
128 private get
numberOfAddedChargingStations (): number {
129 return [...this.chargingStationsByTemplate
.values()].reduce(
130 (accumulator
, value
) => accumulator
+ value
.added
,
135 private get
numberOfStartedChargingStations (): number {
136 return [...this.chargingStationsByTemplate
.values()].reduce(
137 (accumulator
, value
) => accumulator
+ value
.started
,
142 public async start (): Promise
<void> {
144 if (!this.starting
) {
146 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
147 this.on(ChargingStationWorkerMessageEvents
.deleted
, this.workerEventDeleted
)
148 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
149 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
150 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
152 ChargingStationWorkerMessageEvents
.performanceStatistics
,
153 this.workerEventPerformanceStatistics
156 ChargingStationWorkerMessageEvents
.workerElementError
,
157 (eventError
: ChargingStationWorkerEventError
) => {
159 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
164 this.initializeCounters()
165 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
166 ConfigurationSection
.worker
168 this.initializeWorkerImplementation(workerConfiguration
)
169 await this.workerImplementation
?.start()
170 const performanceStorageConfiguration
=
171 Configuration
.getConfigurationSection
<StorageConfiguration
>(
172 ConfigurationSection
.performanceStorage
174 if (performanceStorageConfiguration
.enabled
=== true) {
175 this.storage
= StorageFactory
.getStorage(
176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177 performanceStorageConfiguration
.type!,
178 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
179 performanceStorageConfiguration
.uri
!,
182 await this.storage
?.open()
184 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
185 .enabled
=== true && this.uiServer
?.start()
186 // Start ChargingStation object instance in worker thread
187 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
188 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
191 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
192 ?.configured
?? stationTemplateUrl
.numberOfStations
193 for (let index
= 1; index
<= nbStations
; index
++) {
194 await this.addChargingStation(index
, stationTemplateUrl
.file
)
199 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
207 `Charging stations simulator ${
209 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
210 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
211 }${this.workerImplementation?.size}${
212 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
213 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
214 this.workerImplementation?.maxElementsPerWorker != null
215 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
220 Configuration
.workerDynamicPoolInUse() &&
223 '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'
226 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
228 this.starting
= false
230 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
233 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
237 public async stop (): Promise
<void> {
239 if (!this.stopping
) {
241 await this.uiServer
?.sendInternalRequest(
242 this.uiServer
.buildProtocolRequest(
244 ProcedureName
.STOP_CHARGING_STATION
,
245 Constants
.EMPTY_FROZEN_OBJECT
249 await this.waitChargingStationsStopped()
251 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
253 await this.workerImplementation
?.stop()
254 delete this.workerImplementation
255 this.removeAllListeners()
256 await this.storage
?.close()
259 this.stopping
= false
261 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
264 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
268 private async restart (): Promise
<void> {
270 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
271 .enabled
!== true && this.uiServer
?.stop()
272 this.initializedCounters
= false
276 private async waitChargingStationsStopped (): Promise
<string> {
277 return await new Promise
<string>((resolve
, reject
) => {
278 const waitTimeout
= setTimeout(() => {
279 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
280 Constants.STOP_CHARGING_STATIONS_TIMEOUT
281 )} reached at stopping charging stations`
282 console
.warn(chalk
.yellow(timeoutMessage
))
283 reject(new Error(timeoutMessage
))
284 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
285 waitChargingStationEvents(
287 ChargingStationWorkerMessageEvents
.stopped
,
288 this.numberOfStartedChargingStations
291 resolve('Charging stations stopped')
295 clearTimeout(waitTimeout
)
300 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
304 let elementsPerWorker
: number
305 switch (workerConfiguration
.elementsPerWorker
) {
307 elementsPerWorker
= this.numberOfConfiguredChargingStations
312 this.numberOfConfiguredChargingStations
> availableParallelism()
313 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
317 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
319 dirname(fileURLToPath(import.meta
.url
)),
320 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
322 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
323 workerConfiguration
.processType
!,
325 workerStartDelay
: workerConfiguration
.startDelay
,
326 elementStartDelay
: workerConfiguration
.elementStartDelay
,
327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
328 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
329 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
330 poolMinSize
: workerConfiguration
.poolMinSize
!,
333 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
334 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
340 private messageHandler (
341 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
344 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
352 case ChargingStationWorkerMessageEvents
.added
:
353 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
355 case ChargingStationWorkerMessageEvents
.deleted
:
356 this.emit(ChargingStationWorkerMessageEvents
.deleted
, msg
.data
)
358 case ChargingStationWorkerMessageEvents
.started
:
359 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
361 case ChargingStationWorkerMessageEvents
.stopped
:
362 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
364 case ChargingStationWorkerMessageEvents
.updated
:
365 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
367 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
368 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
370 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
371 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
373 case ChargingStationWorkerMessageEvents
.workerElementError
:
374 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
378 `Unknown charging station worker event: '${
380 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
385 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
393 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
394 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
398 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
399 data.stationInfo.chargingStationId
400 } (hashId: ${data.stationInfo.hashId}) added (${
401 this.numberOfAddedChargingStations
402 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
406 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
407 this.uiServer
?.chargingStations
.delete(data
.stationInfo
.hashId
)
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
409 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
411 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
412 data.stationInfo.chargingStationId
413 } (hashId: ${data.stationInfo.hashId}) deleted (${
414 this.numberOfAddedChargingStations
415 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
419 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
420 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
424 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
425 data.stationInfo.chargingStationId
426 } (hashId: ${data.stationInfo.hashId}) started (${
427 this.numberOfStartedChargingStations
428 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
432 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
433 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
434 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
435 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
437 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
438 data.stationInfo.chargingStationId
439 } (hashId: ${data.stationInfo.hashId}) stopped (${
440 this.numberOfStartedChargingStations
441 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
445 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
446 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
449 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
450 // eslint-disable-next-line @typescript-eslint/unbound-method
451 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
453 this.storage
.storePerformanceStatistics
as (
454 performanceStatistics
: Statistics
456 )(data
).catch(Constants
.EMPTY_FUNCTION
)
458 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
464 private initializeCounters (): void {
465 if (!this.initializedCounters
) {
466 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
467 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
468 if (isNotEmptyArray(stationTemplateUrls
)) {
469 for (const stationTemplateUrl
of stationTemplateUrls
) {
470 const templateName
= parse(stationTemplateUrl
.file
).name
471 this.chargingStationsByTemplate
.set(templateName
, {
472 configured
: stationTemplateUrl
.numberOfStations
,
477 this.uiServer
?.chargingStationTemplates
.add(templateName
)
479 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
482 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
485 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
489 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
491 exit(exitCodes
.missingChargingStationsConfiguration
)
494 this.numberOfConfiguredChargingStations
=== 0 &&
495 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
500 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
503 exit(exitCodes
.noChargingStationTemplates
)
505 this.initializedCounters
= true
509 public async addChargingStation (
511 stationTemplateFile
: string,
512 options
?: ChargingStationOptions
514 await this.workerImplementation
?.addElement({
517 dirname(fileURLToPath(import.meta
.url
)),
524 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
525 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
527 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
531 private gracefulShutdown (): void {
534 console
.info(chalk
.green('Graceful shutdown'))
535 this.uiServer
?.stop()
536 this.waitChargingStationsStopped()
538 exit(exitCodes
.succeeded
)
541 exit(exitCodes
.gracefulShutdownError
)
545 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
546 exit(exitCodes
.gracefulShutdownError
)
550 private readonly logPrefix
= (): string => {
551 return logPrefix(' Bootstrap |')