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 type { Worker
} from
'worker_threads'
9 import chalk from
'chalk'
10 import { type MessageHandler
, availableParallelism
} from
'poolifier'
12 import { waitChargingStationEvents
} from
'./Helpers.js'
13 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer.js'
14 import { UIServerFactory
} from
'./ui-server/UIServerFactory.js'
15 import { version
} from
'../../package.json'
16 import { BaseError
} from
'../exception/index.js'
17 import { type Storage
, StorageFactory
} from
'../performance/index.js'
19 type ChargingStationData
,
20 type ChargingStationWorkerData
,
21 type ChargingStationWorkerMessage
,
22 type ChargingStationWorkerMessageData
,
23 ChargingStationWorkerMessageEvents
,
26 type StationTemplateUrl
,
28 type StorageConfiguration
,
29 type UIServerConfiguration
,
30 type WorkerConfiguration
31 } from
'../types/index.js'
35 formatDurationMilliSeconds
,
37 handleUncaughtException
,
38 handleUnhandledRejection
,
43 } from
'../utils/index.js'
44 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
46 const moduleName
= 'Bootstrap'
50 missingChargingStationsConfiguration
= 1,
51 duplicateChargingStationTemplateUrls
= 2,
52 noChargingStationTemplates
= 3,
53 gracefulShutdownError
= 4
56 export class Bootstrap
extends EventEmitter
{
57 private static instance
: Bootstrap
| null = null
58 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
59 private readonly uiServer
?: AbstractUIServer
60 private storage
?: Storage
61 private readonly chargingStationsByTemplate
: Map
<string, { configured
: number, started
: number }>
62 private readonly version
: string = version
63 private initializedCounters
: boolean
64 private started
: boolean
65 private starting
: boolean
66 private stopping
: boolean
68 private constructor () {
70 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
71 process
.on(signal
, this.gracefulShutdown
.bind(this))
73 // Enable unconditionally for now
74 handleUnhandledRejection()
75 handleUncaughtException()
79 this.chargingStationsByTemplate
= new Map
<
86 this.uiServer
= UIServerFactory
.getUIServerImplementation(
87 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
89 this.initializedCounters
= false
90 this.initializeCounters()
91 Configuration
.configurationChangeCallback
= async () => {
92 await Bootstrap
.getInstance().restart(false)
96 public static getInstance (): Bootstrap
{
97 if (Bootstrap
.instance
=== null) {
98 Bootstrap
.instance
= new Bootstrap()
100 return Bootstrap
.instance
103 public get
numberOfChargingStationTemplates (): number {
104 return this.chargingStationsByTemplate
.size
107 public get
numberOfConfiguredChargingStations (): number {
108 return [...this.chargingStationsByTemplate
.values()].reduce(
109 (accumulator
, value
) => accumulator
+ value
.configured
,
114 private get
numberOfStartedChargingStations (): number {
115 return [...this.chargingStationsByTemplate
.values()].reduce(
116 (accumulator
, value
) => accumulator
+ value
.started
,
121 public async start (): Promise
<void> {
123 if (!this.starting
) {
125 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
126 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
127 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
129 ChargingStationWorkerMessageEvents
.performanceStatistics
,
130 this.workerEventPerformanceStatistics
132 this.initializeCounters()
133 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
134 ConfigurationSection
.worker
136 this.initializeWorkerImplementation(workerConfiguration
)
137 await this.workerImplementation
?.start()
138 const performanceStorageConfiguration
=
139 Configuration
.getConfigurationSection
<StorageConfiguration
>(
140 ConfigurationSection
.performanceStorage
142 if (performanceStorageConfiguration
.enabled
=== true) {
143 this.storage
= StorageFactory
.getStorage(
144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
145 performanceStorageConfiguration
.type!,
146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
147 performanceStorageConfiguration
.uri
!,
150 await this.storage
?.open()
152 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
153 .enabled
=== true && this.uiServer
?.start()
154 // Start ChargingStation object instance in worker thread
155 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
156 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
159 this.chargingStationsByTemplate
.get(parse(stationTemplateUrl
.file
).name
)
160 ?.configured
?? stationTemplateUrl
.numberOfStations
161 for (let index
= 1; index
<= nbStations
; index
++) {
162 await this.startChargingStation(index
, stationTemplateUrl
)
167 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
175 `Charging stations simulator ${
177 } started with ${this.numberOfConfiguredChargingStations} charging station(s) from ${this.numberOfChargingStationTemplates} configured charging station template(s) and ${
178 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}
/` : ''
179 }${this.workerImplementation?.size}${
180 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}
` : ''
181 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
182 this.workerImplementation?.maxElementsPerWorker != null
183 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
188 Configuration
.workerDynamicPoolInUse() &&
191 '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'
194 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
196 this.starting
= false
198 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
201 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
205 public async stop (stopChargingStations
= true): Promise
<void> {
207 if (!this.stopping
) {
209 if (stopChargingStations
) {
210 await this.uiServer
?.sendInternalRequest(
211 this.uiServer
.buildProtocolRequest(
213 ProcedureName
.STOP_CHARGING_STATION
,
214 Constants
.EMPTY_FROZEN_OBJECT
218 await this.waitChargingStationsStopped()
220 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
223 await this.workerImplementation
?.stop()
224 delete this.workerImplementation
225 this.removeAllListeners()
226 await this.storage
?.close()
229 this.stopping
= false
231 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
234 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
238 public async restart (stopChargingStations
?: boolean): Promise
<void> {
239 await this.stop(stopChargingStations
)
240 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
241 .enabled
=== false && this.uiServer
?.stop()
242 this.initializedCounters
= false
246 private async waitChargingStationsStopped (): Promise
<string> {
247 return await new Promise
<string>((resolve
, reject
) => {
248 const waitTimeout
= setTimeout(() => {
249 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
250 Constants.STOP_CHARGING_STATIONS_TIMEOUT
251 )} reached at stopping charging stations`
252 console
.warn(chalk
.yellow(timeoutMessage
))
253 reject(new Error(timeoutMessage
))
254 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
255 waitChargingStationEvents(
257 ChargingStationWorkerMessageEvents
.stopped
,
258 this.numberOfStartedChargingStations
261 resolve('Charging stations stopped')
265 clearTimeout(waitTimeout
)
270 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
271 let elementsPerWorker
: number | undefined
272 switch (workerConfiguration
.elementsPerWorker
) {
275 this.numberOfConfiguredChargingStations
> availableParallelism()
276 ? Math.round(this.numberOfConfiguredChargingStations
/ (availableParallelism() * 1.5))
280 elementsPerWorker
= this.numberOfConfiguredChargingStations
283 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
285 dirname(fileURLToPath(import.meta
.url
)),
286 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
288 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
289 workerConfiguration
.processType
!,
291 workerStartDelay
: workerConfiguration
.startDelay
,
292 elementStartDelay
: workerConfiguration
.elementStartDelay
,
293 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
294 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 poolMinSize
: workerConfiguration
.poolMinSize
!,
297 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
299 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
300 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
306 private messageHandler (
307 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
310 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
318 case ChargingStationWorkerMessageEvents
.started
:
319 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
321 case ChargingStationWorkerMessageEvents
.stopped
:
322 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
324 case ChargingStationWorkerMessageEvents
.updated
:
325 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
327 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
329 ChargingStationWorkerMessageEvents
.performanceStatistics
,
330 msg
.data
as Statistics
333 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
335 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while starting worker element:`,
338 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
)
340 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
344 `Unknown charging station worker event: '${
346 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
351 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
359 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
360 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
361 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
362 ++this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
364 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
365 data.stationInfo.chargingStationId
366 } (hashId: ${data.stationInfo.hashId}) started (${
367 this.numberOfStartedChargingStations
368 } started from ${this.numberOfConfiguredChargingStations})`
372 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
373 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
375 --this.chargingStationsByTemplate
.get(data
.stationInfo
.templateName
)!.started
377 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
378 data.stationInfo.chargingStationId
379 } (hashId: ${data.stationInfo.hashId}) stopped (${
380 this.numberOfStartedChargingStations
381 } started from ${this.numberOfConfiguredChargingStations})`
385 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
386 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
389 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
390 // eslint-disable-next-line @typescript-eslint/unbound-method
391 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
393 this.storage
.storePerformanceStatistics
as (
394 performanceStatistics
: Statistics
396 )(data
).catch(Constants
.EMPTY_FUNCTION
)
398 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
404 private initializeCounters (): void {
405 if (!this.initializedCounters
) {
406 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
407 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
408 if (isNotEmptyArray(stationTemplateUrls
)) {
409 for (const stationTemplateUrl
of stationTemplateUrls
) {
410 const templateName
= parse(stationTemplateUrl
.file
).name
411 this.chargingStationsByTemplate
.set(templateName
, {
412 configured
: stationTemplateUrl
.numberOfStations
,
415 this.uiServer
?.chargingStationTemplates
.add(templateName
)
417 if (this.chargingStationsByTemplate
.size
!== stationTemplateUrls
.length
) {
420 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
423 exit(exitCodes
.duplicateChargingStationTemplateUrls
)
427 chalk
.red("'stationTemplateUrls' not defined or empty, please check your configuration")
429 exit(exitCodes
.missingChargingStationsConfiguration
)
431 if (this.numberOfConfiguredChargingStations
=== 0) {
434 "'stationTemplateUrls' has no charging station enabled, please check your configuration"
437 exit(exitCodes
.noChargingStationTemplates
)
439 this.initializedCounters
= true
443 private async startChargingStation (
445 stationTemplateUrl
: StationTemplateUrl
447 await this.workerImplementation
?.addElement({
450 dirname(fileURLToPath(import.meta
.url
)),
453 stationTemplateUrl
.file
458 private gracefulShutdown (): void {
461 console
.info(chalk
.green('Graceful shutdown'))
462 this.uiServer
?.stop()
463 // stop() asks for charging stations to stop by default
464 this.waitChargingStationsStopped()
466 exit(exitCodes
.succeeded
)
469 exit(exitCodes
.gracefulShutdownError
)
473 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
474 exit(exitCodes
.gracefulShutdownError
)
478 private readonly logPrefix
= (): string => {
479 return logPrefix(' Bootstrap |')