1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { EventEmitter
} from
'node:events'
4 import { dirname
, extname
, join
} 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
,
28 type InternalTemplateStatistics
,
32 type StorageConfiguration
,
33 type UIServerConfiguration
,
34 type WorkerConfiguration
35 } from
'../types/index.js'
40 buildTemplateStatisticsPayload
,
41 formatDurationMilliSeconds
,
43 handleUncaughtException
,
44 handleUnhandledRejection
,
49 } from
'../utils/index.js'
50 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
52 const moduleName
= 'Bootstrap'
56 missingChargingStationsConfiguration
= 1,
57 duplicateChargingStationTemplateUrls
= 2,
58 noChargingStationTemplates
= 3,
59 gracefulShutdownError
= 4
62 export class Bootstrap
extends EventEmitter
{
63 private static instance
: Bootstrap
| null = null
64 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
65 private readonly uiServer
: AbstractUIServer
66 private storage
?: Storage
67 private readonly templateStatistics
: Map
<string, InternalTemplateStatistics
>
68 private readonly version
: string = version
69 private initializedCounters
: boolean
70 private started
: boolean
71 private starting
: boolean
72 private stopping
: boolean
73 private uiServerStarted
: boolean
75 private constructor () {
77 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
78 process
.on(signal
, this.gracefulShutdown
.bind(this))
80 // Enable unconditionally for now
81 handleUnhandledRejection()
82 handleUncaughtException()
86 this.uiServerStarted
= false
87 this.uiServer
= UIServerFactory
.getUIServerImplementation(
88 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
90 this.templateStatistics
= new Map
<string, InternalTemplateStatistics
>()
91 this.initializedCounters
= false
92 this.initializeCounters()
93 Configuration
.configurationChangeCallback
= async () => {
95 await Bootstrap
.getInstance().restart()
100 public static getInstance (): Bootstrap
{
101 if (Bootstrap
.instance
=== null) {
102 Bootstrap
.instance
= new Bootstrap()
104 return Bootstrap
.instance
107 public get
numberOfChargingStationTemplates (): number {
108 return this.templateStatistics
.size
111 public get
numberOfConfiguredChargingStations (): number {
112 return [...this.templateStatistics
.values()].reduce(
113 (accumulator
, value
) => accumulator
+ value
.configured
,
118 public getState (): SimulatorState
{
120 version
: this.version
,
121 started
: this.started
,
122 templateStatistics
: buildTemplateStatisticsPayload(this.templateStatistics
)
126 public getLastIndex (templateName
: string): number {
127 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
128 const indexes
= [...this.templateStatistics
.get(templateName
)!.indexes
]
130 .sort((a
, b
) => a
- b
)
131 for (let i
= 0; i
< indexes
.length
- 1; i
++) {
132 if (indexes
[i
+ 1] - indexes
[i
] !== 1) {
136 return indexes
[indexes
.length
- 1]
139 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
140 return this.storage
?.getPerformanceStatistics()
143 private get
numberOfAddedChargingStations (): number {
144 return [...this.templateStatistics
.values()].reduce(
145 (accumulator
, value
) => accumulator
+ value
.added
,
150 private get
numberOfStartedChargingStations (): number {
151 return [...this.templateStatistics
.values()].reduce(
152 (accumulator
, value
) => accumulator
+ value
.started
,
157 public async start (): Promise
<void> {
159 if (!this.starting
) {
161 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
162 this.on(ChargingStationWorkerMessageEvents
.deleted
, this.workerEventDeleted
)
163 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
164 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
165 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
167 ChargingStationWorkerMessageEvents
.performanceStatistics
,
168 this.workerEventPerformanceStatistics
171 ChargingStationWorkerMessageEvents
.workerElementError
,
172 (eventError
: ChargingStationWorkerEventError
) => {
174 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
179 this.initializeCounters()
180 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
181 ConfigurationSection
.worker
183 this.initializeWorkerImplementation(workerConfiguration
)
184 await this.workerImplementation
?.start()
185 const performanceStorageConfiguration
=
186 Configuration
.getConfigurationSection
<StorageConfiguration
>(
187 ConfigurationSection
.performanceStorage
189 if (performanceStorageConfiguration
.enabled
=== true) {
190 this.storage
= StorageFactory
.getStorage(
191 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
192 performanceStorageConfiguration
.type!,
193 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
194 performanceStorageConfiguration
.uri
!,
197 await this.storage
?.open()
200 !this.uiServerStarted
&&
201 Configuration
.getConfigurationSection
<UIServerConfiguration
>(
202 ConfigurationSection
.uiServer
205 this.uiServer
.start()
206 this.uiServerStarted
= true
208 // Start ChargingStation object instance in worker thread
209 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
210 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
212 const nbStations
= stationTemplateUrl
.numberOfStations
213 for (let index
= 1; index
<= nbStations
; index
++) {
214 await this.addChargingStation(index
, stationTemplateUrl
.file
)
219 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
227 `Charging stations simulator ${
229 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
230 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
231 }${this.workerImplementation?.size}${
232 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
233 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
234 this.workerImplementation?.maxElementsPerWorker != null
235 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
240 Configuration
.workerDynamicPoolInUse() &&
243 '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'
246 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
248 this.starting
= false
250 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
253 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
257 public async stop (): Promise
<void> {
259 if (!this.stopping
) {
261 await this.uiServer
.sendInternalRequest(
262 this.uiServer
.buildProtocolRequest(
264 ProcedureName
.STOP_CHARGING_STATION
,
265 Constants
.EMPTY_FROZEN_OBJECT
269 await this.waitChargingStationsStopped()
271 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
273 await this.workerImplementation
?.stop()
274 delete this.workerImplementation
275 this.removeAllListeners()
276 this.uiServer
.clearCaches()
277 this.initializedCounters
= false
278 await this.storage
?.close()
281 this.stopping
= false
283 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
286 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
290 private async restart (): Promise
<void> {
293 this.uiServerStarted
&&
294 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
298 this.uiServerStarted
= false
303 private async waitChargingStationsStopped (): Promise
<string> {
304 return await new Promise
<string>((resolve
, reject
) => {
305 const waitTimeout
= setTimeout(() => {
306 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
307 Constants.STOP_CHARGING_STATIONS_TIMEOUT
308 )} reached at stopping charging stations`
309 console
.warn(chalk
.yellow(timeoutMessage
))
310 reject(new Error(timeoutMessage
))
311 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
312 waitChargingStationEvents(
314 ChargingStationWorkerMessageEvents
.stopped
,
315 this.numberOfStartedChargingStations
318 resolve('Charging stations stopped')
322 clearTimeout(waitTimeout
)
327 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
331 let elementsPerWorker
: number
332 switch (workerConfiguration
.elementsPerWorker
) {
334 elementsPerWorker
= this.numberOfConfiguredChargingStations
339 this.numberOfConfiguredChargingStations
> availableParallelism()
340 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
344 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
346 dirname(fileURLToPath(import.meta
.url
)),
347 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
349 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
350 workerConfiguration
.processType
!,
352 workerStartDelay
: workerConfiguration
.startDelay
,
353 elementStartDelay
: workerConfiguration
.elementStartDelay
,
354 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
355 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
356 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357 poolMinSize
: workerConfiguration
.poolMinSize
!,
360 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
361 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
367 private messageHandler (
368 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
371 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
379 case ChargingStationWorkerMessageEvents
.added
:
380 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
382 case ChargingStationWorkerMessageEvents
.deleted
:
383 this.emit(ChargingStationWorkerMessageEvents
.deleted
, msg
.data
)
385 case ChargingStationWorkerMessageEvents
.started
:
386 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
388 case ChargingStationWorkerMessageEvents
.stopped
:
389 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
391 case ChargingStationWorkerMessageEvents
.updated
:
392 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
394 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
395 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
397 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
398 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
400 case ChargingStationWorkerMessageEvents
.workerElementError
:
401 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
405 `Unknown charging station worker event: '${
407 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
412 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
420 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
421 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
423 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
424 data.stationInfo.chargingStationId
425 } (hashId: ${data.stationInfo.hashId}) added (${
426 this.numberOfAddedChargingStations
427 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
431 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
432 this.uiServer
.chargingStations
.delete(data
.stationInfo
.hashId
)
433 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
434 const templateStatistics
= this.templateStatistics
.get(data
.stationInfo
.templateName
)!
435 --templateStatistics
.added
436 templateStatistics
.indexes
.delete(data
.stationInfo
.templateIndex
)
438 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
439 data.stationInfo.chargingStationId
440 } (hashId: ${data.stationInfo.hashId}) deleted (${
441 this.numberOfAddedChargingStations
442 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
446 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
447 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
448 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
449 ++this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
451 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
452 data.stationInfo.chargingStationId
453 } (hashId: ${data.stationInfo.hashId}) started (${
454 this.numberOfStartedChargingStations
455 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
459 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
460 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
461 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
462 --this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
464 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
465 data.stationInfo.chargingStationId
466 } (hashId: ${data.stationInfo.hashId}) stopped (${
467 this.numberOfStartedChargingStations
468 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
472 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
473 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
476 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
477 // eslint-disable-next-line @typescript-eslint/unbound-method
478 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
480 this.storage
.storePerformanceStatistics
as (
481 performanceStatistics
: Statistics
483 )(data
).catch(Constants
.EMPTY_FUNCTION
)
485 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
491 private initializeCounters (): void {
492 if (!this.initializedCounters
) {
493 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
494 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
495 if (isNotEmptyArray(stationTemplateUrls
)) {
496 for (const stationTemplateUrl
of stationTemplateUrls
) {
497 const templateName
= buildTemplateName(stationTemplateUrl
.file
)
498 this.templateStatistics
.set(templateName
, {
499 configured
: stationTemplateUrl
.numberOfStations
,
502 indexes
: new Set
<number>()
504 this.uiServer
.chargingStationTemplates
.add(templateName
)
506 if (this.templateStatistics
.size
!== stationTemplateUrls
.length
) {
509 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
512 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
516 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
518 exit(exitCodes
.missingChargingStationsConfiguration
)
521 this.numberOfConfiguredChargingStations
=== 0 &&
522 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
527 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
530 exit(exitCodes
.noChargingStationTemplates
)
532 this.initializedCounters
= true
536 public async addChargingStation (
538 templateFile
: string,
539 options
?: ChargingStationOptions
541 await this.workerImplementation
?.addElement({
544 dirname(fileURLToPath(import.meta
.url
)),
551 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
552 const templateStatistics
= this.templateStatistics
.get(buildTemplateName(templateFile
))!
553 ++templateStatistics
.added
554 templateStatistics
.indexes
.add(index
)
557 private gracefulShutdown (): void {
560 console
.info(chalk
.green('Graceful shutdown'))
562 this.uiServerStarted
= false
563 this.waitChargingStationsStopped()
565 exit(exitCodes
.succeeded
)
568 exit(exitCodes
.gracefulShutdownError
)
572 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
573 exit(exitCodes
.gracefulShutdownError
)
577 private readonly logPrefix
= (): string => {
578 return logPrefix(' Bootstrap |')