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
.started
, this.workerEventStarted
)
148 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
149 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
151 ChargingStationWorkerMessageEvents
.performanceStatistics
,
152 this.workerEventPerformanceStatistics
155 ChargingStationWorkerMessageEvents
.workerElementError
,
156 (eventError
: ChargingStationWorkerEventError
) => {
158 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
163 this.initializeCounters()
164 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
165 ConfigurationSection
.worker
167 this.initializeWorkerImplementation(workerConfiguration
)
168 await this.workerImplementation
?.start()
169 const performanceStorageConfiguration
=
170 Configuration
.getConfigurationSection
<StorageConfiguration
>(
171 ConfigurationSection
.performanceStorage
173 if (performanceStorageConfiguration
.enabled
=== true) {
174 this.storage
= StorageFactory
.getStorage(
175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
176 performanceStorageConfiguration
.type!,
177 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
178 performanceStorageConfiguration
.uri
!,
181 await this.storage
?.open()
183 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
184 .enabled
=== true && this.uiServer
?.start()
185 // Start ChargingStation object instance in worker thread
186 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
187 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
190 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
191 ?.configured
?? stationTemplateUrl
.numberOfStations
192 for (let index
= 1; index
<= nbStations
; index
++) {
193 await this.addChargingStation(index
, stationTemplateUrl
.file
)
198 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
206 `Charging stations simulator ${
208 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
209 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
210 }${this.workerImplementation?.size}${
211 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
212 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
213 this.workerImplementation?.maxElementsPerWorker != null
214 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
219 Configuration
.workerDynamicPoolInUse() &&
222 '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'
225 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
227 this.starting
= false
229 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
232 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
236 public async stop (): Promise
<void> {
238 if (!this.stopping
) {
240 await this.uiServer
?.sendInternalRequest(
241 this.uiServer
.buildProtocolRequest(
243 ProcedureName
.STOP_CHARGING_STATION
,
244 Constants
.EMPTY_FROZEN_OBJECT
248 await this.waitChargingStationsStopped()
250 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
252 await this.workerImplementation
?.stop()
253 delete this.workerImplementation
254 this.removeAllListeners()
255 await this.storage
?.close()
258 this.stopping
= false
260 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
263 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
267 private async restart (): Promise
<void> {
269 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
270 .enabled
!== true && this.uiServer
?.stop()
271 this.initializedCounters
= false
275 private async waitChargingStationsStopped (): Promise
<string> {
276 return await new Promise
<string>((resolve
, reject
) => {
277 const waitTimeout
= setTimeout(() => {
278 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
279 Constants.STOP_CHARGING_STATIONS_TIMEOUT
280 )} reached at stopping charging stations`
281 console
.warn(chalk
.yellow(timeoutMessage
))
282 reject(new Error(timeoutMessage
))
283 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
284 waitChargingStationEvents(
286 ChargingStationWorkerMessageEvents
.stopped
,
287 this.numberOfStartedChargingStations
290 resolve('Charging stations stopped')
294 clearTimeout(waitTimeout
)
299 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
303 let elementsPerWorker
: number | undefined
304 switch (workerConfiguration
.elementsPerWorker
) {
307 this.numberOfConfiguredChargingStations
> availableParallelism()
308 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
312 elementsPerWorker
= this.numberOfConfiguredChargingStations
315 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
317 dirname(fileURLToPath(import.meta
.url
)),
318 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
320 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
321 workerConfiguration
.processType
!,
323 workerStartDelay
: workerConfiguration
.startDelay
,
324 elementStartDelay
: workerConfiguration
.elementStartDelay
,
325 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
326 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
328 poolMinSize
: workerConfiguration
.poolMinSize
!,
329 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
331 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
332 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
338 private messageHandler (
339 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
342 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
350 case ChargingStationWorkerMessageEvents
.added
:
351 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
353 case ChargingStationWorkerMessageEvents
.started
:
354 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
356 case ChargingStationWorkerMessageEvents
.stopped
:
357 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
359 case ChargingStationWorkerMessageEvents
.updated
:
360 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
362 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
363 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
365 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
366 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
368 case ChargingStationWorkerMessageEvents
.workerElementError
:
369 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
373 `Unknown charging station worker event: '${
375 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
380 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
388 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
389 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
390 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
391 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
393 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
394 data.stationInfo.chargingStationId
395 } (hashId: ${data.stationInfo.hashId}) added (${
396 this.numberOfAddedChargingStations
397 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
401 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
402 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
406 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
407 data.stationInfo.chargingStationId
408 } (hashId: ${data.stationInfo.hashId}) started (${
409 this.numberOfStartedChargingStations
410 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
414 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
415 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
416 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
417 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
419 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
420 data.stationInfo.chargingStationId
421 } (hashId: ${data.stationInfo.hashId}) stopped (${
422 this.numberOfStartedChargingStations
423 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
427 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
428 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
431 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
432 // eslint-disable-next-line @typescript-eslint/unbound-method
433 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
435 this.storage
.storePerformanceStatistics
as (
436 performanceStatistics
: Statistics
438 )(data
).catch(Constants
.EMPTY_FUNCTION
)
440 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
446 private initializeCounters (): void {
447 if (!this.initializedCounters
) {
448 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
449 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
450 if (isNotEmptyArray(stationTemplateUrls
)) {
451 for (const stationTemplateUrl
of stationTemplateUrls
) {
452 const templateName
= parse(stationTemplateUrl
.file
).name
453 this.chargingStationsByTemplate
.set(templateName
, {
454 configured
: stationTemplateUrl
.numberOfStations
,
459 this.uiServer
?.chargingStationTemplates
.add(templateName
)
461 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
464 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
467 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
471 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
473 exit(exitCodes
.missingChargingStationsConfiguration
)
476 this.numberOfConfiguredChargingStations
=== 0 &&
477 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
482 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
485 exit(exitCodes
.noChargingStationTemplates
)
487 this.initializedCounters
= true
491 public async addChargingStation (
493 stationTemplateFile
: string,
494 options
?: ChargingStationOptions
496 await this.workerImplementation
?.addElement({
499 dirname(fileURLToPath(import.meta
.url
)),
506 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
507 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
509 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
513 private gracefulShutdown (): void {
516 console
.info(chalk
.green('Graceful shutdown'))
517 this.uiServer
?.stop()
518 this.waitChargingStationsStopped()
520 exit(exitCodes
.succeeded
)
523 exit(exitCodes
.gracefulShutdownError
)
527 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
528 exit(exitCodes
.gracefulShutdownError
)
532 private readonly logPrefix
= (): string => {
533 return logPrefix(' Bootstrap |')