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
,
45 } from
'../utils/index.js'
46 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
48 const moduleName
= 'Bootstrap'
52 missingChargingStationsConfiguration
= 1,
53 duplicateChargingStationTemplateUrls
= 2,
54 noChargingStationTemplates
= 3,
55 gracefulShutdownError
= 4
58 interface TemplateChargingStations
{
65 export class Bootstrap
extends EventEmitter
{
66 private static instance
: Bootstrap
| null = null
67 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
68 private readonly uiServer
: AbstractUIServer
69 private storage
?: Storage
70 private readonly templatesChargingStations
: Map
<string, TemplateChargingStations
>
71 private readonly version
: string = version
72 private initializedCounters
: boolean
73 private started
: boolean
74 private starting
: boolean
75 private stopping
: boolean
76 private uiServerStarted
: 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.uiServerStarted
= false
90 this.uiServer
= UIServerFactory
.getUIServerImplementation(
91 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
93 this.templatesChargingStations
= new Map
<string, TemplateChargingStations
>()
94 this.initializedCounters
= false
95 this.initializeCounters()
96 Configuration
.configurationChangeCallback
= async () => {
98 await Bootstrap
.getInstance().restart()
103 public static getInstance (): Bootstrap
{
104 if (Bootstrap
.instance
=== null) {
105 Bootstrap
.instance
= new Bootstrap()
107 return Bootstrap
.instance
110 public get
numberOfChargingStationTemplates (): number {
111 return this.templatesChargingStations
.size
114 public get
numberOfConfiguredChargingStations (): number {
115 return [...this.templatesChargingStations
.values()].reduce(
116 (accumulator
, value
) => accumulator
+ value
.configured
,
121 public getState (): { started
: boolean } {
123 started
: this.started
127 public getLastIndex (templateName
: string): number {
128 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129 const indexes
= [...this.templatesChargingStations
.get(templateName
)!.indexes
]
131 .sort((a
, b
) => a
- b
)
132 for (let i
= 0; i
< indexes
.length
- 1; i
++) {
133 if (indexes
[i
+ 1] - indexes
[i
] !== 1) {
137 return indexes
[indexes
.length
- 1]
140 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
141 return this.storage
?.getPerformanceStatistics()
144 private get
numberOfAddedChargingStations (): number {
145 return [...this.templatesChargingStations
.values()].reduce(
146 (accumulator
, value
) => accumulator
+ value
.added
,
151 private get
numberOfStartedChargingStations (): number {
152 return [...this.templatesChargingStations
.values()].reduce(
153 (accumulator
, value
) => accumulator
+ value
.started
,
158 public async start (): Promise
<void> {
160 if (!this.starting
) {
162 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
163 this.on(ChargingStationWorkerMessageEvents
.deleted
, this.workerEventDeleted
)
164 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
165 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
166 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
168 ChargingStationWorkerMessageEvents
.performanceStatistics
,
169 this.workerEventPerformanceStatistics
172 ChargingStationWorkerMessageEvents
.workerElementError
,
173 (eventError
: ChargingStationWorkerEventError
) => {
175 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
180 this.initializeCounters()
181 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
182 ConfigurationSection
.worker
184 this.initializeWorkerImplementation(workerConfiguration
)
185 await this.workerImplementation
?.start()
186 const performanceStorageConfiguration
=
187 Configuration
.getConfigurationSection
<StorageConfiguration
>(
188 ConfigurationSection
.performanceStorage
190 if (performanceStorageConfiguration
.enabled
=== true) {
191 this.storage
= StorageFactory
.getStorage(
192 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
193 performanceStorageConfiguration
.type!,
194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
195 performanceStorageConfiguration
.uri
!,
198 await this.storage
?.open()
201 !this.uiServerStarted
&&
202 Configuration
.getConfigurationSection
<UIServerConfiguration
>(
203 ConfigurationSection
.uiServer
206 this.uiServer
.start()
207 this.uiServerStarted
= true
209 // Start ChargingStation object instance in worker thread
210 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
211 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
214 this.templatesChargingStations
.get(parse(stationTemplateUrl
.file
).name
)?.configured
??
215 stationTemplateUrl
.numberOfStations
216 for (let index
= 1; index
<= nbStations
; index
++) {
217 await this.addChargingStation(index
, stationTemplateUrl
.file
)
222 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
230 `Charging stations simulator ${
232 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
233 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
234 }${this.workerImplementation?.size}${
235 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
236 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
237 this.workerImplementation?.maxElementsPerWorker != null
238 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
243 Configuration
.workerDynamicPoolInUse() &&
246 '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'
249 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
251 this.starting
= false
253 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
256 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
260 public async stop (): Promise
<void> {
262 if (!this.stopping
) {
264 await this.uiServer
.sendInternalRequest(
265 this.uiServer
.buildProtocolRequest(
267 ProcedureName
.STOP_CHARGING_STATION
,
268 Constants
.EMPTY_FROZEN_OBJECT
272 await this.waitChargingStationsStopped()
274 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
276 await this.workerImplementation
?.stop()
277 delete this.workerImplementation
278 this.removeAllListeners()
279 this.uiServer
.clearCaches()
280 this.initializedCounters
= false
281 await this.storage
?.close()
284 this.stopping
= false
286 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
289 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
293 private async restart (): Promise
<void> {
296 this.uiServerStarted
&&
297 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
301 this.uiServerStarted
= false
306 private async waitChargingStationsStopped (): Promise
<string> {
307 return await new Promise
<string>((resolve
, reject
) => {
308 const waitTimeout
= setTimeout(() => {
309 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
310 Constants.STOP_CHARGING_STATIONS_TIMEOUT
311 )} reached at stopping charging stations`
312 console
.warn(chalk
.yellow(timeoutMessage
))
313 reject(new Error(timeoutMessage
))
314 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
315 waitChargingStationEvents(
317 ChargingStationWorkerMessageEvents
.stopped
,
318 this.numberOfStartedChargingStations
321 resolve('Charging stations stopped')
325 clearTimeout(waitTimeout
)
330 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
334 let elementsPerWorker
: number
335 switch (workerConfiguration
.elementsPerWorker
) {
337 elementsPerWorker
= this.numberOfConfiguredChargingStations
342 this.numberOfConfiguredChargingStations
> availableParallelism()
343 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
347 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
349 dirname(fileURLToPath(import.meta
.url
)),
350 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
352 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353 workerConfiguration
.processType
!,
355 workerStartDelay
: workerConfiguration
.startDelay
,
356 elementStartDelay
: workerConfiguration
.elementStartDelay
,
357 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
358 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360 poolMinSize
: workerConfiguration
.poolMinSize
!,
363 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
364 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
370 private messageHandler (
371 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
374 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
382 case ChargingStationWorkerMessageEvents
.added
:
383 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
385 case ChargingStationWorkerMessageEvents
.deleted
:
386 this.emit(ChargingStationWorkerMessageEvents
.deleted
, msg
.data
)
388 case ChargingStationWorkerMessageEvents
.started
:
389 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
391 case ChargingStationWorkerMessageEvents
.stopped
:
392 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
394 case ChargingStationWorkerMessageEvents
.updated
:
395 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
397 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
398 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
400 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
401 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
403 case ChargingStationWorkerMessageEvents
.workerElementError
:
404 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
408 `Unknown charging station worker event: '${
410 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
415 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
423 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
424 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
426 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
427 data.stationInfo.chargingStationId
428 } (hashId: ${data.stationInfo.hashId}) added (${
429 this.numberOfAddedChargingStations
430 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
434 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
435 this.uiServer
.chargingStations
.delete(data
.stationInfo
.hashId
)
436 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
437 const templateChargingStations
= this.templatesChargingStations
.get(
438 data
.stationInfo
.templateName
440 --templateChargingStations
.added
441 templateChargingStations
.indexes
.delete(data
.stationInfo
.templateIndex
)
443 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
444 data.stationInfo.chargingStationId
445 } (hashId: ${data.stationInfo.hashId}) deleted (${
446 this.numberOfAddedChargingStations
447 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
451 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
452 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
453 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
454 ++this.templatesChargingStations
.get(data
.stationInfo
.templateName
)!.started
456 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
457 data.stationInfo.chargingStationId
458 } (hashId: ${data.stationInfo.hashId}) started (${
459 this.numberOfStartedChargingStations
460 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
464 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
465 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
466 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
467 --this.templatesChargingStations
.get(data
.stationInfo
.templateName
)!.started
469 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
470 data.stationInfo.chargingStationId
471 } (hashId: ${data.stationInfo.hashId}) stopped (${
472 this.numberOfStartedChargingStations
473 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
477 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
478 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
481 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
482 // eslint-disable-next-line @typescript-eslint/unbound-method
483 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
485 this.storage
.storePerformanceStatistics
as (
486 performanceStatistics
: Statistics
488 )(data
).catch(Constants
.EMPTY_FUNCTION
)
490 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
496 private initializeCounters (): void {
497 if (!this.initializedCounters
) {
498 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
499 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
500 if (isNotEmptyArray(stationTemplateUrls
)) {
501 for (const stationTemplateUrl
of stationTemplateUrls
) {
502 const templateName
= parse(stationTemplateUrl
.file
).name
503 this.templatesChargingStations
.set(templateName
, {
504 configured
: stationTemplateUrl
.numberOfStations
,
507 indexes
: new Set
<number>()
509 this.uiServer
.chargingStationTemplates
.add(templateName
)
511 if (this.templatesChargingStations
.size
!== stationTemplateUrls
.length
) {
514 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
517 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
521 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
523 exit(exitCodes
.missingChargingStationsConfiguration
)
526 this.numberOfConfiguredChargingStations
=== 0 &&
527 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
532 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
535 exit(exitCodes
.noChargingStationTemplates
)
537 this.initializedCounters
= true
541 public async addChargingStation (
543 stationTemplateFile
: string,
544 options
?: ChargingStationOptions
546 await this.workerImplementation
?.addElement({
549 dirname(fileURLToPath(import.meta
.url
)),
556 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
557 const templateChargingStations
= this.templatesChargingStations
.get(
558 parse(stationTemplateFile
).name
560 ++templateChargingStations
.added
561 templateChargingStations
.indexes
.add(index
)
564 private gracefulShutdown (): void {
567 console
.info(chalk
.green('Graceful shutdown'))
569 this.uiServerStarted
= false
570 this.waitChargingStationsStopped()
572 exit(exitCodes
.succeeded
)
575 exit(exitCodes
.gracefulShutdownError
)
579 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
580 exit(exitCodes
.gracefulShutdownError
)
584 private readonly logPrefix
= (): string => {
585 return logPrefix(' Bootstrap |')