1 // Partial Copyright Jerome Benoit. 2021-2023. 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
?? 0
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 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
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 message
= `Timeout ${formatDurationMilliSeconds(
228 Constants.STOP_CHARGING_STATIONS_TIMEOUT
229 )} reached at stopping charging stations`
230 console
.warn(chalk
.yellow(message
))
231 reject(new Error(message
))
232 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
233 waitChargingStationEvents(
235 ChargingStationWorkerMessageEvents
.stopped
,
236 this.numberOfChargingStations
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 this.storage
?.storePerformanceStatistics(data
) as undefined
369 private initializeCounters (): void {
370 if (!this.initializedCounters
) {
372 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
374 if (isNotEmptyArray(stationTemplateUrls
)) {
375 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
376 for (const stationTemplateUrl
of stationTemplateUrls
) {
377 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0
381 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
383 exit(exitCodes
.missingChargingStationsConfiguration
)
385 if (this.numberOfChargingStations
=== 0) {
386 console
.warn(chalk
.yellow('No charging station template enabled in configuration, exiting'))
387 exit(exitCodes
.noChargingStationTemplates
)
389 this.initializedCounters
= true
393 private resetCounters (): void {
394 this.numberOfChargingStationTemplates
= 0
395 this.numberOfChargingStations
= 0
396 this.numberOfStartedChargingStations
= 0
399 private async startChargingStation (
401 stationTemplateUrl
: StationTemplateUrl
403 await this.workerImplementation
?.addElement({
406 dirname(fileURLToPath(import.meta
.url
)),
409 stationTemplateUrl
.file
414 private gracefulShutdown (): void {
417 console
.info(`${chalk.green('Graceful shutdown')}`)
418 this.uiServer
?.stop()
419 // stop() asks for charging stations to stop by default
420 this.waitChargingStationsStopped()
422 exit(exitCodes
.succeeded
)
425 exit(exitCodes
.gracefulShutdownError
)
429 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
430 exit(exitCodes
.gracefulShutdownError
)
434 private readonly logPrefix
= (): string => {
435 return logPrefix(' Bootstrap |')