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'
9 import chalk from
'chalk'
10 import { availableParallelism
, type MessageHandler
} from
'poolifier'
11 import type { Worker
} from
'worker_threads'
13 import { version
} from
'../../package.json'
14 import { BaseError
} from
'../exception/index.js'
15 import { type Storage
, StorageFactory
} from
'../performance/index.js'
17 type ChargingStationData
,
18 type ChargingStationOptions
,
19 type ChargingStationWorkerData
,
20 type ChargingStationWorkerEventError
,
21 type ChargingStationWorkerMessage
,
22 type ChargingStationWorkerMessageData
,
23 ChargingStationWorkerMessageEvents
,
25 type InternalTemplateStatistics
,
29 type StorageConfiguration
,
30 type UIServerConfiguration
,
31 type WorkerConfiguration
32 } from
'../types/index.js'
34 buildTemplateStatisticsPayload
,
37 formatDurationMilliSeconds
,
39 handleUncaughtException
,
40 handleUnhandledRejection
,
45 } from
'../utils/index.js'
46 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
47 import { buildTemplateName
, waitChargingStationEvents
} from
'./Helpers.js'
48 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer.js'
49 import { UIServerFactory
} from
'./ui-server/UIServerFactory.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()!) {
211 const nbStations
= stationTemplateUrl
.numberOfStations
212 for (let index
= 1; index
<= nbStations
; index
++) {
213 await this.addChargingStation(index
, stationTemplateUrl
.file
)
218 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
226 `Charging stations simulator ${
228 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
229 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
230 }${this.workerImplementation?.size}${
231 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
232 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
233 this.workerImplementation?.maxElementsPerWorker != null
234 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
239 Configuration
.workerDynamicPoolInUse() &&
242 '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'
245 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
247 this.starting
= false
249 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
252 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
256 public async stop (): Promise
<void> {
258 if (!this.stopping
) {
260 await this.uiServer
.sendInternalRequest(
261 this.uiServer
.buildProtocolRequest(
263 ProcedureName
.STOP_CHARGING_STATION
,
264 Constants
.EMPTY_FROZEN_OBJECT
268 await this.waitChargingStationsStopped()
270 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
272 await this.workerImplementation
?.stop()
273 delete this.workerImplementation
274 this.removeAllListeners()
275 this.uiServer
.clearCaches()
276 this.initializedCounters
= false
277 await this.storage
?.close()
280 this.stopping
= false
282 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
285 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
289 private async restart (): Promise
<void> {
292 this.uiServerStarted
&&
293 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
297 this.uiServerStarted
= false
302 private async waitChargingStationsStopped (): Promise
<string> {
303 return await new Promise
<string>((resolve
, reject
) => {
304 const waitTimeout
= setTimeout(() => {
305 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
306 Constants.STOP_CHARGING_STATIONS_TIMEOUT
307 )} reached at stopping charging stations`
308 console
.warn(chalk
.yellow(timeoutMessage
))
309 reject(new Error(timeoutMessage
))
310 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
311 waitChargingStationEvents(
313 ChargingStationWorkerMessageEvents
.stopped
,
314 this.numberOfStartedChargingStations
317 resolve('Charging stations stopped')
321 clearTimeout(waitTimeout
)
326 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
330 let elementsPerWorker
: number
331 switch (workerConfiguration
.elementsPerWorker
) {
333 elementsPerWorker
= this.numberOfConfiguredChargingStations
338 this.numberOfConfiguredChargingStations
> availableParallelism()
339 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
343 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
345 dirname(fileURLToPath(import.meta
.url
)),
346 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
348 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
349 workerConfiguration
.processType
!,
351 workerStartDelay
: workerConfiguration
.startDelay
,
352 elementStartDelay
: workerConfiguration
.elementStartDelay
,
353 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
354 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
355 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
356 poolMinSize
: workerConfiguration
.poolMinSize
!,
359 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
360 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
366 private messageHandler (
367 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
370 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
378 case ChargingStationWorkerMessageEvents
.added
:
379 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
)
381 case ChargingStationWorkerMessageEvents
.deleted
:
382 this.emit(ChargingStationWorkerMessageEvents
.deleted
, msg
.data
)
384 case ChargingStationWorkerMessageEvents
.started
:
385 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
)
387 case ChargingStationWorkerMessageEvents
.stopped
:
388 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
)
390 case ChargingStationWorkerMessageEvents
.updated
:
391 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
)
393 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
394 this.emit(ChargingStationWorkerMessageEvents
.performanceStatistics
, msg
.data
)
396 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
397 this.emit(ChargingStationWorkerMessageEvents
.addWorkerElement
, msg
.data
)
399 case ChargingStationWorkerMessageEvents
.workerElementError
:
400 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
404 `Unknown charging station worker event: '${
406 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
411 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
419 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
420 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
422 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
423 data.stationInfo.chargingStationId
424 } (hashId: ${data.stationInfo.hashId}) added (${
425 this.numberOfAddedChargingStations
426 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
430 private readonly workerEventDeleted
= (data
: ChargingStationData
): void => {
431 this.uiServer
.chargingStations
.delete(data
.stationInfo
.hashId
)
432 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
433 const templateStatistics
= this.templateStatistics
.get(data
.stationInfo
.templateName
)!
434 --templateStatistics
.added
435 templateStatistics
.indexes
.delete(data
.stationInfo
.templateIndex
)
437 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
438 data.stationInfo.chargingStationId
439 } (hashId: ${data.stationInfo.hashId}) deleted (${
440 this.numberOfAddedChargingStations
441 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
445 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
446 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
447 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
448 ++this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
450 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
451 data.stationInfo.chargingStationId
452 } (hashId: ${data.stationInfo.hashId}) started (${
453 this.numberOfStartedChargingStations
454 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
458 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
459 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
460 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
461 --this.templateStatistics
.get(data
.stationInfo
.templateName
)!.started
463 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
464 data.stationInfo.chargingStationId
465 } (hashId: ${data.stationInfo.hashId}) stopped (${
466 this.numberOfStartedChargingStations
467 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
471 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
472 this.uiServer
.chargingStations
.set(data
.stationInfo
.hashId
, data
)
475 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
476 // eslint-disable-next-line @typescript-eslint/unbound-method
477 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
479 this.storage
.storePerformanceStatistics
as (
480 performanceStatistics
: Statistics
482 )(data
).catch(Constants
.EMPTY_FUNCTION
)
484 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
490 private initializeCounters (): void {
491 if (!this.initializedCounters
) {
492 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
493 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
494 if (isNotEmptyArray(stationTemplateUrls
)) {
495 for (const stationTemplateUrl
of stationTemplateUrls
) {
496 const templateName
= buildTemplateName(stationTemplateUrl
.file
)
497 this.templateStatistics
.set(templateName
, {
498 configured
: stationTemplateUrl
.numberOfStations
,
501 indexes
: new Set
<number>()
503 this.uiServer
.chargingStationTemplates
.add(templateName
)
505 if (this.templateStatistics
.size
!== stationTemplateUrls
.length
) {
508 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
511 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
515 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
517 exit(exitCodes
.missingChargingStationsConfiguration
)
520 this.numberOfConfiguredChargingStations
=== 0 &&
521 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
526 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
529 exit(exitCodes
.noChargingStationTemplates
)
531 this.initializedCounters
= true
535 public async addChargingStation (
537 templateFile
: string,
538 options
?: ChargingStationOptions
540 if (!this.started
&& !this.starting
) {
542 'Cannot add charging station while the charging stations simulator is not started'
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(buildTemplateName(templateFile
))!
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 |')