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 ChargingStationWorkerMessage
,
23 type ChargingStationWorkerMessageData
,
24 ChargingStationWorkerMessageEvents
,
28 type StorageConfiguration
,
29 type UIServerConfiguration
,
30 type WorkerConfiguration
31 } from
'../types/index.js'
35 formatDurationMilliSeconds
,
37 handleUncaughtException
,
38 handleUnhandledRejection
,
44 } from
'../utils/index.js'
45 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
47 const moduleName
= 'Bootstrap'
51 missingChargingStationsConfiguration
= 1,
52 duplicateChargingStationTemplateUrls
= 2,
53 noChargingStationTemplates
= 3,
54 gracefulShutdownError
= 4
57 export class Bootstrap
extends EventEmitter
{
58 private static instance
: Bootstrap
| null = null
59 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
60 private readonly uiServer
?: AbstractUIServer
61 private storage
?: Storage
62 private readonly chargingStationsByTemplate
: Map
<
64 { configured
: number, started
: number, lastIndex
: number }
67 private readonly version
: string = version
68 private initializedCounters
: boolean
69 private started
: boolean
70 private starting
: boolean
71 private stopping
: boolean
73 private constructor () {
75 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
76 process
.on(signal
, this.gracefulShutdown
.bind(this))
78 // Enable unconditionally for now
79 handleUnhandledRejection()
80 handleUncaughtException()
84 this.chargingStationsByTemplate
= new Map
<
86 { configured
: number, started
: number, lastIndex
: number }
88 this.uiServer
= UIServerFactory
.getUIServerImplementation(
89 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
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.chargingStationsByTemplate
.size
111 public get
numberOfConfiguredChargingStations (): number {
112 return [...this.chargingStationsByTemplate
.values()].reduce(
113 (accumulator
, value
) => accumulator
+ value
.configured
,
118 public getLastIndex (templateName
: string): number {
119 return this.chargingStationsByTemplate
.get(templateName
)?.lastIndex
?? 0
122 private get
numberOfStartedChargingStations (): number {
123 return [...this.chargingStationsByTemplate
.values()].reduce(
124 (accumulator
, value
) => accumulator
+ value
.started
,
129 public async start (): Promise
<void> {
131 if (!this.starting
) {
133 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
134 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
135 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
137 ChargingStationWorkerMessageEvents
.performanceStatistics
,
138 this.workerEventPerformanceStatistics
140 this.initializeCounters()
141 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
142 ConfigurationSection
.worker
144 this.initializeWorkerImplementation(workerConfiguration
)
145 await this.workerImplementation
?.start()
146 const performanceStorageConfiguration
=
147 Configuration
.getConfigurationSection
<StorageConfiguration
>(
148 ConfigurationSection
.performanceStorage
150 if (performanceStorageConfiguration
.enabled
=== true) {
151 this.storage
= StorageFactory
.getStorage(
152 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153 performanceStorageConfiguration
.type!,
154 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
155 performanceStorageConfiguration
.uri
!,
158 await this.storage
?.open()
160 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
161 .enabled
=== true && this.uiServer
?.start()
162 // Start ChargingStation object instance in worker thread
163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
164 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
167 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
168 ?.configured
?? stationTemplateUrl
.numberOfStations
169 for (let index
= 1; index
<= nbStations
; index
++) {
170 await this.addChargingStation(index
, stationTemplateUrl
.file
)
175 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
183 `Charging stations simulator ${
185 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
186 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
187 }${this.workerImplementation?.size}${
188 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
189 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
190 this.workerImplementation?.maxElementsPerWorker != null
191 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
196 Configuration
.workerDynamicPoolInUse() &&
199 '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'
202 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
204 this.starting
= false
206 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
209 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
213 public async stop (): Promise
<void> {
215 if (!this.stopping
) {
217 await this.uiServer
?.sendInternalRequest(
218 this.uiServer
.buildProtocolRequest(
220 ProcedureName
.STOP_CHARGING_STATION
,
221 Constants
.EMPTY_FROZEN_OBJECT
225 await this.waitChargingStationsStopped()
227 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
229 await this.workerImplementation
?.stop()
230 delete this.workerImplementation
231 this.removeAllListeners()
232 await this.storage
?.close()
235 this.stopping
= false
237 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
240 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
244 private async restart (): Promise
<void> {
246 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
247 .enabled
=== false && this.uiServer
?.stop()
248 this.initializedCounters
= false
252 private async waitChargingStationsStopped (): Promise
<string> {
253 return await new Promise
<string>((resolve
, reject
) => {
254 const waitTimeout
= setTimeout(() => {
255 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
256 Constants.STOP_CHARGING_STATIONS_TIMEOUT
257 )} reached at stopping charging stations`
258 console
.warn(chalk
.yellow(timeoutMessage
))
259 reject(new Error(timeoutMessage
))
260 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
261 waitChargingStationEvents(
263 ChargingStationWorkerMessageEvents
.stopped
,
264 this.numberOfStartedChargingStations
267 resolve('Charging stations stopped')
271 clearTimeout(waitTimeout
)
276 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
280 let elementsPerWorker
: number | undefined
281 switch (workerConfiguration
.elementsPerWorker
) {
284 this.numberOfConfiguredChargingStations
> availableParallelism()
285 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
289 elementsPerWorker
= this.numberOfConfiguredChargingStations
292 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
294 dirname(fileURLToPath(import.meta
.url
)),
295 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
297 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
298 workerConfiguration
.processType
!,
300 workerStartDelay
: workerConfiguration
.startDelay
,
301 elementStartDelay
: workerConfiguration
.elementStartDelay
,
302 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
303 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
304 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
305 poolMinSize
: workerConfiguration
.poolMinSize
!,
306 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
308 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
309 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
315 private messageHandler (
316 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
319 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
327 case ChargingStationWorkerMessageEvents
.started
:
328 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
330 case ChargingStationWorkerMessageEvents
.stopped
:
331 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
333 case ChargingStationWorkerMessageEvents
.updated
:
334 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
336 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
338 ChargingStationWorkerMessageEvents
.performanceStatistics
,
339 msg
.data
as Statistics
342 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
344 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while starting worker element:`,
347 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
)
349 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
353 `Unknown charging station worker event: '${
355 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
360 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
368 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
369 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
370 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
371 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
373 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
374 data.stationInfo.chargingStationId
375 } (hashId: ${data.stationInfo.hashId}) started (${
376 this.numberOfStartedChargingStations
377 } started from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
381 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
382 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
383 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
386 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
387 data.stationInfo.chargingStationId
388 } (hashId: ${data.stationInfo.hashId}) stopped (${
389 this.numberOfStartedChargingStations
390 } started from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
394 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
395 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
398 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
399 // eslint-disable-next-line @typescript-eslint/unbound-method
400 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
402 this.storage
.storePerformanceStatistics
as (
403 performanceStatistics
: Statistics
405 )(data
).catch(Constants
.EMPTY_FUNCTION
)
407 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
413 private initializeCounters (): void {
414 if (!this.initializedCounters
) {
415 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
417 if (isNotEmptyArray(stationTemplateUrls
)) {
418 for (const stationTemplateUrl
of stationTemplateUrls
) {
419 const templateName
= parse(stationTemplateUrl
.file
).name
420 this.chargingStationsByTemplate
.set(templateName
, {
421 configured
: stationTemplateUrl
.numberOfStations
,
425 this.uiServer
?.chargingStationTemplates
.add(templateName
)
427 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
430 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
433 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
437 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
439 exit(exitCodes
.missingChargingStationsConfiguration
)
442 this.numberOfConfiguredChargingStations
=== 0 &&
443 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
448 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
451 exit(exitCodes
.noChargingStationTemplates
)
453 this.initializedCounters
= true
457 public async addChargingStation (index
: number, stationTemplateFile
: string): Promise
<void> {
458 await this.workerImplementation
?.addElement({
461 dirname(fileURLToPath(import.meta
.url
)),
467 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
468 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
470 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
474 private gracefulShutdown (): void {
477 console
.info(chalk
.green('Graceful shutdown'))
478 this.uiServer
?.stop()
479 // stop() asks for charging stations to stop by default
480 this.waitChargingStationsStopped()
482 exit(exitCodes
.succeeded
)
485 exit(exitCodes
.gracefulShutdownError
)
489 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
490 exit(exitCodes
.gracefulShutdownError
)
494 private readonly logPrefix
= (): string => {
495 return logPrefix(' Bootstrap |')