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 private get
numberOfAddedChargingStations (): number {
124 return [...this.chargingStationsByTemplate
.values()].reduce(
125 (accumulator
, value
) => accumulator
+ value
.added
,
130 private get
numberOfStartedChargingStations (): number {
131 return [...this.chargingStationsByTemplate
.values()].reduce(
132 (accumulator
, value
) => accumulator
+ value
.started
,
137 public async start (): Promise
<void> {
139 if (!this.starting
) {
141 this.on(ChargingStationWorkerMessageEvents
.added
, this.workerEventAdded
)
142 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
143 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
144 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
146 ChargingStationWorkerMessageEvents
.performanceStatistics
,
147 this.workerEventPerformanceStatistics
149 this.initializeCounters()
150 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
151 ConfigurationSection
.worker
153 this.initializeWorkerImplementation(workerConfiguration
)
154 await this.workerImplementation
?.start()
155 const performanceStorageConfiguration
=
156 Configuration
.getConfigurationSection
<StorageConfiguration
>(
157 ConfigurationSection
.performanceStorage
159 if (performanceStorageConfiguration
.enabled
=== true) {
160 this.storage
= StorageFactory
.getStorage(
161 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
162 performanceStorageConfiguration
.type!,
163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
164 performanceStorageConfiguration
.uri
!,
167 await this.storage
?.open()
169 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
170 .enabled
=== true && this.uiServer
?.start()
171 // Start ChargingStation object instance in worker thread
172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
173 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
176 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
177 ?.configured
?? stationTemplateUrl
.numberOfStations
178 for (let index
= 1; index
<= nbStations
; index
++) {
179 await this.addChargingStation(index
, stationTemplateUrl
.file
)
184 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
192 `Charging stations simulator ${
194 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
195 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
196 }${this.workerImplementation?.size}${
197 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
198 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
199 this.workerImplementation?.maxElementsPerWorker != null
200 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
205 Configuration
.workerDynamicPoolInUse() &&
208 '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'
211 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
213 this.starting
= false
215 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
218 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
222 public async stop (): Promise
<void> {
224 if (!this.stopping
) {
226 await this.uiServer
?.sendInternalRequest(
227 this.uiServer
.buildProtocolRequest(
229 ProcedureName
.STOP_CHARGING_STATION
,
230 Constants
.EMPTY_FROZEN_OBJECT
234 await this.waitChargingStationsStopped()
236 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
238 await this.workerImplementation
?.stop()
239 delete this.workerImplementation
240 this.removeAllListeners()
241 await this.storage
?.close()
244 this.stopping
= false
246 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
249 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
253 private async restart (): Promise
<void> {
255 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
256 .enabled
!== true && this.uiServer
?.stop()
257 this.initializedCounters
= false
261 private async waitChargingStationsStopped (): Promise
<string> {
262 return await new Promise
<string>((resolve
, reject
) => {
263 const waitTimeout
= setTimeout(() => {
264 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
265 Constants.STOP_CHARGING_STATIONS_TIMEOUT
266 )} reached at stopping charging stations`
267 console
.warn(chalk
.yellow(timeoutMessage
))
268 reject(new Error(timeoutMessage
))
269 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
270 waitChargingStationEvents(
272 ChargingStationWorkerMessageEvents
.stopped
,
273 this.numberOfStartedChargingStations
276 resolve('Charging stations stopped')
280 clearTimeout(waitTimeout
)
285 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
289 let elementsPerWorker
: number | undefined
290 switch (workerConfiguration
.elementsPerWorker
) {
293 this.numberOfConfiguredChargingStations
> availableParallelism()
294 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
298 elementsPerWorker
= this.numberOfConfiguredChargingStations
301 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
303 dirname(fileURLToPath(import.meta
.url
)),
304 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307 workerConfiguration
.processType
!,
309 workerStartDelay
: workerConfiguration
.startDelay
,
310 elementStartDelay
: workerConfiguration
.elementStartDelay
,
311 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
312 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
313 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
314 poolMinSize
: workerConfiguration
.poolMinSize
!,
315 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
317 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
318 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
324 private messageHandler (
325 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
328 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
336 case ChargingStationWorkerMessageEvents
.added
:
337 this.emit(ChargingStationWorkerMessageEvents
.added
, msg
.data
as ChargingStationData
)
339 case ChargingStationWorkerMessageEvents
.started
:
340 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
342 case ChargingStationWorkerMessageEvents
.stopped
:
343 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
345 case ChargingStationWorkerMessageEvents
.updated
:
346 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
348 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
350 ChargingStationWorkerMessageEvents
.performanceStatistics
,
351 msg
.data
as Statistics
354 case ChargingStationWorkerMessageEvents
.workerElementError
:
356 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${(msg.data as ChargingStationWorkerEventError).event}' event on worker:`,
359 this.emit(ChargingStationWorkerMessageEvents
.workerElementError
, msg
.data
)
361 case ChargingStationWorkerMessageEvents
.addedWorkerElement
:
365 `Unknown charging station worker event: '${
367 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
372 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
380 private readonly workerEventAdded
= (data
: ChargingStationData
): void => {
381 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
382 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
383 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.added
385 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
386 data.stationInfo.chargingStationId
387 } (hashId: ${data.stationInfo.hashId}) added (${
388 this.numberOfAddedChargingStations
389 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
393 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
394 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
398 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
399 data.stationInfo.chargingStationId
400 } (hashId: ${data.stationInfo.hashId}) started (${
401 this.numberOfStartedChargingStations
402 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
406 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
407 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
409 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
411 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
412 data.stationInfo.chargingStationId
413 } (hashId: ${data.stationInfo.hashId}) stopped (${
414 this.numberOfStartedChargingStations
415 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
419 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
420 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
423 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
424 // eslint-disable-next-line @typescript-eslint/unbound-method
425 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
427 this.storage
.storePerformanceStatistics
as (
428 performanceStatistics
: Statistics
430 )(data
).catch(Constants
.EMPTY_FUNCTION
)
432 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
438 private initializeCounters (): void {
439 if (!this.initializedCounters
) {
440 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
441 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
442 if (isNotEmptyArray(stationTemplateUrls
)) {
443 for (const stationTemplateUrl
of stationTemplateUrls
) {
444 const templateName
= parse(stationTemplateUrl
.file
).name
445 this.chargingStationsByTemplate
.set(templateName
, {
446 configured
: stationTemplateUrl
.numberOfStations
,
451 this.uiServer
?.chargingStationTemplates
.add(templateName
)
453 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
456 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
459 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
463 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
465 exit(exitCodes
.missingChargingStationsConfiguration
)
468 this.numberOfConfiguredChargingStations
=== 0 &&
469 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
474 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
477 exit(exitCodes
.noChargingStationTemplates
)
479 this.initializedCounters
= true
483 public async addChargingStation (index
: number, stationTemplateFile
: string): Promise
<void> {
484 await this.workerImplementation
?.addElement({
487 dirname(fileURLToPath(import.meta
.url
)),
493 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
494 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
496 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
500 private gracefulShutdown (): void {
503 console
.info(chalk
.green('Graceful shutdown'))
504 this.uiServer
?.stop()
505 this.waitChargingStationsStopped()
507 exit(exitCodes
.succeeded
)
510 exit(exitCodes
.gracefulShutdownError
)
514 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
515 exit(exitCodes
.gracefulShutdownError
)
519 private readonly logPrefix
= (): string => {
520 return logPrefix(' Bootstrap |')