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'
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 noChargingStationTemplates
= 2,
52 gracefulShutdownError
= 3
55 export class Bootstrap
extends EventEmitter
{
56 private static instance
: Bootstrap
| null = null
57 public numberOfChargingStations
!: number
58 public numberOfChargingStationTemplates
!: number
59 private workerImplementation
?: WorkerAbstract
<ChargingStationWorkerData
>
60 private readonly uiServer
?: AbstractUIServer
61 private storage
?: Storage
62 private numberOfStartedChargingStations
!: number
63 private readonly version
: string = version
64 private initializedCounters
: boolean
65 private started
: boolean
66 private starting
: boolean
67 private stopping
: boolean
69 private constructor () {
71 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
72 process
.on(signal
, this.gracefulShutdown
.bind(this))
74 // Enable unconditionally for now
75 handleUnhandledRejection()
76 handleUncaughtException()
80 this.initializedCounters
= false
81 this.initializeCounters()
82 this.uiServer
= UIServerFactory
.getUIServerImplementation(
83 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
85 Configuration
.configurationChangeCallback
= async () => {
86 await Bootstrap
.getInstance().restart(false)
90 public static getInstance (): Bootstrap
{
91 if (Bootstrap
.instance
=== null) {
92 Bootstrap
.instance
= new Bootstrap()
94 return Bootstrap
.instance
97 public async start (): Promise
<void> {
101 this.on(ChargingStationWorkerMessageEvents
.started
, this.workerEventStarted
)
102 this.on(ChargingStationWorkerMessageEvents
.stopped
, this.workerEventStopped
)
103 this.on(ChargingStationWorkerMessageEvents
.updated
, this.workerEventUpdated
)
105 ChargingStationWorkerMessageEvents
.performanceStatistics
,
106 this.workerEventPerformanceStatistics
108 this.initializeCounters()
109 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
110 ConfigurationSection
.worker
112 this.initializeWorkerImplementation(workerConfiguration
)
113 await this.workerImplementation
?.start()
114 const performanceStorageConfiguration
=
115 Configuration
.getConfigurationSection
<StorageConfiguration
>(
116 ConfigurationSection
.performanceStorage
118 if (performanceStorageConfiguration
.enabled
=== true) {
119 this.storage
= StorageFactory
.getStorage(
120 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
121 performanceStorageConfiguration
.type!,
122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123 performanceStorageConfiguration
.uri
!,
126 await this.storage
?.open()
128 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
129 .enabled
=== true && this.uiServer
?.start()
130 // Start ChargingStation object instance in worker thread
131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
132 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
134 const nbStations
= stationTemplateUrl
.numberOfStations
135 for (let index
= 1; index
<= nbStations
; index
++) {
136 await this.startChargingStation(index
, stationTemplateUrl
)
141 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
149 `Charging stations simulator ${
151 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
152 Configuration.workerDynamicPoolInUse()
153 ? `${workerConfiguration.poolMinSize?.toString()}
/`
155 }${this.workerImplementation?.size}${
156 Configuration.workerPoolInUse()
157 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
159 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
160 this.workerImplementation?.maxElementsPerWorker != null
161 ? ` (${this.workerImplementation.maxElementsPerWorker} charging
station(s
) per worker
)`
166 Configuration
.workerDynamicPoolInUse() &&
169 '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'
172 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
)
174 this.starting
= false
176 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'))
179 console
.error(chalk
.red('Cannot start an already started charging stations simulator'))
183 public async stop (stopChargingStations
= true): Promise
<void> {
185 if (!this.stopping
) {
187 if (stopChargingStations
) {
188 await this.uiServer
?.sendInternalRequest(
189 this.uiServer
.buildProtocolRequest(
191 ProcedureName
.STOP_CHARGING_STATION
,
192 Constants
.EMPTY_FROZEN_OBJECT
196 await this.waitChargingStationsStopped()
198 console
.error(chalk
.red('Error while waiting for charging stations to stop: '), error
)
201 await this.workerImplementation
?.stop()
202 delete this.workerImplementation
203 this.removeAllListeners()
204 await this.storage
?.close()
207 this.initializedCounters
= false
209 this.stopping
= false
211 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'))
214 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'))
218 public async restart (stopChargingStations
?: boolean): Promise
<void> {
219 await this.stop(stopChargingStations
)
220 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
221 .enabled
=== false && this.uiServer
?.stop()
225 private async waitChargingStationsStopped (): Promise
<string> {
226 return await new Promise
<string>((resolve
, reject
) => {
227 const waitTimeout
= setTimeout(() => {
228 const timeoutMessage
= `Timeout ${formatDurationMilliSeconds(
229 Constants.STOP_CHARGING_STATIONS_TIMEOUT
230 )} reached at stopping charging stations`
231 console
.warn(chalk
.yellow(timeoutMessage
))
232 reject(new Error(timeoutMessage
))
233 }, Constants
.STOP_CHARGING_STATIONS_TIMEOUT
)
234 waitChargingStationEvents(
236 ChargingStationWorkerMessageEvents
.stopped
,
237 this.numberOfStartedChargingStations
240 resolve('Charging stations stopped')
244 clearTimeout(waitTimeout
)
249 private initializeWorkerImplementation (workerConfiguration
: WorkerConfiguration
): void {
250 let elementsPerWorker
: number | undefined
251 switch (workerConfiguration
.elementsPerWorker
) {
254 this.numberOfChargingStations
> availableParallelism()
255 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
259 elementsPerWorker
= this.numberOfChargingStations
262 this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
264 dirname(fileURLToPath(import.meta
.url
)),
265 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
267 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
268 workerConfiguration
.processType
!,
270 workerStartDelay
: workerConfiguration
.startDelay
,
271 elementStartDelay
: workerConfiguration
.elementStartDelay
,
272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
274 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
275 poolMinSize
: workerConfiguration
.poolMinSize
!,
276 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
278 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
279 workerOptions
: { resourceLimits
: workerConfiguration
.resourceLimits
}
285 private messageHandler (
286 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
289 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
297 case ChargingStationWorkerMessageEvents
.started
:
298 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
)
300 case ChargingStationWorkerMessageEvents
.stopped
:
301 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
)
303 case ChargingStationWorkerMessageEvents
.updated
:
304 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
)
306 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
308 ChargingStationWorkerMessageEvents
.performanceStatistics
,
309 msg
.data
as Statistics
312 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
314 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while starting worker element:`,
317 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
)
319 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
323 `Unknown charging station worker event: '${
325 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
330 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
338 private readonly workerEventStarted
= (data
: ChargingStationData
): void => {
339 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
340 ++this.numberOfStartedChargingStations
342 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
343 data.stationInfo.chargingStationId
344 } (hashId: ${data.stationInfo.hashId}) started (${
345 this.numberOfStartedChargingStations
346 } started from ${this.numberOfChargingStations})`
350 private readonly workerEventStopped
= (data
: ChargingStationData
): void => {
351 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
352 --this.numberOfStartedChargingStations
354 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
355 data.stationInfo.chargingStationId
356 } (hashId: ${data.stationInfo.hashId}) stopped (${
357 this.numberOfStartedChargingStations
358 } started from ${this.numberOfChargingStations})`
362 private readonly workerEventUpdated
= (data
: ChargingStationData
): void => {
363 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
)
366 private readonly workerEventPerformanceStatistics
= (data
: Statistics
): void => {
367 // eslint-disable-next-line @typescript-eslint/unbound-method
368 if (isAsyncFunction(this.storage
?.storePerformanceStatistics
)) {
370 this.storage
.storePerformanceStatistics
as (
371 performanceStatistics
: Statistics
373 )(data
).catch(Constants
.EMPTY_FUNCTION
)
375 (this.storage
?.storePerformanceStatistics
as (performanceStatistics
: Statistics
) => void)(
381 private initializeCounters (): void {
382 if (!this.initializedCounters
) {
384 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
385 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!
386 if (isNotEmptyArray(stationTemplateUrls
)) {
387 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
388 for (const stationTemplateUrl
of stationTemplateUrls
) {
389 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
393 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
395 exit(exitCodes
.missingChargingStationsConfiguration
)
397 if (this.numberOfChargingStations
=== 0) {
398 console
.warn(chalk
.yellow('No charging station template enabled in configuration, exiting'))
399 exit(exitCodes
.noChargingStationTemplates
)
401 this.initializedCounters
= true
405 private resetCounters (): void {
406 this.numberOfChargingStationTemplates
= 0
407 this.numberOfChargingStations
= 0
408 this.numberOfStartedChargingStations
= 0
411 private async startChargingStation (
413 stationTemplateUrl
: StationTemplateUrl
415 await this.workerImplementation
?.addElement({
418 dirname(fileURLToPath(import.meta
.url
)),
421 stationTemplateUrl
.file
426 private gracefulShutdown (): void {
429 console
.info(chalk
.green('Graceful shutdown'))
430 this.uiServer
?.stop()
431 // stop() asks for charging stations to stop by default
432 this.waitChargingStationsStopped()
434 exit(exitCodes
.succeeded
)
437 exit(exitCodes
.gracefulShutdownError
)
441 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
)
442 exit(exitCodes
.gracefulShutdownError
)
446 private readonly logPrefix
= (): string => {
447 return logPrefix(' Bootstrap |')