1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import type { Worker
} from
'node:worker_threads'
5 import chalk from
'chalk'
6 import { EventEmitter
} from
'node:events'
7 import { dirname
, extname
, join
} from
'node:path'
8 import process
, { exit
} from
'node:process'
9 import { fileURLToPath
} from
'node:url'
10 import { isMainThread
} from
'node:worker_threads'
11 import { availableParallelism
, type MessageHandler
} from
'poolifier'
13 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer.js'
15 import { version
} from
'../../package.json'
16 import { BaseError
} from
'../exception/index.js'
17 import { type Storage
, StorageFactory
} from
'../performance/index.js'
19 type ChargingStationData
,
20 type ChargingStationInfo
,
21 type ChargingStationOptions
,
22 type ChargingStationWorkerData
,
23 type ChargingStationWorkerMessage
,
24 type ChargingStationWorkerMessageData
,
25 ChargingStationWorkerMessageEvents
,
30 type StorageConfiguration
,
31 type TemplateStatistics
,
32 type UIServerConfiguration
,
33 type WorkerConfiguration
,
34 } from
'../types/index.js'
38 formatDurationMilliSeconds
,
40 handleUncaughtException
,
41 handleUnhandledRejection
,
46 } from
'../utils/index.js'
47 import { DEFAULT_ELEMENTS_PER_WORKER
, type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
48 import { buildTemplateName
, waitChargingStationEvents
} from
'./Helpers.js'
49 import { UIServerFactory
} from
'./ui-server/UIServerFactory.js'
51 const moduleName
= 'Bootstrap'
53 /* eslint-disable perfectionist/sort-enums */
56 missingChargingStationsConfiguration
= 1,
57 duplicateChargingStationTemplateUrls
= 2,
58 noChargingStationTemplates
= 3,
59 gracefulShutdownError
= 4,
61 /* eslint-enable perfectionist/sort-enums */
63 export class Bootstrap
extends EventEmitter
{
64 private static instance
: Bootstrap
| null = null
65 public get
numberOfChargingStationTemplates (): number {
66 return this.templateStatistics
.size
69 public get
numberOfConfiguredChargingStations (): number {
70 return [...this.templateStatistics
.values()].reduce(
71 (accumulator
, value
) => accumulator
+ value
.configured
,
76 public get
numberOfProvisionedChargingStations (): number {
77 return [...this.templateStatistics
.values()].reduce(
78 (accumulator
, value
) => accumulator
+ value
.provisioned
,
83 private started
: boolean
84 private starting
: boolean
85 private stopping
: boolean
86 private storage
?: Storage
87 private readonly templateStatistics
: Map
<string, TemplateStatistics
>
88 private readonly uiServer
: AbstractUIServer
89 private uiServerStarted
: boolean
90 private readonly version
: string = version
91 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
, ChargingStationInfo
>
93 private get
numberOfAddedChargingStations (): number {
94 return [...this.templateStatistics
.values()].reduce(
95 (accumulator
, value
) => accumulator
+ value
.added
,
100 private get
numberOfStartedChargingStations (): number {
101 return [...this.templateStatistics
.values()].reduce(
102 (accumulator
, value
) => accumulator
+ value
.started
,
107 private constructor () {
109 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
110 process
.on(signal
, this.gracefulShutdown
.bind(this))
112 // Enable unconditionally for now
113 handleUnhandledRejection()
114 handleUncaughtException()
116 this.starting
= false
117 this.stopping
= false
118 this.uiServerStarted
= false
119 this.templateStatistics
= new Map
<string, TemplateStatistics
>()
120 this.uiServer
= UIServerFactory
.getUIServerImplementation(
121 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
123 this.initializeCounters()
124 this.initializeWorkerImplementation(
125 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
127 Configuration
.configurationChangeCallback
= async () => {
129 await Bootstrap
.getInstance().restart()
134 public static getInstance (): Bootstrap
{
135 Bootstrap
.instance
??= new Bootstrap()
136 return Bootstrap
.instance
139 public async addChargingStation (
141 templateFile
: string,
142 options
?: ChargingStationOptions
143 ): Promise
<ChargingStationInfo
| undefined> {
144 if (!this.started
&& !this.starting
) {
146 'Cannot add charging station while the charging stations simulator is not started'
149 const stationInfo
= await this.workerImplementation
?.addElement({
153 dirname(fileURLToPath(import.meta
.url
)),
159 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
160 const templateStatistics
= this.templateStatistics
.get(buildTemplateName(templateFile
))!
161 ++templateStatistics
.added
162 templateStatistics
.indexes
.add(index
)
166 public getLastIndex (templateName
: string): number {
167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168 const indexes
= [...this.templateStatistics
.get(templateName
)!.indexes
]
170 .sort((a
, b
) => a
- b
)
171 for (let i
= 0; i
< indexes
.length
- 1; i
++) {
172 if (indexes
[i
+ 1] - indexes
[i
] !== 1) {
176 return indexes
[indexes
.length
- 1]
179 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
180 return this.storage
?.getPerformanceStatistics()
183 public getState (): SimulatorState
{
185 configuration
: Configuration
.getConfigurationData(),
186 started
: this.started
,
187 templateStatistics
: this.templateStatistics
,
188 version
: this.version
,
192 public async start (): Promise
<void> {
194 if (!this.starting
) {
196 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
197 this.on(ChargingStationWorkerMessageEvents
.deleted
, this.workerEventDeleted
)
198 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
199 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
200 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
202 ChargingStationWorkerMessageEvents
.performanceStatistics
,
203 this.workerEventPerformanceStatistics
205 // eslint-disable-next-line @typescript-eslint/unbound-method
206 if (isAsyncFunction(this.workerImplementation
?.start
)) {
207 await this.workerImplementation
.start()
209 ;(this.workerImplementation
?.start
as () => void)()
211 const performanceStorageConfiguration
=
212 Configuration
.getConfigurationSection
<StorageConfiguration
>(
213 ConfigurationSection
.performanceStorage
215 if (performanceStorageConfiguration
.enabled
=== true) {
216 this.storage
= StorageFactory
.getStorage(
217 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 performanceStorageConfiguration
.type!,
219 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
220 performanceStorageConfiguration
.uri
!,
223 await this.storage
?.open()
226 !this.uiServerStarted
&&
227 Configuration
.getConfigurationSection
<UIServerConfiguration
>(
228 ConfigurationSection
.uiServer
231 this.uiServer
.start()
232 this.uiServerStarted
= true
234 // Start ChargingStation object instance in worker thread
235 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
236 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
238 const nbStations
= stationTemplateUrl
.numberOfStations
239 for (let index
= 1; index
<= nbStations
; index
++) {
240 await this.addChargingStation(index
, stationTemplateUrl
.file
)
245 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
251 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
252 ConfigurationSection
.worker
256 `Charging stations simulator ${this.version} started with ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s) from ${this.numberOfChargingStationTemplates.toString()} charging station template(s) and ${
257 Configuration.workerDynamicPoolInUse()
258 ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
259 `${workerConfiguration.poolMinSize?.toString()}
/`
261 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
262 }${this.workerImplementation?.size.toString()}${
263 Configuration.workerPoolInUse()
264 ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
265 `/${workerConfiguration.poolMaxSize?.toString()}
`
267 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
268 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
269 this.workerImplementation?.maxElementsPerWorker != null
270 ? ` (${this.workerImplementation.maxElementsPerWorker.toString()} charging
station(s
) per worker
)`
275 Configuration
.workerDynamicPoolInUse() &&
278 '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'
281 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
283 this.starting
= false
285 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
288 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
292 public async stop (): Promise
<void> {
294 if (!this.stopping
) {
296 await this.uiServer
.sendInternalRequest(
297 this.uiServer
.buildProtocolRequest(
299 ProcedureName
.STOP_CHARGING_STATION
,
300 Constants
.EMPTY_FROZEN_OBJECT
304 await this.waitChargingStationsStopped()
306 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
308 await this.workerImplementation
?.stop()
309 this.removeAllListeners()
310 this.uiServer
.clearCaches()
311 await this.storage
?.close()
314 this.stopping
= false
316 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
319 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
323 private gracefulShutdown (): void {
326 console
.info(chalk
.green('Graceful shutdown'))
328 this.uiServerStarted
= false
329 this.waitChargingStationsStopped()
330 // eslint-disable-next-line promise/no-nesting
332 return exit(exitCodes
.succeeded
)
334 // eslint-disable-next-line promise/no-nesting
336 exit(exitCodes
.gracefulShutdownError
)
340 .catch((error
: unknown
) => {
341 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
342 exit(exitCodes
.gracefulShutdownError
)
346 private initializeCounters (): void {
347 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
348 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
349 if (isNotEmptyArray(stationTemplateUrls
)) {
350 for (const stationTemplateUrl
of stationTemplateUrls
) {
351 const templateName
= buildTemplateName(stationTemplateUrl
.file
)
352 this.templateStatistics
.set(templateName
, {
354 configured
: stationTemplateUrl
.numberOfStations
,
355 indexes
: new Set
<number>(),
356 provisioned
: stationTemplateUrl
.provisionedNumberOfStations
?? 0,
359 this.uiServer
.chargingStationTemplates
.add(templateName
)
361 if (this.templateStatistics
.size
!== stationTemplateUrls
.length
) {
364 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
367 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
371 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
373 exit(exitCodes
.missingChargingStationsConfiguration
)
376 this.numberOfConfiguredChargingStations
=== 0 &&
377 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
382 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
385 exit(exitCodes
.noChargingStationTemplates
)
389 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
393 let elementsPerWorker
: number
394 switch (workerConfiguration
.elementsPerWorker
) {
397 this.numberOfConfiguredChargingStations
+ this.numberOfProvisionedChargingStations
401 this.numberOfConfiguredChargingStations
+ this.numberOfProvisionedChargingStations
>
402 availableParallelism()
404 (this.numberOfConfiguredChargingStations
+
405 this.numberOfProvisionedChargingStations
) /
406 (availableParallelism() * 1.5)
411 elementsPerWorker
= workerConfiguration
.elementsPerWorker
?? DEFAULT_ELEMENTS_PER_WORKER
413 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<
414 ChargingStationWorkerData
,
418 dirname(fileURLToPath(import.meta
.url
)),
419 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422 workerConfiguration
.processType
!,
424 elementAddDelay
: workerConfiguration
.elementAddDelay
,
426 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
427 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
428 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
429 poolMinSize
: workerConfiguration
.poolMinSize
!,
431 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
432 ...(workerConfiguration
.resourceLimits
!= null && {
434 resourceLimits
: workerConfiguration
.resourceLimits
,
438 workerStartDelay
: workerConfiguration
.startDelay
,
443 private readonly logPrefix
= (): string => {
444 return logPrefix(' Bootstrap |')
447 private messageHandler (
448 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
451 // `${this.logPrefix()} ${moduleName}.messageHandler: Charging station worker message received: ${JSON.stringify(
457 // Skip worker message events processing
458 // eslint-disable-next-line @typescript-eslint/dot-notation
459 if (msg
['uuid'] != null) {
462 const { data
, event
} = msg
465 case ChargingStationWorkerMessageEvents
.added
:
466 this.emit(ChargingStationWorkerMessageEvents
.added
, data
)
468 case ChargingStationWorkerMessageEvents
.deleted
:
469 this.emit(ChargingStationWorkerMessageEvents
.deleted
, data
)
471 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
472 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, data
)
474 case ChargingStationWorkerMessageEvents
.started
:
475 this.emit(ChargingStationWorkerMessageEvents
.started
, data
)
477 case ChargingStationWorkerMessageEvents
.stopped
:
478 this.emit(ChargingStationWorkerMessageEvents
.stopped
, data
)
480 case ChargingStationWorkerMessageEvents
.updated
:
481 this.emit(ChargingStationWorkerMessageEvents
.updated
, data
)
485 `Unknown charging station worker message event: '${event}' received with data: ${JSON.stringify(
494 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling charging station worker message event '${event}':`,
500 private async restart (): Promise
<void> {
503 this.uiServerStarted
&&
504 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
508 this.uiServerStarted
= false
510 this.initializeCounters()
511 // FIXME: initialize worker implementation only if the worker section has changed
512 this.initializeWorkerImplementation(
513 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
518 private async waitChargingStationsStopped (): Promise
<string> {
519 return await new Promise
<string>((resolve
, reject
: (reason
?: unknown
) => void) => {
520 const waitTimeout
= setTimeout(() => {
521 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
522 Constants.STOP_CHARGING_STATIONS_TIMEOUT
523 )} reached at stopping charging stations`
524 console
.warn(chalk
.yellow(timeoutMessage
))
525 reject(new Error(timeoutMessage
))
526 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
527 waitChargingStationEvents(
529 ChargingStationWorkerMessageEvents
.stopped
,
530 this.numberOfStartedChargingStations
533 resolve('Charging stations stopped')
537 clearTimeout(waitTimeout
)
543 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
544 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
546 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
547 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
548 data.stationInfo.chargingStationId
549 } (hashId: ${data.stationInfo.hashId}) added (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))`
553 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
554 this.uiServer
.chargingStations
.delete(data
.stationInfo
.hashId
)
555 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
556 const templateStatistics
= this.templateStatistics
.get(data
.stationInfo
.templateName
)!
557 --templateStatistics
.added
558 templateStatistics
.indexes
.delete(data
.stationInfo
.templateIndex
)
560 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
561 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
562 data.stationInfo.chargingStationId
563 } (hashId: ${data.stationInfo.hashId}) deleted (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))`
567 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
568 // eslint-disable-next-line @typescript-eslint/unbound-method
569 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
571 this.storage
.storePerformanceStatistics
as (
572 performanceStatistics
: Statistics
574 )(data
).catch(Constants
.EMPTY_FUNCTION
)
576 ;(this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
582 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
583 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
584 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
585 ++this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
587 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
588 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
589 data.stationInfo.chargingStationId
590 } (hashId: ${data.stationInfo.hashId}) started (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))`
594 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
595 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
596 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
597 --this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
599 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
600 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
601 data.stationInfo.chargingStationId
602 } (hashId: ${data.stationInfo.hashId}) stopped (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))`
606 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
607 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)