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 ChargingStationWorkerData
,
22 type ChargingStationWorkerEventError
,
23 type ChargingStationWorkerMessage
,
24 type ChargingStationWorkerMessageData
,
25 ChargingStationWorkerMessageEvents
,
29 type StorageConfiguration
,
30 type UIServerConfiguration
,
31 type WorkerConfiguration
32 } from
'../types/index.js'
36 formatDurationMilliSeconds
,
38 handleUncaughtException
,
39 handleUnhandledRejection
,
45 } from
'../utils/index.js'
46 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
48 const moduleName
= 'Bootstrap'
52 missingChargingStationsConfiguration
= 1,
53 duplicateChargingStationTemplateUrls
= 2,
54 noChargingStationTemplates
= 3,
55 gracefulShutdownError
= 4
58 interface TemplateChargingStations
{
65 export class Bootstrap
extends EventEmitter
{
66 private static instance
: Bootstrap
| null = null
67 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
68 private readonly uiServer
?: AbstractUIServer
69 private storage
?: Storage
70 private readonly chargingStationsByTemplate
: Map
<string, TemplateChargingStations
>
71 private readonly version
: string = version
72 private initializedCounters
: boolean
73 private started
: boolean
74 private starting
: boolean
75 private stopping
: boolean
77 private constructor () {
79 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
80 process
.on(signal
, this.gracefulShutdown
.bind(this))
82 // Enable unconditionally for now
83 handleUnhandledRejection()
84 handleUncaughtException()
88 this.chargingStationsByTemplate
= new Map
<string, TemplateChargingStations
>()
89 this.uiServer
= UIServerFactory
.getUIServerImplementation(
90 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
92 this.initializedCounters
= false
93 this.initializeCounters()
94 Configuration
.configurationChangeCallback
= async () => {
96 await Bootstrap
.getInstance().restart()
101 public static getInstance (): Bootstrap
{
102 if (Bootstrap
.instance
=== null) {
103 Bootstrap
.instance
= new Bootstrap()
105 return Bootstrap
.instance
108 public get
numberOfChargingStationTemplates (): number {
109 return this.chargingStationsByTemplate
.size
112 public get
numberOfConfiguredChargingStations (): number {
113 return [...this.chargingStationsByTemplate
.values()].reduce(
114 (accumulator
, value
) => accumulator
+ value
.configured
,
119 public getLastIndex (templateName
: string): number {
120 return this.chargingStationsByTemplate
.get(templateName
)?.lastIndex
?? 0
123 public getPerformanceStatistics (): IterableIterator
<Statistics
> | undefined {
124 return this.storage
?.getPerformanceStatistics()
127 private get
numberOfAddedChargingStations (): number {
128 return [...this.chargingStationsByTemplate
.values()].reduce(
129 (accumulator
, value
) => accumulator
+ value
.added
,
134 private get
numberOfStartedChargingStations (): number {
135 return [...this.chargingStationsByTemplate
.values()].reduce(
136 (accumulator
, value
) => accumulator
+ value
.started
,
141 public async start (): Promise
<void> {
143 if (!this.starting
) {
145 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
146 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
147 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
148 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
150 ChargingStationWorkerMessageEvents
.performanceStatistics
,
151 this.workerEventPerformanceStatistics
153 this.initializeCounters()
154 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
155 ConfigurationSection
.worker
157 this.initializeWorkerImplementation(workerConfiguration
)
158 await this.workerImplementation
?.start()
159 const performanceStorageConfiguration
=
160 Configuration
.getConfigurationSection
<StorageConfiguration
>(
161 ConfigurationSection
.performanceStorage
163 if (performanceStorageConfiguration
.enabled
=== true) {
164 this.storage
= StorageFactory
.getStorage(
165 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
166 performanceStorageConfiguration
.type!,
167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168 performanceStorageConfiguration
.uri
!,
171 await this.storage
?.open()
173 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
174 .enabled
=== true && this.uiServer
?.start()
175 // Start ChargingStation object instance in worker thread
176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
180 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
181 ?.configured
?? stationTemplateUrl
.numberOfStations
182 for (let index
= 1; index
<= nbStations
; index
++) {
183 await this.addChargingStation(index
, stationTemplateUrl
.file
)
188 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
196 `Charging stations simulator ${
198 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
199 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
200 }${this.workerImplementation?.size}${
201 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
202 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
203 this.workerImplementation?.maxElementsPerWorker != null
204 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
209 Configuration
.workerDynamicPoolInUse() &&
212 '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'
215 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
217 this.starting
= false
219 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
222 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
226 public async stop (): Promise
<void> {
228 if (!this.stopping
) {
230 await this.uiServer
?.sendInternalRequest(
231 this.uiServer
.buildProtocolRequest(
233 ProcedureName
.STOP_CHARGING_STATION
,
234 Constants
.EMPTY_FROZEN_OBJECT
238 await this.waitChargingStationsStopped()
240 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
242 await this.workerImplementation
?.stop()
243 delete this.workerImplementation
244 this.removeAllListeners()
245 await this.storage
?.close()
248 this.stopping
= false
250 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
253 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
257 private async restart (): Promise
<void> {
259 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
260 .enabled
!== true && this.uiServer
?.stop()
261 this.initializedCounters
= false
265 private async waitChargingStationsStopped (): Promise
<string> {
266 return await new Promise
<string>((resolve
, reject
) => {
267 const waitTimeout
= setTimeout(() => {
268 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
269 Constants.STOP_CHARGING_STATIONS_TIMEOUT
270 )} reached at stopping charging stations`
271 console
.warn(chalk
.yellow(timeoutMessage
))
272 reject(new Error(timeoutMessage
))
273 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
274 waitChargingStationEvents(
276 ChargingStationWorkerMessageEvents
.stopped
,
277 this.numberOfStartedChargingStations
280 resolve('Charging stations stopped')
284 clearTimeout(waitTimeout
)
289 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
293 let elementsPerWorker
: number | undefined
294 switch (workerConfiguration
.elementsPerWorker
) {
297 this.numberOfConfiguredChargingStations
> availableParallelism()
298 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
302 elementsPerWorker
= this.numberOfConfiguredChargingStations
305 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
307 dirname(fileURLToPath(import.meta
.url
)),
308 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
310 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
311 workerConfiguration
.processType
!,
313 workerStartDelay
: workerConfiguration
.startDelay
,
314 elementStartDelay
: workerConfiguration
.elementStartDelay
,
315 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
316 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
317 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
318 poolMinSize
: workerConfiguration
.poolMinSize
!,
319 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
321 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
322 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
328 private messageHandler (
329 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
332 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
340 case ChargingStationWorkerMessageEvents
.added
:
341 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
as ChargingStationData
)
343 case ChargingStationWorkerMessageEvents
.started
:
344 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
346 case ChargingStationWorkerMessageEvents
.stopped
:
347 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
349 case ChargingStationWorkerMessageEvents
.updated
:
350 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
352 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
354 ChargingStationWorkerMessageEvents
.performanceStatistics
,
355 msg
.data
as Statistics
358 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
360 case ChargingStationWorkerMessageEvents
.workerElementError
:
362 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${(msg.data as ChargingStationWorkerEventError).event}' event on worker:`,
365 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
369 `Unknown charging station worker event: '${
371 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
376 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
384 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
385 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
386 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
387 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
389 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
390 data.stationInfo.chargingStationId
391 } (hashId: ${data.stationInfo.hashId}) added (${
392 this.numberOfAddedChargingStations
393 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
397 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
398 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
402 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
403 data.stationInfo.chargingStationId
404 } (hashId: ${data.stationInfo.hashId}) started (${
405 this.numberOfStartedChargingStations
406 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
410 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
411 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
412 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
413 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
415 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
416 data.stationInfo.chargingStationId
417 } (hashId: ${data.stationInfo.hashId}) stopped (${
418 this.numberOfStartedChargingStations
419 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
423 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
424 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
427 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
428 // eslint-disable-next-line @typescript-eslint/unbound-method
429 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
431 this.storage
.storePerformanceStatistics
as (
432 performanceStatistics
: Statistics
434 )(data
).catch(Constants
.EMPTY_FUNCTION
)
436 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
442 private initializeCounters (): void {
443 if (!this.initializedCounters
) {
444 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
445 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
446 if (isNotEmptyArray(stationTemplateUrls
)) {
447 for (const stationTemplateUrl
of stationTemplateUrls
) {
448 const templateName
= parse(stationTemplateUrl
.file
).name
449 this.chargingStationsByTemplate
.set(templateName
, {
450 configured
: stationTemplateUrl
.numberOfStations
,
455 this.uiServer
?.chargingStationTemplates
.add(templateName
)
457 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
460 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
463 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
467 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
469 exit(exitCodes
.missingChargingStationsConfiguration
)
472 this.numberOfConfiguredChargingStations
=== 0 &&
473 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
478 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
481 exit(exitCodes
.noChargingStationTemplates
)
483 this.initializedCounters
= true
487 public async addChargingStation (index
: number, stationTemplateFile
: string): Promise
<void> {
488 await this.workerImplementation
?.addElement({
491 dirname(fileURLToPath(import.meta
.url
)),
497 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
498 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
500 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
504 private gracefulShutdown (): void {
507 console
.info(chalk
.green('Graceful shutdown'))
508 this.uiServer
?.stop()
509 this.waitChargingStationsStopped()
511 exit(exitCodes
.succeeded
)
514 exit(exitCodes
.gracefulShutdownError
)
518 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
519 exit(exitCodes
.gracefulShutdownError
)
523 private readonly logPrefix
= (): string => {
524 return logPrefix(' Bootstrap |')