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 { fileURLToPath
} from
'node:url';
6 import { isMainThread
} from
'node:worker_threads';
8 import chalk from
'chalk';
9 import { availableParallelism
} from
'poolifier';
11 import { waitChargingStationEvents
} from
'./ChargingStationUtils';
12 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
13 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
14 import { version
} from
'../../package.json' assert
{ type: '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';
48 missingChargingStationsConfiguration
= 1,
49 noChargingStationTemplates
= 2,
52 export class Bootstrap
extends EventEmitter
{
53 private static instance
: Bootstrap
| null = null;
54 public numberOfChargingStations
!: number;
55 public numberOfChargingStationTemplates
!: number;
56 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
57 private readonly uiServer
!: AbstractUIServer
| null;
58 private readonly storage
!: Storage
;
59 private numberOfStartedChargingStations
!: number;
60 private readonly version
: string = version
;
61 private initializedCounters
: boolean;
62 private started
: boolean;
63 private starting
: boolean;
64 private stopping
: boolean;
65 private readonly workerScript
: string;
67 private constructor() {
69 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
70 process
.on(signal
, this.gracefulShutdown
);
72 // Enable unconditionally for now
73 handleUnhandledRejection();
74 handleUncaughtException();
76 this.starting
= false;
77 this.stopping
= false;
78 this.initializedCounters
= false;
79 this.initializeCounters();
80 this.workerImplementation
= null;
81 this.workerScript
= join(
82 dirname(fileURLToPath(import.meta
.url
)),
83 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
85 const uiServerConfiguration
= Configuration
.getConfigurationSection
<UIServerConfiguration
>(
86 ConfigurationSection
.uiServer
,
88 uiServerConfiguration
.enabled
=== true &&
89 (this.uiServer
= UIServerFactory
.getUIServerImplementation(uiServerConfiguration
));
90 const performanceStorageConfiguration
=
91 Configuration
.getConfigurationSection
<StorageConfiguration
>(
92 ConfigurationSection
.performanceStorage
,
94 performanceStorageConfiguration
.enabled
=== true &&
95 (this.storage
= StorageFactory
.getStorage(
96 performanceStorageConfiguration
.type!,
97 performanceStorageConfiguration
.uri
!,
100 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
103 public static getInstance(): Bootstrap
{
104 if (Bootstrap
.instance
=== null) {
105 Bootstrap
.instance
= new Bootstrap();
107 return Bootstrap
.instance
;
110 public async start(): Promise
<void> {
112 throw new Error('Cannot start charging stations simulator from worker thread');
114 if (this.started
=== false) {
115 if (this.starting
=== false) {
116 this.starting
= true;
117 this.initializeCounters();
118 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
119 ConfigurationSection
.worker
,
121 this.initializeWorkerImplementation(workerConfiguration
);
122 await this.workerImplementation
?.start();
123 await this.storage
?.open();
124 this.uiServer
?.start();
125 // Start ChargingStation object instance in worker thread
126 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
128 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
129 for (let index
= 1; index
<= nbStations
; index
++) {
130 await this.startChargingStation(index
, stationTemplateUrl
);
135 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
143 `Charging stations simulator ${
145 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
146 Configuration.workerDynamicPoolInUse()
147 ? `${workerConfiguration.poolMinSize?.toString()}
/`
149 }${this.workerImplementation?.size}${
150 Configuration.workerPoolInUse()
151 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
153 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
154 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
155 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
160 Configuration
.workerDynamicPoolInUse() &&
163 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using static pool or worker set mode instead',
166 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
168 this.starting
= false;
170 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
173 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
177 public async stop(): Promise
<void> {
179 throw new Error('Cannot stop charging stations simulator from worker thread');
181 if (this.started
=== true) {
182 if (this.stopping
=== false) {
183 this.stopping
= true;
184 await this.uiServer
?.sendInternalRequest(
185 this.uiServer
.buildProtocolRequest(
187 ProcedureName
.STOP_CHARGING_STATION
,
188 Constants
.EMPTY_FREEZED_OBJECT
,
192 waitChargingStationEvents(
194 ChargingStationWorkerMessageEvents
.stopped
,
195 this.numberOfChargingStations
,
197 new Promise
<string>((resolve
) => {
199 const message
= `Timeout reached ${formatDurationMilliSeconds(
200 Constants.STOP_SIMULATOR_TIMEOUT,
201 )} at stopping charging stations simulator`;
202 console
.warn(chalk
.yellow(message
));
204 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
207 await this.workerImplementation
?.stop();
208 this.workerImplementation
= null;
209 this.uiServer
?.stop();
210 await this.storage
?.close();
211 this.resetCounters();
212 this.initializedCounters
= false;
213 this.started
= false;
214 this.stopping
= false;
216 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
219 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
223 public async restart(): Promise
<void> {
228 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
229 let elementsPerWorker
: number | undefined;
230 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
232 this.numberOfChargingStations
> availableParallelism()
233 ? Math.round(this.numberOfChargingStations
/ availableParallelism())
236 this.workerImplementation
=== null &&
237 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
239 workerConfiguration
.processType
!,
241 workerStartDelay
: workerConfiguration
.startDelay
,
242 elementStartDelay
: workerConfiguration
.elementStartDelay
,
243 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
244 poolMinSize
: workerConfiguration
.poolMinSize
!,
245 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
247 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
253 private messageHandler(
254 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
257 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
265 case ChargingStationWorkerMessageEvents
.started
:
266 this.workerEventStarted(msg
.data
as ChargingStationData
);
267 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
269 case ChargingStationWorkerMessageEvents
.stopped
:
270 this.workerEventStopped(msg
.data
as ChargingStationData
);
271 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
273 case ChargingStationWorkerMessageEvents
.updated
:
274 this.workerEventUpdated(msg
.data
as ChargingStationData
);
275 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
277 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
278 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
280 ChargingStationWorkerMessageEvents
.performanceStatistics
,
281 msg
.data
as Statistics
,
286 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`,
291 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
299 private workerEventStarted
= (data
: ChargingStationData
) => {
300 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
301 ++this.numberOfStartedChargingStations
;
303 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
304 data.stationInfo.chargingStationId
305 } (hashId: ${data.stationInfo.hashId}) started (${
306 this.numberOfStartedChargingStations
307 } started from ${this.numberOfChargingStations})`,
311 private workerEventStopped
= (data
: ChargingStationData
) => {
312 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
313 --this.numberOfStartedChargingStations
;
315 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
316 data.stationInfo.chargingStationId
317 } (hashId: ${data.stationInfo.hashId}) stopped (${
318 this.numberOfStartedChargingStations
319 } started from ${this.numberOfChargingStations})`,
323 private workerEventUpdated
= (data
: ChargingStationData
) => {
324 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
327 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
328 this.storage
.storePerformanceStatistics(data
) as void;
331 private initializeCounters() {
332 if (this.initializedCounters
=== false) {
333 this.resetCounters();
334 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
335 if (isNotEmptyArray(stationTemplateUrls
)) {
336 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
337 for (const stationTemplateUrl
of stationTemplateUrls
) {
338 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
342 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
344 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
346 if (this.numberOfChargingStations
=== 0) {
348 chalk
.yellow('No charging station template enabled in configuration, exiting'),
350 process
.exit(exitCodes
.noChargingStationTemplates
);
352 this.initializedCounters
= true;
356 private resetCounters(): void {
357 this.numberOfChargingStationTemplates
= 0;
358 this.numberOfChargingStations
= 0;
359 this.numberOfStartedChargingStations
= 0;
362 private async startChargingStation(
364 stationTemplateUrl
: StationTemplateUrl
,
366 await this.workerImplementation
?.addElement({
369 dirname(fileURLToPath(import.meta
.url
)),
372 stationTemplateUrl
.file
,
377 private gracefulShutdown
= (): void => {
378 console
.info(`${chalk.green('Graceful shutdown')}`);
384 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
389 private logPrefix
= (): string => {
390 return logPrefix(' Bootstrap |');