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
,
28 type InternalTemplateStatistics
,
32 type StorageConfiguration
,
33 type UIServerConfiguration
,
34 type WorkerConfiguration
35 } from
'../types/index.js'
39 buildTemplateStatisticsPayload
,
40 formatDurationMilliSeconds
,
42 handleUncaughtException
,
43 handleUnhandledRejection
,
48 } from
'../utils/index.js'
49 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
51 const moduleName
= 'Bootstrap'
55 missingChargingStationsConfiguration
= 1,
56 duplicateChargingStationTemplateUrls
= 2,
57 noChargingStationTemplates
= 3,
58 gracefulShutdownError
= 4
61 export class Bootstrap
extends EventEmitter
{
62 private static instance
: Bootstrap
| null = null
63 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
64 private readonly uiServer
: AbstractUIServer
65 private storage
?: Storage
66 private readonly templateStatistics
: Map
<string, InternalTemplateStatistics
>
67 private readonly version
: string = version
68 private initializedCounters
: boolean
69 private started
: boolean
70 private starting
: boolean
71 private stopping
: boolean
72 private uiServerStarted
: boolean
74 private constructor () {
76 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
77 process
.on(signal
, this.gracefulShutdown
.bind(this))
79 // Enable unconditionally for now
80 handleUnhandledRejection()
81 handleUncaughtException()
85 this.uiServerStarted
= false
86 this.uiServer
= UIServerFactory
.getUIServerImplementation(
87 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
89 this.templateStatistics
= new Map
<string, InternalTemplateStatistics
>()
90 this.initializedCounters
= false
91 this.initializeCounters()
92 Configuration
.configurationChangeCallback
= async () => {
94 await Bootstrap
.getInstance().restart()
99 public static getInstance (): Bootstrap
{
100 if (Bootstrap
.instance
=== null) {
101 Bootstrap
.instance
= new Bootstrap()
103 return Bootstrap
.instance
106 public get
numberOfChargingStationTemplates (): number {
107 return this.templateStatistics
.size
110 public get
numberOfConfiguredChargingStations (): number {
111 return [...this.templateStatistics
.values()].reduce(
112 (accumulator
, value
) => accumulator
+ value
.configured
,
117 public getState (): SimulatorState
{
119 version
: this.version
,
120 started
: this.started
,
121 templateStatistics
: buildTemplateStatisticsPayload(this.templateStatistics
)
125 public getLastIndex (templateName
: string): number {
126 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
127 const indexes
= [...this.templateStatistics
.get(templateName
)!.indexes
]
129 .sort((a
, b
) => a
- b
)
130 for (let i
= 0; i
< indexes
.length
- 1; i
++) {
131 if (indexes
[i
+ 1] - indexes
[i
] !== 1) {
135 return indexes
[indexes
.length
- 1]
138 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
139 return this.storage
?.getPerformanceStatistics()
142 private get
numberOfAddedChargingStations (): number {
143 return [...this.templateStatistics
.values()].reduce(
144 (accumulator
, value
) => accumulator
+ value
.added
,
149 private get
numberOfStartedChargingStations (): number {
150 return [...this.templateStatistics
.values()].reduce(
151 (accumulator
, value
) => accumulator
+ value
.started
,
156 public async start (): Promise
<void> {
158 if (!this.starting
) {
160 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
161 this.on(ChargingStationWorkerMessageEvents
.deleted
, this.workerEventDeleted
)
162 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
163 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
164 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
166 ChargingStationWorkerMessageEvents
.performanceStatistics
,
167 this.workerEventPerformanceStatistics
170 ChargingStationWorkerMessageEvents
.workerElementError
,
171 (eventError
: ChargingStationWorkerEventError
) => {
173 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
178 this.initializeCounters()
179 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
180 ConfigurationSection
.worker
182 this.initializeWorkerImplementation(workerConfiguration
)
183 await this.workerImplementation
?.start()
184 const performanceStorageConfiguration
=
185 Configuration
.getConfigurationSection
<StorageConfiguration
>(
186 ConfigurationSection
.performanceStorage
188 if (performanceStorageConfiguration
.enabled
=== true) {
189 this.storage
= StorageFactory
.getStorage(
190 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191 performanceStorageConfiguration
.type!,
192 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
193 performanceStorageConfiguration
.uri
!,
196 await this.storage
?.open()
199 !this.uiServerStarted
&&
200 Configuration
.getConfigurationSection
<UIServerConfiguration
>(
201 ConfigurationSection
.uiServer
204 this.uiServer
.start()
205 this.uiServerStarted
= true
207 // Start ChargingStation object instance in worker thread
208 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
209 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
212 this.templateStatistics
.get(parse(stationTemplateUrl
.file
).name
)?.configured
??
213 stationTemplateUrl
.numberOfStations
214 for (let index
= 1; index
<= nbStations
; index
++) {
215 await this.addChargingStation(index
, stationTemplateUrl
.file
)
220 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
228 `Charging stations simulator ${
230 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
231 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
232 }${this.workerImplementation?.size}${
233 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
234 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
235 this.workerImplementation?.maxElementsPerWorker != null
236 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
241 Configuration
.workerDynamicPoolInUse() &&
244 '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'
247 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
249 this.starting
= false
251 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
254 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
258 public async stop (): Promise
<void> {
260 if (!this.stopping
) {
262 await this.uiServer
.sendInternalRequest(
263 this.uiServer
.buildProtocolRequest(
265 ProcedureName
.STOP_CHARGING_STATION
,
266 Constants
.EMPTY_FROZEN_OBJECT
270 await this.waitChargingStationsStopped()
272 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
274 await this.workerImplementation
?.stop()
275 delete this.workerImplementation
276 this.removeAllListeners()
277 this.uiServer
.clearCaches()
278 this.initializedCounters
= false
279 await this.storage
?.close()
282 this.stopping
= false
284 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
287 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
291 private async restart (): Promise
<void> {
294 this.uiServerStarted
&&
295 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
299 this.uiServerStarted
= false
304 private async waitChargingStationsStopped (): Promise
<string> {
305 return await new Promise
<string>((resolve
, reject
) => {
306 const waitTimeout
= setTimeout(() => {
307 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
308 Constants.STOP_CHARGING_STATIONS_TIMEOUT
309 )} reached at stopping charging stations`
310 console
.warn(chalk
.yellow(timeoutMessage
))
311 reject(new Error(timeoutMessage
))
312 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
313 waitChargingStationEvents(
315 ChargingStationWorkerMessageEvents
.stopped
,
316 this.numberOfStartedChargingStations
319 resolve('Charging stations stopped')
323 clearTimeout(waitTimeout
)
328 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
332 let elementsPerWorker
: number
333 switch (workerConfiguration
.elementsPerWorker
) {
335 elementsPerWorker
= this.numberOfConfiguredChargingStations
340 this.numberOfConfiguredChargingStations
> availableParallelism()
341 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
345 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
347 dirname(fileURLToPath(import.meta
.url
)),
348 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
350 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
351 workerConfiguration
.processType
!,
353 workerStartDelay
: workerConfiguration
.startDelay
,
354 elementStartDelay
: workerConfiguration
.elementStartDelay
,
355 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
356 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
357 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
358 poolMinSize
: workerConfiguration
.poolMinSize
!,
361 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
362 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
368 private messageHandler (
369 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
372 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
380 case ChargingStationWorkerMessageEvents
.added
:
381 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
383 case ChargingStationWorkerMessageEvents
.deleted
:
384 this.emit(ChargingStationWorkerMessageEvents
.deleted
, msg
.data
)
386 case ChargingStationWorkerMessageEvents
.started
:
387 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
389 case ChargingStationWorkerMessageEvents
.stopped
:
390 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
392 case ChargingStationWorkerMessageEvents
.updated
:
393 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
395 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
396 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
398 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
399 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
401 case ChargingStationWorkerMessageEvents
.workerElementError
:
402 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
406 `Unknown charging station worker event: '${
408 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
413 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
421 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
422 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
424 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
425 data.stationInfo.chargingStationId
426 } (hashId: ${data.stationInfo.hashId}) added (${
427 this.numberOfAddedChargingStations
428 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
432 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
433 this.uiServer
.chargingStations
.delete(data
.stationInfo
.hashId
)
434 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
435 const templateStatistics
= this.templateStatistics
.get(data
.stationInfo
.templateName
)!
436 --templateStatistics
.added
437 templateStatistics
.indexes
.delete(data
.stationInfo
.templateIndex
)
439 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
440 data.stationInfo.chargingStationId
441 } (hashId: ${data.stationInfo.hashId}) deleted (${
442 this.numberOfAddedChargingStations
443 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
447 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
448 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
449 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
450 ++this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
452 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
453 data.stationInfo.chargingStationId
454 } (hashId: ${data.stationInfo.hashId}) started (${
455 this.numberOfStartedChargingStations
456 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
460 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
461 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
462 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
463 --this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
465 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
466 data.stationInfo.chargingStationId
467 } (hashId: ${data.stationInfo.hashId}) stopped (${
468 this.numberOfStartedChargingStations
469 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
473 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
474 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
477 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
478 // eslint-disable-next-line @typescript-eslint/unbound-method
479 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
481 this.storage
.storePerformanceStatistics
as (
482 performanceStatistics
: Statistics
484 )(data
).catch(Constants
.EMPTY_FUNCTION
)
486 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
492 private initializeCounters (): void {
493 if (!this.initializedCounters
) {
494 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
495 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
496 if (isNotEmptyArray(stationTemplateUrls
)) {
497 for (const stationTemplateUrl
of stationTemplateUrls
) {
498 const templateName
= join(
499 parse(stationTemplateUrl
.file
).dir
,
500 parse(stationTemplateUrl
.file
).name
502 this.templateStatistics
.set(templateName
, {
503 configured
: stationTemplateUrl
.numberOfStations
,
506 indexes
: new Set
<number>()
508 this.uiServer
.chargingStationTemplates
.add(templateName
)
510 if (this.templateStatistics
.size
!== stationTemplateUrls
.length
) {
513 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
516 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
520 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
522 exit(exitCodes
.missingChargingStationsConfiguration
)
525 this.numberOfConfiguredChargingStations
=== 0 &&
526 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
531 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
534 exit(exitCodes
.noChargingStationTemplates
)
536 this.initializedCounters
= true
540 public async addChargingStation (
542 stationTemplateFile
: string,
543 options
?: ChargingStationOptions
545 await this.workerImplementation
?.addElement({
548 dirname(fileURLToPath(import.meta
.url
)),
555 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
556 const templateStatistics
= this.templateStatistics
.get(parse(stationTemplateFile
).name
)!
557 ++templateStatistics
.added
558 templateStatistics
.indexes
.add(index
)
561 private gracefulShutdown (): void {
564 console
.info(chalk
.green('Graceful shutdown'))
566 this.uiServerStarted
= false
567 this.waitChargingStationsStopped()
569 exit(exitCodes
.succeeded
)
572 exit(exitCodes
.gracefulShutdownError
)
576 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
577 exit(exitCodes
.gracefulShutdownError
)
581 private readonly logPrefix
= (): string => {
582 return logPrefix(' Bootstrap |')