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
,
41 } from
'../utils/index.js'
42 import { type WorkerAbstract
, WorkerFactory
} from
'../worker/index.js'
44 const moduleName
= 'Bootstrap'
48 missingChargingStationsConfiguration
= 1,
49 noChargingStationTemplates
= 2,
50 gracefulShutdownError
= 3,
53 export class Bootstrap
extends EventEmitter
{
54 private static instance
: Bootstrap
| null = null
55 public numberOfChargingStations
!: number
56 public numberOfChargingStationTemplates
!: number
57 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
58 private readonly uiServer
?: AbstractUIServer
59 private storage
?: Storage
60 private numberOfStartedChargingStations
!: number
61 private readonly version
: string = version
62 private initializedCounters
: boolean
63 private started
: boolean
64 private starting
: boolean
65 private stopping
: boolean
67 private constructor () {
69 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
70 process
.on(signal
, this.gracefulShutdown
.bind(this))
72 // Enable unconditionally for now
73 handleUnhandledRejection()
74 handleUncaughtException()
78 this.initializedCounters
= false
79 this.initializeCounters()
80 this.uiServer
= UIServerFactory
.getUIServerImplementation(
81 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
83 Configuration
.configurationChangeCallback
= async () => {
84 await Bootstrap
.getInstance().restart(false)
88 public static getInstance (): Bootstrap
{
89 if (Bootstrap
.instance
=== null) {
90 Bootstrap
.instance
= new Bootstrap()
92 return Bootstrap
.instance
95 public async start (): Promise
<void> {
99 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
100 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
101 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
103 ChargingStationWorkerMessageEvents
.performanceStatistics
,
104 this.workerEventPerformanceStatistics
106 this.initializeCounters()
107 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
108 ConfigurationSection
.worker
110 this.initializeWorkerImplementation(workerConfiguration
)
111 await this.workerImplementation
?.start()
112 const performanceStorageConfiguration
=
113 Configuration
.getConfigurationSection
<StorageConfiguration
>(
114 ConfigurationSection
.performanceStorage
116 if (performanceStorageConfiguration
.enabled
=== true) {
117 this.storage
= StorageFactory
.getStorage(
118 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
119 performanceStorageConfiguration
.type!,
120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
121 performanceStorageConfiguration
.uri
!,
124 await this.storage
?.open()
126 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
127 .enabled
=== true && this.uiServer
?.start()
128 // Start ChargingStation object instance in worker thread
129 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
132 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0
133 for (let index
= 1; index
<= nbStations
; index
++) {
134 await this.startChargingStation(index
, stationTemplateUrl
)
139 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
147 `Charging stations simulator ${
149 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
150 Configuration.workerDynamicPoolInUse()
151 ? `${workerConfiguration.poolMinSize?.toString()}
/`
153 }${this.workerImplementation?.size}${
154 Configuration.workerPoolInUse()
155 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
157 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
158 this.workerImplementation?.maxElementsPerWorker != null
159 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
164 Configuration
.workerDynamicPoolInUse() &&
167 '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'
170 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
172 this.starting
= false
174 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
177 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
181 public async stop (stopChargingStations
= true): Promise
<void> {
183 if (!this.stopping
) {
185 if (stopChargingStations
) {
186 await this.uiServer
?.sendInternalRequest(
187 this.uiServer
.buildProtocolRequest(
189 ProcedureName
.STOP_CHARGING_STATION
,
190 Constants
.EMPTY_FROZEN_OBJECT
194 await this.waitChargingStationsStopped()
196 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
199 await this.workerImplementation
?.stop()
200 delete this.workerImplementation
201 this.removeAllListeners()
202 await this.storage
?.close()
205 this.initializedCounters
= false
207 this.stopping
= false
209 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
212 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
216 public async restart (stopChargingStations
?: boolean): Promise
<void> {
217 await this.stop(stopChargingStations
)
218 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
219 .enabled
=== false && this.uiServer
?.stop()
223 private async waitChargingStationsStopped (): Promise
<string> {
224 return await new Promise
<string>((resolve
, reject
) => {
225 const waitTimeout
= setTimeout(() => {
226 const message
= `Timeout ${formatDurationMilliSeconds(
227 Constants.STOP_CHARGING_STATIONS_TIMEOUT
228 )} reached at stopping charging stations`
229 console
.warn(chalk
.yellow(message
))
230 reject(new Error(message
))
231 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
232 waitChargingStationEvents(
234 ChargingStationWorkerMessageEvents
.stopped
,
235 this.numberOfChargingStations
238 resolve('Charging stations stopped')
242 clearTimeout(waitTimeout
)
247 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
248 let elementsPerWorker
: number | undefined
249 switch (workerConfiguration
?.elementsPerWorker
) {
252 this.numberOfChargingStations
> availableParallelism()
253 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
257 elementsPerWorker
= this.numberOfChargingStations
260 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
262 dirname(fileURLToPath(import.meta
.url
)),
263 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
265 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 workerConfiguration
.processType
!,
268 workerStartDelay
: workerConfiguration
.startDelay
,
269 elementStartDelay
: workerConfiguration
.elementStartDelay
,
270 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273 poolMinSize
: workerConfiguration
.poolMinSize
!,
274 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
276 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
277 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
283 private messageHandler (
284 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
287 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
295 case ChargingStationWorkerMessageEvents
.started
:
296 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
298 case ChargingStationWorkerMessageEvents
.stopped
:
299 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
301 case ChargingStationWorkerMessageEvents
.updated
:
302 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
304 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
306 ChargingStationWorkerMessageEvents
.performanceStatistics
,
307 msg
.data
as Statistics
310 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
312 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
315 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
)
317 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
321 `Unknown charging station worker event: '${
323 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
328 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
336 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
337 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
338 ++this.numberOfStartedChargingStations
340 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
341 data.stationInfo.chargingStationId
342 } (hashId: ${data.stationInfo.hashId}) started (${
343 this.numberOfStartedChargingStations
344 } started from ${this.numberOfChargingStations})`
348 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
349 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
350 --this.numberOfStartedChargingStations
352 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
353 data.stationInfo.chargingStationId
354 } (hashId: ${data.stationInfo.hashId}) stopped (${
355 this.numberOfStartedChargingStations
356 } started from ${this.numberOfChargingStations})`
360 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
361 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
364 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
365 this.storage
?.storePerformanceStatistics(data
) as undefined
368 private initializeCounters (): void {
369 if (!this.initializedCounters
) {
371 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
372 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
373 if (isNotEmptyArray(stationTemplateUrls
)) {
374 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
375 for (const stationTemplateUrl
of stationTemplateUrls
) {
376 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0
380 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
382 exit(exitCodes
.missingChargingStationsConfiguration
)
384 if (this.numberOfChargingStations
=== 0) {
385 console
.warn(chalk
.yellow('No charging station template enabled in configuration, exiting'))
386 exit(exitCodes
.noChargingStationTemplates
)
388 this.initializedCounters
= true
392 private resetCounters (): void {
393 this.numberOfChargingStationTemplates
= 0
394 this.numberOfChargingStations
= 0
395 this.numberOfStartedChargingStations
= 0
398 private async startChargingStation (
400 stationTemplateUrl
: StationTemplateUrl
402 await this.workerImplementation
?.addElement({
405 dirname(fileURLToPath(import.meta
.url
)),
408 stationTemplateUrl
.file
413 private gracefulShutdown (): void {
416 console
.info(`${chalk.green('Graceful shutdown')}`)
417 this.uiServer
?.stop()
418 // stop() asks for charging stations to stop by default
419 this.waitChargingStationsStopped()
421 exit(exitCodes
.succeeded
)
424 exit(exitCodes
.gracefulShutdownError
)
428 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
429 exit(exitCodes
.gracefulShutdownError
)
433 private readonly logPrefix
= (): string => {
434 return logPrefix(' Bootstrap |')