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
304 switch (workerConfiguration
.elementsPerWorker
) {
306 elementsPerWorker
= this.numberOfConfiguredChargingStations
311 this.numberOfConfiguredChargingStations
> availableParallelism()
312 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
316 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
318 dirname(fileURLToPath(import.meta
.url
)),
319 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
321 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
322 workerConfiguration
.processType
!,
324 workerStartDelay
: workerConfiguration
.startDelay
,
325 elementStartDelay
: workerConfiguration
.elementStartDelay
,
326 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
327 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
328 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
329 poolMinSize
: workerConfiguration
.poolMinSize
!,
332 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
333 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
339 private messageHandler (
340 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
343 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
351 case ChargingStationWorkerMessageEvents
.added
:
352 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
354 case ChargingStationWorkerMessageEvents
.started
:
355 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
357 case ChargingStationWorkerMessageEvents
.stopped
:
358 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
360 case ChargingStationWorkerMessageEvents
.updated
:
361 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
363 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
364 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
366 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
367 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
369 case ChargingStationWorkerMessageEvents
.workerElementError
:
370 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
374 `Unknown charging station worker event: '${
376 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
381 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
389 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
390 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
391 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
394 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
395 data.stationInfo.chargingStationId
396 } (hashId: ${data.stationInfo.hashId}) added (${
397 this.numberOfAddedChargingStations
398 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
402 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
403 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
404 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
405 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
407 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
408 data.stationInfo.chargingStationId
409 } (hashId: ${data.stationInfo.hashId}) started (${
410 this.numberOfStartedChargingStations
411 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
415 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
416 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
417 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
418 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
420 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
421 data.stationInfo.chargingStationId
422 } (hashId: ${data.stationInfo.hashId}) stopped (${
423 this.numberOfStartedChargingStations
424 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
428 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
429 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
432 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
433 // eslint-disable-next-line @typescript-eslint/unbound-method
434 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
436 this.storage
.storePerformanceStatistics
as (
437 performanceStatistics
: Statistics
439 )(data
).catch(Constants
.EMPTY_FUNCTION
)
441 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
447 private initializeCounters (): void {
448 if (!this.initializedCounters
) {
449 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
450 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
451 if (isNotEmptyArray(stationTemplateUrls
)) {
452 for (const stationTemplateUrl
of stationTemplateUrls
) {
453 const templateName
= parse(stationTemplateUrl
.file
).name
454 this.chargingStationsByTemplate
.set(templateName
, {
455 configured
: stationTemplateUrl
.numberOfStations
,
460 this.uiServer
?.chargingStationTemplates
.add(templateName
)
462 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
465 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
468 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
472 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
474 exit(exitCodes
.missingChargingStationsConfiguration
)
477 this.numberOfConfiguredChargingStations
=== 0 &&
478 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
483 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
486 exit(exitCodes
.noChargingStationTemplates
)
488 this.initializedCounters
= true
492 public async addChargingStation (
494 stationTemplateFile
: string,
495 options
?: ChargingStationOptions
497 await this.workerImplementation
?.addElement({
500 dirname(fileURLToPath(import.meta
.url
)),
507 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
508 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
510 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
514 private gracefulShutdown (): void {
517 console
.info(chalk
.green('Graceful shutdown'))
518 this.uiServer
?.stop()
519 this.waitChargingStationsStopped()
521 exit(exitCodes
.succeeded
)
524 exit(exitCodes
.gracefulShutdownError
)
528 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
529 exit(exitCodes
.gracefulShutdownError
)
533 private readonly logPrefix
= (): string => {
534 return logPrefix(' Bootstrap |')