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 interface TemplateChargingStations
{
63 export class Bootstrap
extends EventEmitter
{
64 private static instance
: Bootstrap
| null = null
65 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
66 private readonly uiServer
?: AbstractUIServer
67 private storage
?: Storage
68 private readonly chargingStationsByTemplate
: Map
<string, TemplateChargingStations
>
69 private readonly version
: string = version
70 private initializedCounters
: boolean
71 private started
: boolean
72 private starting
: boolean
73 private stopping
: boolean
75 private constructor () {
77 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
78 process
.on(signal
, this.gracefulShutdown
.bind(this))
80 // Enable unconditionally for now
81 handleUnhandledRejection()
82 handleUncaughtException()
86 this.chargingStationsByTemplate
= new Map
<string, TemplateChargingStations
>()
87 this.uiServer
= UIServerFactory
.getUIServerImplementation(
88 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
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.chargingStationsByTemplate
.size
110 public get
numberOfConfiguredChargingStations (): number {
111 return [...this.chargingStationsByTemplate
.values()].reduce(
112 (accumulator
, value
) => accumulator
+ value
.configured
,
117 public getLastIndex (templateName
: string): number {
118 return this.chargingStationsByTemplate
.get(templateName
)?.lastIndex
?? 0
121 private get
numberOfStartedChargingStations (): number {
122 return [...this.chargingStationsByTemplate
.values()].reduce(
123 (accumulator
, value
) => accumulator
+ value
.started
,
128 public async start (): Promise
<void> {
130 if (!this.starting
) {
132 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
133 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
134 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
136 ChargingStationWorkerMessageEvents
.performanceStatistics
,
137 this.workerEventPerformanceStatistics
139 this.initializeCounters()
140 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
141 ConfigurationSection
.worker
143 this.initializeWorkerImplementation(workerConfiguration
)
144 await this.workerImplementation
?.start()
145 const performanceStorageConfiguration
=
146 Configuration
.getConfigurationSection
<StorageConfiguration
>(
147 ConfigurationSection
.performanceStorage
149 if (performanceStorageConfiguration
.enabled
=== true) {
150 this.storage
= StorageFactory
.getStorage(
151 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
152 performanceStorageConfiguration
.type!,
153 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154 performanceStorageConfiguration
.uri
!,
157 await this.storage
?.open()
159 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
160 .enabled
=== true && this.uiServer
?.start()
161 // Start ChargingStation object instance in worker thread
162 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
163 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
166 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
167 ?.configured
?? stationTemplateUrl
.numberOfStations
168 for (let index
= 1; index
<= nbStations
; index
++) {
169 await this.addChargingStation(index
, stationTemplateUrl
.file
)
174 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
182 `Charging stations simulator ${
184 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
185 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
186 }${this.workerImplementation?.size}${
187 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
188 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
189 this.workerImplementation?.maxElementsPerWorker != null
190 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
195 Configuration
.workerDynamicPoolInUse() &&
198 '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'
201 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
203 this.starting
= false
205 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
208 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
212 public async stop (): Promise
<void> {
214 if (!this.stopping
) {
216 await this.uiServer
?.sendInternalRequest(
217 this.uiServer
.buildProtocolRequest(
219 ProcedureName
.STOP_CHARGING_STATION
,
220 Constants
.EMPTY_FROZEN_OBJECT
224 await this.waitChargingStationsStopped()
226 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
228 await this.workerImplementation
?.stop()
229 delete this.workerImplementation
230 this.removeAllListeners()
231 await this.storage
?.close()
234 this.stopping
= false
236 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
239 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
243 private async restart (): Promise
<void> {
245 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
246 .enabled
!== true && this.uiServer
?.stop()
247 this.initializedCounters
= false
251 private async waitChargingStationsStopped (): Promise
<string> {
252 return await new Promise
<string>((resolve
, reject
) => {
253 const waitTimeout
= setTimeout(() => {
254 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
255 Constants.STOP_CHARGING_STATIONS_TIMEOUT
256 )} reached at stopping charging stations`
257 console
.warn(chalk
.yellow(timeoutMessage
))
258 reject(new Error(timeoutMessage
))
259 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
260 waitChargingStationEvents(
262 ChargingStationWorkerMessageEvents
.stopped
,
263 this.numberOfStartedChargingStations
266 resolve('Charging stations stopped')
270 clearTimeout(waitTimeout
)
275 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
279 let elementsPerWorker
: number | undefined
280 switch (workerConfiguration
.elementsPerWorker
) {
283 this.numberOfConfiguredChargingStations
> availableParallelism()
284 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
288 elementsPerWorker
= this.numberOfConfiguredChargingStations
291 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
293 dirname(fileURLToPath(import.meta
.url
)),
294 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
296 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
297 workerConfiguration
.processType
!,
299 workerStartDelay
: workerConfiguration
.startDelay
,
300 elementStartDelay
: workerConfiguration
.elementStartDelay
,
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
303 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
304 poolMinSize
: workerConfiguration
.poolMinSize
!,
305 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
307 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
308 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
314 private messageHandler (
315 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
318 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
326 case ChargingStationWorkerMessageEvents
.started
:
327 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
329 case ChargingStationWorkerMessageEvents
.stopped
:
330 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
332 case ChargingStationWorkerMessageEvents
.updated
:
333 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
335 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
337 ChargingStationWorkerMessageEvents
.performanceStatistics
,
338 msg
.data
as Statistics
341 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
343 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while starting worker element:`,
346 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
)
348 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
352 `Unknown charging station worker event: '${
354 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
359 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
367 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
368 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
369 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
370 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
372 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
373 data.stationInfo.chargingStationId
374 } (hashId: ${data.stationInfo.hashId}) started (${
375 this.numberOfStartedChargingStations
376 } started from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
380 private readonly workerEventStopped
= (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
)!.started
385 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
386 data.stationInfo.chargingStationId
387 } (hashId: ${data.stationInfo.hashId}) stopped (${
388 this.numberOfStartedChargingStations
389 } started from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
393 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
394 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
397 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
398 // eslint-disable-next-line @typescript-eslint/unbound-method
399 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
401 this.storage
.storePerformanceStatistics
as (
402 performanceStatistics
: Statistics
404 )(data
).catch(Constants
.EMPTY_FUNCTION
)
406 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
412 private initializeCounters (): void {
413 if (!this.initializedCounters
) {
414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
416 if (isNotEmptyArray(stationTemplateUrls
)) {
417 for (const stationTemplateUrl
of stationTemplateUrls
) {
418 const templateName
= parse(stationTemplateUrl
.file
).name
419 this.chargingStationsByTemplate
.set(templateName
, {
420 configured
: stationTemplateUrl
.numberOfStations
,
424 this.uiServer
?.chargingStationTemplates
.add(templateName
)
426 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
429 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
432 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
436 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
438 exit(exitCodes
.missingChargingStationsConfiguration
)
441 this.numberOfConfiguredChargingStations
=== 0 &&
442 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
447 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
450 exit(exitCodes
.noChargingStationTemplates
)
452 this.initializedCounters
= true
456 public async addChargingStation (index
: number, stationTemplateFile
: string): Promise
<void> {
457 await this.workerImplementation
?.addElement({
460 dirname(fileURLToPath(import.meta
.url
)),
466 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
467 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)!.lastIndex
= max(
469 this.chargingStationsByTemplate
.get(parse(stationTemplateFile
).name
)?.lastIndex
?? -Infinity
473 private gracefulShutdown (): void {
476 console
.info(chalk
.green('Graceful shutdown'))
477 this.uiServer
?.stop()
478 // stop() asks for charging stations to stop by default
479 this.waitChargingStationsStopped()
481 exit(exitCodes
.succeeded
)
484 exit(exitCodes
.gracefulShutdownError
)
488 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
489 exit(exitCodes
.gracefulShutdownError
)
493 private readonly logPrefix
= (): string => {
494 return logPrefix(' Bootstrap |')