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 chargingStationsByTemplate
: 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
77 private constructor () {
79 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
80 process
.on(signal
, this.gracefulShutdown
.bind(this))
82 // Enable unconditionally for now
83 handleUnhandledRejection()
84 handleUncaughtException()
88 this.chargingStationsByTemplate
= new Map
<string, TemplateChargingStations
>()
89 this.uiServer
= UIServerFactory
.getUIServerImplementation(
90 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
92 this.initializedCounters
= false
93 this.initializeCounters()
94 Configuration
.configurationChangeCallback
= async () => {
96 await Bootstrap
.getInstance().restart()
101 public static getInstance (): Bootstrap
{
102 if (Bootstrap
.instance
=== null) {
103 Bootstrap
.instance
= new Bootstrap()
105 return Bootstrap
.instance
108 public get
numberOfChargingStationTemplates (): number {
109 return this.chargingStationsByTemplate
.size
112 public get
numberOfConfiguredChargingStations (): number {
113 return [...this.chargingStationsByTemplate
.values()].reduce(
114 (accumulator
, value
) => accumulator
+ value
.configured
,
119 public getLastIndex (templateName
: string): number {
120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
121 const indexes
= [...this.chargingStationsByTemplate
.get(templateName
)!.indexes
]
123 .sort((a
, b
) => a
- b
)
124 for (let i
= 0; i
< indexes
.length
- 1; i
++) {
125 if (indexes
[i
+ 1] - indexes
[i
] !== 1) {
129 return indexes
[indexes
.length
- 1]
132 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
133 return this.storage
?.getPerformanceStatistics()
136 private get
numberOfAddedChargingStations (): number {
137 return [...this.chargingStationsByTemplate
.values()].reduce(
138 (accumulator
, value
) => accumulator
+ value
.added
,
143 private get
numberOfStartedChargingStations (): number {
144 return [...this.chargingStationsByTemplate
.values()].reduce(
145 (accumulator
, value
) => accumulator
+ value
.started
,
150 public async start (): Promise
<void> {
152 if (!this.starting
) {
154 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
155 this.on(ChargingStationWorkerMessageEvents
.deleted
, this.workerEventDeleted
)
156 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
157 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
158 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
160 ChargingStationWorkerMessageEvents
.performanceStatistics
,
161 this.workerEventPerformanceStatistics
164 ChargingStationWorkerMessageEvents
.workerElementError
,
165 (eventError
: ChargingStationWorkerEventError
) => {
167 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
172 this.initializeCounters()
173 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
174 ConfigurationSection
.worker
176 this.initializeWorkerImplementation(workerConfiguration
)
177 await this.workerImplementation
?.start()
178 const performanceStorageConfiguration
=
179 Configuration
.getConfigurationSection
<StorageConfiguration
>(
180 ConfigurationSection
.performanceStorage
182 if (performanceStorageConfiguration
.enabled
=== true) {
183 this.storage
= StorageFactory
.getStorage(
184 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
185 performanceStorageConfiguration
.type!,
186 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
187 performanceStorageConfiguration
.uri
!,
190 await this.storage
?.open()
192 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
193 .enabled
=== true && this.uiServer
?.start()
194 // Start ChargingStation object instance in worker thread
195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
196 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
199 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
200 ?.configured
?? stationTemplateUrl
.numberOfStations
201 for (let index
= 1; index
<= nbStations
; index
++) {
202 await this.addChargingStation(index
, stationTemplateUrl
.file
)
207 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
215 `Charging stations simulator ${
217 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
218 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
219 }${this.workerImplementation?.size}${
220 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
221 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
222 this.workerImplementation?.maxElementsPerWorker != null
223 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
228 Configuration
.workerDynamicPoolInUse() &&
231 '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'
234 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
236 this.starting
= false
238 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
241 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
245 public async stop (): Promise
<void> {
247 if (!this.stopping
) {
249 await this.uiServer
?.sendInternalRequest(
250 this.uiServer
.buildProtocolRequest(
252 ProcedureName
.STOP_CHARGING_STATION
,
253 Constants
.EMPTY_FROZEN_OBJECT
257 await this.waitChargingStationsStopped()
259 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
261 await this.workerImplementation
?.stop()
262 delete this.workerImplementation
263 this.removeAllListeners()
264 await this.storage
?.close()
267 this.stopping
= false
269 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
272 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
276 private async restart (): Promise
<void> {
278 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
279 .enabled
!== true && this.uiServer
?.stop()
280 this.initializedCounters
= false
284 private async waitChargingStationsStopped (): Promise
<string> {
285 return await new Promise
<string>((resolve
, reject
) => {
286 const waitTimeout
= setTimeout(() => {
287 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
288 Constants.STOP_CHARGING_STATIONS_TIMEOUT
289 )} reached at stopping charging stations`
290 console
.warn(chalk
.yellow(timeoutMessage
))
291 reject(new Error(timeoutMessage
))
292 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
293 waitChargingStationEvents(
295 ChargingStationWorkerMessageEvents
.stopped
,
296 this.numberOfStartedChargingStations
299 resolve('Charging stations stopped')
303 clearTimeout(waitTimeout
)
308 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
312 let elementsPerWorker
: number
313 switch (workerConfiguration
.elementsPerWorker
) {
315 elementsPerWorker
= this.numberOfConfiguredChargingStations
320 this.numberOfConfiguredChargingStations
> availableParallelism()
321 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
325 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
327 dirname(fileURLToPath(import.meta
.url
)),
328 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
330 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
331 workerConfiguration
.processType
!,
333 workerStartDelay
: workerConfiguration
.startDelay
,
334 elementStartDelay
: workerConfiguration
.elementStartDelay
,
335 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
336 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
337 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
338 poolMinSize
: workerConfiguration
.poolMinSize
!,
341 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
342 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
348 private messageHandler (
349 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
352 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
360 case ChargingStationWorkerMessageEvents
.added
:
361 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
363 case ChargingStationWorkerMessageEvents
.deleted
:
364 this.emit(ChargingStationWorkerMessageEvents
.deleted
, msg
.data
)
366 case ChargingStationWorkerMessageEvents
.started
:
367 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
369 case ChargingStationWorkerMessageEvents
.stopped
:
370 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
372 case ChargingStationWorkerMessageEvents
.updated
:
373 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
375 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
376 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
378 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
379 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
381 case ChargingStationWorkerMessageEvents
.workerElementError
:
382 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
386 `Unknown charging station worker event: '${
388 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
393 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
401 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
402 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
404 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
405 data.stationInfo.chargingStationId
406 } (hashId: ${data.stationInfo.hashId}) added (${
407 this.numberOfAddedChargingStations
408 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
412 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
413 this.uiServer
?.chargingStations
.delete(data
.stationInfo
.hashId
)
414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
416 this.chargingStationsByTemplate
417 .get(data
.stationInfo
.templateName
)
418 ?.indexes
.delete(data
.stationInfo
.templateIndex
)
420 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
421 data.stationInfo.chargingStationId
422 } (hashId: ${data.stationInfo.hashId}) deleted (${
423 this.numberOfAddedChargingStations
424 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
428 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
429 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
430 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
431 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
433 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
434 data.stationInfo.chargingStationId
435 } (hashId: ${data.stationInfo.hashId}) started (${
436 this.numberOfStartedChargingStations
437 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
441 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
442 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
446 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
447 data.stationInfo.chargingStationId
448 } (hashId: ${data.stationInfo.hashId}) stopped (${
449 this.numberOfStartedChargingStations
450 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
454 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
455 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
458 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
459 // eslint-disable-next-line @typescript-eslint/unbound-method
460 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
462 this.storage
.storePerformanceStatistics
as (
463 performanceStatistics
: Statistics
465 )(data
).catch(Constants
.EMPTY_FUNCTION
)
467 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
473 private initializeCounters (): void {
474 if (!this.initializedCounters
) {
475 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
476 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
477 if (isNotEmptyArray(stationTemplateUrls
)) {
478 for (const stationTemplateUrl
of stationTemplateUrls
) {
479 const templateName
= parse(stationTemplateUrl
.file
).name
480 this.chargingStationsByTemplate
.set(templateName
, {
481 configured
: stationTemplateUrl
.numberOfStations
,
484 indexes
: new Set
<number>()
486 this.uiServer
?.chargingStationTemplates
.add(templateName
)
488 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
491 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
494 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
498 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
500 exit(exitCodes
.missingChargingStationsConfiguration
)
503 this.numberOfConfiguredChargingStations
=== 0 &&
504 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
509 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
512 exit(exitCodes
.noChargingStationTemplates
)
514 this.initializedCounters
= true
518 public async addChargingStation (
520 stationTemplateFile
: string,
521 options
?: ChargingStationOptions
523 await this.workerImplementation
?.addElement({
526 dirname(fileURLToPath(import.meta
.url
)),
533 const templateName
= parse(stationTemplateFile
).name
534 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
535 ++this.chargingStationsByTemplate
.get(templateName
)!.added
536 this.chargingStationsByTemplate
.get(templateName
)?.indexes
.add(index
)
539 private gracefulShutdown (): void {
542 console
.info(chalk
.green('Graceful shutdown'))
543 this.uiServer
?.stop()
544 this.waitChargingStationsStopped()
546 exit(exitCodes
.succeeded
)
549 exit(exitCodes
.gracefulShutdownError
)
553 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
554 exit(exitCodes
.gracefulShutdownError
)
558 private readonly logPrefix
= (): string => {
559 return logPrefix(' Bootstrap |')