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 ChargingStationWorkerData
,
22 type ChargingStationWorkerEventError
,
23 type ChargingStationWorkerMessage
,
24 type ChargingStationWorkerMessageData
,
25 ChargingStationWorkerMessageEvents
,
29 type StorageConfiguration
,
30 type UIServerConfiguration
,
31 type WorkerConfiguration
32 } from
'../types/index.js'
36 formatDurationMilliSeconds
,
38 handleUncaughtException
,
39 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 return this.chargingStationsByTemplate
.get(templateName
)?.lastIndex
?? 0
123 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
124 return this.storage
?.getPerformanceStatistics()
127 private get
numberOfAddedChargingStations (): number {
128 return [...this.chargingStationsByTemplate
.values()].reduce(
129 (accumulator
, value
) => accumulator
+ value
.added
,
134 private get
numberOfStartedChargingStations (): number {
135 return [...this.chargingStationsByTemplate
.values()].reduce(
136 (accumulator
, value
) => accumulator
+ value
.started
,
141 public async start (): Promise
<void> {
143 if (!this.starting
) {
145 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
146 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
147 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
148 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
150 ChargingStationWorkerMessageEvents
.performanceStatistics
,
151 this.workerEventPerformanceStatistics
154 ChargingStationWorkerMessageEvents
.workerElementError
,
155 (eventError
: ChargingStationWorkerEventError
) => {
157 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
162 this.initializeCounters()
163 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
164 ConfigurationSection
.worker
166 this.initializeWorkerImplementation(workerConfiguration
)
167 await this.workerImplementation
?.start()
168 const performanceStorageConfiguration
=
169 Configuration
.getConfigurationSection
<StorageConfiguration
>(
170 ConfigurationSection
.performanceStorage
172 if (performanceStorageConfiguration
.enabled
=== true) {
173 this.storage
= StorageFactory
.getStorage(
174 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
175 performanceStorageConfiguration
.type!,
176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177 performanceStorageConfiguration
.uri
!,
180 await this.storage
?.open()
182 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
183 .enabled
=== true && this.uiServer
?.start()
184 // Start ChargingStation object instance in worker thread
185 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
186 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
189 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
190 ?.configured
?? stationTemplateUrl
.numberOfStations
191 for (let index
= 1; index
<= nbStations
; index
++) {
192 await this.addChargingStation(index
, stationTemplateUrl
.file
)
197 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
205 `Charging stations simulator ${
207 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
208 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
209 }${this.workerImplementation?.size}${
210 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
211 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
212 this.workerImplementation?.maxElementsPerWorker != null
213 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
218 Configuration
.workerDynamicPoolInUse() &&
221 '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'
224 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
226 this.starting
= false
228 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
231 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
235 public async stop (): Promise
<void> {
237 if (!this.stopping
) {
239 await this.uiServer
?.sendInternalRequest(
240 this.uiServer
.buildProtocolRequest(
242 ProcedureName
.STOP_CHARGING_STATION
,
243 Constants
.EMPTY_FROZEN_OBJECT
247 await this.waitChargingStationsStopped()
249 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
251 await this.workerImplementation
?.stop()
252 delete this.workerImplementation
253 this.removeAllListeners()
254 await this.storage
?.close()
257 this.stopping
= false
259 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
262 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
266 private async restart (): Promise
<void> {
268 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
269 .enabled
!== true && this.uiServer
?.stop()
270 this.initializedCounters
= false
274 private async waitChargingStationsStopped (): Promise
<string> {
275 return await new Promise
<string>((resolve
, reject
) => {
276 const waitTimeout
= setTimeout(() => {
277 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
278 Constants.STOP_CHARGING_STATIONS_TIMEOUT
279 )} reached at stopping charging stations`
280 console
.warn(chalk
.yellow(timeoutMessage
))
281 reject(new Error(timeoutMessage
))
282 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
283 waitChargingStationEvents(
285 ChargingStationWorkerMessageEvents
.stopped
,
286 this.numberOfStartedChargingStations
289 resolve('Charging stations stopped')
293 clearTimeout(waitTimeout
)
298 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
302 let elementsPerWorker
: number | undefined
303 switch (workerConfiguration
.elementsPerWorker
) {
306 this.numberOfConfiguredChargingStations
> availableParallelism()
307 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
311 elementsPerWorker
= this.numberOfConfiguredChargingStations
314 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
316 dirname(fileURLToPath(import.meta
.url
)),
317 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
319 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
320 workerConfiguration
.processType
!,
322 workerStartDelay
: workerConfiguration
.startDelay
,
323 elementStartDelay
: workerConfiguration
.elementStartDelay
,
324 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
325 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
326 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
327 poolMinSize
: workerConfiguration
.poolMinSize
!,
328 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
330 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
331 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
337 private messageHandler (
338 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
341 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
349 case ChargingStationWorkerMessageEvents
.added
:
350 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
352 case ChargingStationWorkerMessageEvents
.started
:
353 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
355 case ChargingStationWorkerMessageEvents
.stopped
:
356 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
358 case ChargingStationWorkerMessageEvents
.updated
:
359 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
361 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
362 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
364 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
365 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
367 case ChargingStationWorkerMessageEvents
.workerElementError
:
368 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
372 `Unknown charging station worker event: '${
374 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
379 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
387 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
388 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
389 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
390 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
392 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
393 data.stationInfo.chargingStationId
394 } (hashId: ${data.stationInfo.hashId}) added (${
395 this.numberOfAddedChargingStations
396 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
400 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
401 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
402 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
403 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
405 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
406 data.stationInfo.chargingStationId
407 } (hashId: ${data.stationInfo.hashId}) started (${
408 this.numberOfStartedChargingStations
409 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
413 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
414 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
415 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
418 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
419 data.stationInfo.chargingStationId
420 } (hashId: ${data.stationInfo.hashId}) stopped (${
421 this.numberOfStartedChargingStations
422 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
426 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
427 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
430 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
431 // eslint-disable-next-line @typescript-eslint/unbound-method
432 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
434 this.storage
.storePerformanceStatistics
as (
435 performanceStatistics
: Statistics
437 )(data
).catch(Constants
.EMPTY_FUNCTION
)
439 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
445 private initializeCounters (): void {
446 if (!this.initializedCounters
) {
447 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
448 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
449 if (isNotEmptyArray(stationTemplateUrls
)) {
450 for (const stationTemplateUrl
of stationTemplateUrls
) {
451 const templateName
= parse(stationTemplateUrl
.file
).name
452 this.chargingStationsByTemplate
.set(templateName
, {
453 configured
: stationTemplateUrl
.numberOfStations
,
458 this.uiServer
?.chargingStationTemplates
.add(templateName
)
460 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
463 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
466 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
470 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
472 exit(exitCodes
.missingChargingStationsConfiguration
)
475 this.numberOfConfiguredChargingStations
=== 0 &&
476 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
481 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
484 exit(exitCodes
.noChargingStationTemplates
)
486 this.initializedCounters
= true
490 public async addChargingStation (index
: number, stationTemplateFile
: string): Promise
<void> {
491 await this.workerImplementation
?.addElement({
494 dirname(fileURLToPath(import.meta
.url
)),
500 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
501 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
503 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
507 private gracefulShutdown (): void {
510 console
.info(chalk
.green('Graceful shutdown'))
511 this.uiServer
?.stop()
512 this.waitChargingStationsStopped()
514 exit(exitCodes
.succeeded
)
517 exit(exitCodes
.gracefulShutdownError
)
521 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
522 exit(exitCodes
.gracefulShutdownError
)
526 private readonly logPrefix
= (): string => {
527 return logPrefix(' Bootstrap |')