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 { 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';
12 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
13 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
14 import { version
} from
'../../package.json';
15 import { BaseError
} from
'../exception';
16 import { type Storage
, StorageFactory
} from
'../performance';
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
,
34 formatDurationMilliSeconds
,
36 handleUncaughtException
,
37 handleUnhandledRejection
,
43 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
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
> | null;
59 private readonly uiServer
!: AbstractUIServer
| null;
60 private readonly 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;
67 private readonly workerScript
: string;
69 private constructor() {
71 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
72 process
.on(signal
, this.gracefulShutdown
);
74 // Enable unconditionally for now
75 handleUnhandledRejection();
76 handleUncaughtException();
78 this.starting
= false;
79 this.stopping
= false;
80 this.initializedCounters
= false;
81 this.initializeCounters();
82 this.workerImplementation
= null;
83 this.workerScript
= join(
84 dirname(fileURLToPath(import.meta
.url
)),
85 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
87 const uiServerConfiguration
= Configuration
.getConfigurationSection
<UIServerConfiguration
>(
88 ConfigurationSection
.uiServer
,
90 uiServerConfiguration
.enabled
=== true &&
91 (this.uiServer
= UIServerFactory
.getUIServerImplementation(uiServerConfiguration
));
92 const performanceStorageConfiguration
=
93 Configuration
.getConfigurationSection
<StorageConfiguration
>(
94 ConfigurationSection
.performanceStorage
,
96 performanceStorageConfiguration
.enabled
=== true &&
97 (this.storage
= StorageFactory
.getStorage(
98 performanceStorageConfiguration
.type!,
99 performanceStorageConfiguration
.uri
!,
102 Configuration
.configurationChangeCallback
= async () => Bootstrap
.getInstance().restart();
105 public static getInstance(): Bootstrap
{
106 if (Bootstrap
.instance
=== null) {
107 Bootstrap
.instance
= new Bootstrap();
109 return Bootstrap
.instance
;
112 public async start(): Promise
<void> {
113 if (this.started
=== false) {
114 if (this.starting
=== false) {
115 this.starting
= true;
116 this.initializeCounters();
117 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
118 ConfigurationSection
.worker
,
120 this.initializeWorkerImplementation(workerConfiguration
);
121 await this.workerImplementation
?.start();
122 await this.storage
?.open();
123 this.uiServer
?.start();
124 // Start ChargingStation object instance in worker thread
125 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
127 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
128 for (let index
= 1; index
<= nbStations
; index
++) {
129 await this.startChargingStation(index
, stationTemplateUrl
);
134 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
142 `Charging stations simulator ${
144 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
145 Configuration.workerDynamicPoolInUse()
146 ? `${workerConfiguration.poolMinSize?.toString()}
/`
148 }${this.workerImplementation?.size}${
149 Configuration.workerPoolInUse()
150 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
152 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
153 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
154 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
159 Configuration
.workerDynamicPoolInUse() &&
162 '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',
165 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
167 this.starting
= false;
169 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
172 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
176 public async stop(): Promise
<void> {
177 if (this.started
=== true) {
178 if (this.stopping
=== false) {
179 this.stopping
= true;
180 await this.uiServer
?.sendInternalRequest(
181 this.uiServer
.buildProtocolRequest(
183 ProcedureName
.STOP_CHARGING_STATION
,
184 Constants
.EMPTY_FROZEN_OBJECT
,
188 waitChargingStationEvents(
190 ChargingStationWorkerMessageEvents
.stopped
,
191 this.numberOfChargingStations
,
193 new Promise
<string>((resolve
) => {
195 const message
= `Timeout ${formatDurationMilliSeconds(
196 Constants.STOP_SIMULATOR_TIMEOUT,
197 )} reached at stopping charging stations simulator`;
198 console
.warn(chalk
.yellow(message
));
200 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
203 await this.workerImplementation
?.stop();
204 this.workerImplementation
= null;
205 this.uiServer
?.stop();
206 await this.storage
?.close();
207 this.resetCounters();
208 this.initializedCounters
= false;
209 this.started
= false;
210 this.stopping
= false;
212 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
215 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
219 public async restart(): Promise
<void> {
224 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
225 let elementsPerWorker
: number | undefined;
226 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
228 this.numberOfChargingStations
> availableParallelism()
229 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
232 this.workerImplementation
=== null &&
233 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
235 workerConfiguration
.processType
!,
237 workerStartDelay
: workerConfiguration
.startDelay
,
238 elementStartDelay
: workerConfiguration
.elementStartDelay
,
239 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
240 poolMinSize
: workerConfiguration
.poolMinSize
!,
241 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
243 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
249 private messageHandler(
250 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
253 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
261 case ChargingStationWorkerMessageEvents
.started
:
262 this.workerEventStarted(msg
.data
as ChargingStationData
);
263 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
265 case ChargingStationWorkerMessageEvents
.stopped
:
266 this.workerEventStopped(msg
.data
as ChargingStationData
);
267 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
269 case ChargingStationWorkerMessageEvents
.updated
:
270 this.workerEventUpdated(msg
.data
as ChargingStationData
);
271 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
273 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
274 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
276 ChargingStationWorkerMessageEvents
.performanceStatistics
,
277 msg
.data
as Statistics
,
280 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
282 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
285 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
287 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
291 `Unknown charging station worker event: '${
293 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
298 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
306 private workerEventStarted
= (data
: ChargingStationData
) => {
307 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
308 ++this.numberOfStartedChargingStations
;
310 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
311 data.stationInfo.chargingStationId
312 } (hashId: ${data.stationInfo.hashId}) started (${
313 this.numberOfStartedChargingStations
314 } started from ${this.numberOfChargingStations})`,
318 private workerEventStopped
= (data
: ChargingStationData
) => {
319 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
320 --this.numberOfStartedChargingStations
;
322 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
323 data.stationInfo.chargingStationId
324 } (hashId: ${data.stationInfo.hashId}) stopped (${
325 this.numberOfStartedChargingStations
326 } started from ${this.numberOfChargingStations})`,
330 private workerEventUpdated
= (data
: ChargingStationData
) => {
331 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
334 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
335 this.storage
.storePerformanceStatistics(data
) as void;
338 private initializeCounters() {
339 if (this.initializedCounters
=== false) {
340 this.resetCounters();
341 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
342 if (isNotEmptyArray(stationTemplateUrls
)) {
343 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
344 for (const stationTemplateUrl
of stationTemplateUrls
) {
345 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
349 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
351 exit(exitCodes
.missingChargingStationsConfiguration
);
353 if (this.numberOfChargingStations
=== 0) {
355 chalk
.yellow('No charging station template enabled in configuration, exiting'),
357 exit(exitCodes
.noChargingStationTemplates
);
359 this.initializedCounters
= true;
363 private resetCounters(): void {
364 this.numberOfChargingStationTemplates
= 0;
365 this.numberOfChargingStations
= 0;
366 this.numberOfStartedChargingStations
= 0;
369 private async startChargingStation(
371 stationTemplateUrl
: StationTemplateUrl
,
373 await this.workerImplementation
?.addElement({
376 dirname(fileURLToPath(import.meta
.url
)),
379 stationTemplateUrl
.file
,
384 private gracefulShutdown
= (): void => {
387 console
.info(`${chalk.green('Graceful shutdown')}`);
388 exit(exitCodes
.succeeded
);
391 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
392 exit(exitCodes
.gracefulShutdownError
);
396 private logPrefix
= (): string => {
397 return logPrefix(' Bootstrap |');