1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { EventEmitter
} from
'node:events'
4 import { dirname
, extname
, join
} from
'node:path'
5 import process
, { exit
} from
'node:process'
6 import { fileURLToPath
} from
'node:url'
8 import chalk from
'chalk'
9 import { availableParallelism
} from
'poolifier'
11 import { waitChargingStationEvents
} from
'./Helpers.js'
12 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer.js'
13 import { UIServerFactory
} from
'./ui-server/UIServerFactory.js'
14 import { version
} from
'../../package.json'
15 import { BaseError
} from
'../exception/index.js'
16 import { type Storage
, StorageFactory
} from
'../performance/index.js'
18 type ChargingStationData
,
19 type ChargingStationWorkerData
,
20 type ChargingStationWorkerMessage
,
21 type ChargingStationWorkerMessageData
,
22 ChargingStationWorkerMessageEvents
,
25 type StationTemplateUrl
,
27 type StorageConfiguration
,
28 type UIServerConfiguration
,
29 type WorkerConfiguration
30 } from
'../types/index.js'
34 formatDurationMilliSeconds
,
36 handleUncaughtException
,
37 handleUnhandledRejection
,
42 } from
'../utils/index.js'
43 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
45 const moduleName
= 'Bootstrap'
49 missingChargingStationsConfiguration
= 1,
50 noChargingStationTemplates
= 2,
51 gracefulShutdownError
= 3
54 export class Bootstrap
extends EventEmitter
{
55 private static instance
: Bootstrap
| null = null
56 public numberOfChargingStations
!: number
57 public numberOfChargingStationTemplates
!: number
58 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
59 private readonly uiServer
?: AbstractUIServer
60 private storage
?: Storage
61 private numberOfStartedChargingStations
!: 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.initializedCounters
= false
80 this.initializeCounters()
81 this.uiServer
= UIServerFactory
.getUIServerImplementation(
82 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
84 Configuration
.configurationChangeCallback
= async () => {
85 await Bootstrap
.getInstance().restart(false)
89 public static getInstance (): Bootstrap
{
90 if (Bootstrap
.instance
=== null) {
91 Bootstrap
.instance
= new Bootstrap()
93 return Bootstrap
.instance
96 public async start (): Promise
<void> {
100 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
101 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
102 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
104 ChargingStationWorkerMessageEvents
.performanceStatistics
,
105 this.workerEventPerformanceStatistics
107 this.initializeCounters()
108 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
109 ConfigurationSection
.worker
111 this.initializeWorkerImplementation(workerConfiguration
)
112 await this.workerImplementation
?.start()
113 const performanceStorageConfiguration
=
114 Configuration
.getConfigurationSection
<StorageConfiguration
>(
115 ConfigurationSection
.performanceStorage
117 if (performanceStorageConfiguration
.enabled
=== true) {
118 this.storage
= StorageFactory
.getStorage(
119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120 performanceStorageConfiguration
.type!,
121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
122 performanceStorageConfiguration
.uri
!,
125 await this.storage
?.open()
127 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
128 .enabled
=== true && this.uiServer
?.start()
129 // Start ChargingStation object instance in worker thread
130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
131 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
133 const nbStations
= stationTemplateUrl
.numberOfStations
134 for (let index
= 1; index
<= nbStations
; index
++) {
135 await this.startChargingStation(index
, stationTemplateUrl
)
140 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
148 `Charging stations simulator ${
150 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
151 Configuration.workerDynamicPoolInUse()
152 ? `${workerConfiguration.poolMinSize?.toString()}
/`
154 }${this.workerImplementation?.size}${
155 Configuration.workerPoolInUse()
156 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
158 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
159 this.workerImplementation?.maxElementsPerWorker != null
160 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
165 Configuration
.workerDynamicPoolInUse() &&
168 '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'
171 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
173 this.starting
= false
175 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
178 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
182 public async stop (stopChargingStations
= true): Promise
<void> {
184 if (!this.stopping
) {
186 if (stopChargingStations
) {
187 await this.uiServer
?.sendInternalRequest(
188 this.uiServer
.buildProtocolRequest(
190 ProcedureName
.STOP_CHARGING_STATION
,
191 Constants
.EMPTY_FROZEN_OBJECT
195 await this.waitChargingStationsStopped()
197 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
200 await this.workerImplementation
?.stop()
201 delete this.workerImplementation
202 this.removeAllListeners()
203 await this.storage
?.close()
206 this.initializedCounters
= false
208 this.stopping
= false
210 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
213 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
217 public async restart (stopChargingStations
?: boolean): Promise
<void> {
218 await this.stop(stopChargingStations
)
219 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
220 .enabled
=== false && this.uiServer
?.stop()
224 private async waitChargingStationsStopped (): Promise
<string> {
225 return await new Promise
<string>((resolve
, reject
) => {
226 const waitTimeout
= setTimeout(() => {
227 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
228 Constants.STOP_CHARGING_STATIONS_TIMEOUT
229 )} reached at stopping charging stations`
230 console
.warn(chalk
.yellow(timeoutMessage
))
231 reject(new Error(timeoutMessage
))
232 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
233 waitChargingStationEvents(
235 ChargingStationWorkerMessageEvents
.stopped
,
236 this.numberOfStartedChargingStations
239 resolve('Charging stations stopped')
243 clearTimeout(waitTimeout
)
248 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
249 let elementsPerWorker
: number | undefined
250 switch (workerConfiguration
.elementsPerWorker
) {
253 this.numberOfChargingStations
> availableParallelism()
254 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
258 elementsPerWorker
= this.numberOfChargingStations
261 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
263 dirname(fileURLToPath(import.meta
.url
)),
264 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
266 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 workerConfiguration
.processType
!,
269 workerStartDelay
: workerConfiguration
.startDelay
,
270 elementStartDelay
: workerConfiguration
.elementStartDelay
,
271 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
272 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 poolMinSize
: workerConfiguration
.poolMinSize
!,
275 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
277 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
278 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
284 private messageHandler (
285 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
288 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
296 case ChargingStationWorkerMessageEvents
.started
:
297 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
299 case ChargingStationWorkerMessageEvents
.stopped
:
300 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
302 case ChargingStationWorkerMessageEvents
.updated
:
303 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
305 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
307 ChargingStationWorkerMessageEvents
.performanceStatistics
,
308 msg
.data
as Statistics
311 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
313 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
316 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
)
318 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
322 `Unknown charging station worker event: '${
324 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
329 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
337 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
338 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
339 ++this.numberOfStartedChargingStations
341 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
342 data.stationInfo.chargingStationId
343 } (hashId: ${data.stationInfo.hashId}) started (${
344 this.numberOfStartedChargingStations
345 } started from ${this.numberOfChargingStations})`
349 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
350 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
351 --this.numberOfStartedChargingStations
353 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
354 data.stationInfo.chargingStationId
355 } (hashId: ${data.stationInfo.hashId}) stopped (${
356 this.numberOfStartedChargingStations
357 } started from ${this.numberOfChargingStations})`
361 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
362 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
365 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
366 // eslint-disable-next-line @typescript-eslint/unbound-method
367 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
369 this.storage
.storePerformanceStatistics
as (
370 performanceStatistics
: Statistics
372 )(data
).catch(Constants
.EMPTY_FUNCTION
)
374 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
380 private initializeCounters (): void {
381 if (!this.initializedCounters
) {
383 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
385 if (isNotEmptyArray(stationTemplateUrls
)) {
386 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
387 for (const stationTemplateUrl
of stationTemplateUrls
) {
388 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
392 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
394 exit(exitCodes
.missingChargingStationsConfiguration
)
396 if (this.numberOfChargingStations
=== 0) {
397 console
.warn(chalk
.yellow('No charging station template enabled in configuration, exiting'))
398 exit(exitCodes
.noChargingStationTemplates
)
400 this.initializedCounters
= true
404 private resetCounters (): void {
405 this.numberOfChargingStationTemplates
= 0
406 this.numberOfChargingStations
= 0
407 this.numberOfStartedChargingStations
= 0
410 private async startChargingStation (
412 stationTemplateUrl
: StationTemplateUrl
414 await this.workerImplementation
?.addElement({
417 dirname(fileURLToPath(import.meta
.url
)),
420 stationTemplateUrl
.file
425 private gracefulShutdown (): void {
428 console
.info(chalk
.green('Graceful shutdown'))
429 this.uiServer
?.stop()
430 // stop() asks for charging stations to stop by default
431 this.waitChargingStationsStopped()
433 exit(exitCodes
.succeeded
)
436 exit(exitCodes
.gracefulShutdownError
)
440 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
441 exit(exitCodes
.gracefulShutdownError
)
445 private readonly logPrefix
= (): string => {
446 return logPrefix(' Bootstrap |')