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';
10 import { waitForChargingStationEvents
} from
'./ChargingStationUtils';
11 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
12 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
13 import { version
} from
'../../package.json' assert
{ type: 'json' };
14 import { BaseError
} from
'../exception';
15 import { type Storage
, StorageFactory
} from
'../performance';
17 type ChargingStationData
,
18 type ChargingStationWorkerData
,
19 type ChargingStationWorkerMessage
,
20 type ChargingStationWorkerMessageData
,
21 ChargingStationWorkerMessageEvents
,
23 type StationTemplateUrl
,
29 formatDurationMilliSeconds
,
31 handleUncaughtException
,
32 handleUnhandledRejection
,
38 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
40 const moduleName
= 'Bootstrap';
43 missingChargingStationsConfiguration
= 1,
44 noChargingStationTemplates
= 2,
47 export class Bootstrap
extends EventEmitter
{
48 private static instance
: Bootstrap
| null = null;
49 public numberOfChargingStations
!: number;
50 public numberOfChargingStationTemplates
!: number;
51 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
52 private readonly uiServer
!: AbstractUIServer
| null;
53 private readonly storage
!: Storage
;
54 private numberOfStartedChargingStations
!: number;
55 private readonly version
: string = version
;
56 private initializedCounters
: boolean;
57 private started
: boolean;
58 private starting
: boolean;
59 private stopping
: boolean;
60 private readonly workerScript
: string;
62 private constructor() {
64 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
65 process
.on(signal
, this.gracefulShutdown
);
67 // Enable unconditionally for now
68 handleUnhandledRejection();
69 handleUncaughtException();
71 this.starting
= false;
72 this.stopping
= false;
73 this.initializedCounters
= false;
74 this.initializeCounters();
75 this.workerImplementation
= null;
76 this.workerScript
= join(
77 dirname(fileURLToPath(import.meta
.url
)),
78 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
80 Configuration
.getUIServer().enabled
=== true &&
81 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
82 Configuration
.getPerformanceStorage().enabled
=== true &&
83 (this.storage
= StorageFactory
.getStorage(
84 Configuration
.getPerformanceStorage().type,
85 Configuration
.getPerformanceStorage().uri
,
88 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
91 public static getInstance(): Bootstrap
{
92 if (Bootstrap
.instance
=== null) {
93 Bootstrap
.instance
= new Bootstrap();
95 return Bootstrap
.instance
;
98 public async start(): Promise
<void> {
100 throw new Error('Cannot start charging stations simulator from worker thread');
102 if (this.started
=== false) {
103 if (this.starting
=== false) {
104 this.starting
= true;
105 this.initializeCounters();
106 this.initializeWorkerImplementation();
107 await this.workerImplementation
?.start();
108 await this.storage
?.open();
109 this.uiServer
?.start();
110 // Start ChargingStation object instance in worker thread
111 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
113 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
114 for (let index
= 1; index
<= nbStations
; index
++) {
115 await this.startChargingStation(index
, stationTemplateUrl
);
120 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
128 `Charging stations simulator ${
130 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
131 Configuration.workerDynamicPoolInUse()
132 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
134 }${this.workerImplementation?.size}${
135 Configuration.workerPoolInUse()
136 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
138 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
139 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
140 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
145 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
147 this.starting
= false;
149 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
152 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
156 public async stop(): Promise
<void> {
158 throw new Error('Cannot stop charging stations simulator from worker thread');
160 if (this.started
=== true) {
161 if (this.stopping
=== false) {
162 this.stopping
= true;
163 await this.uiServer
?.sendInternalRequest(
164 this.uiServer
.buildProtocolRequest(
166 ProcedureName
.STOP_CHARGING_STATION
,
167 Constants
.EMPTY_FREEZED_OBJECT
171 waitForChargingStationEvents(
173 ChargingStationWorkerMessageEvents
.stopped
,
174 this.numberOfChargingStations
176 new Promise
<string>((resolve
) => {
178 const message
= `Timeout reached ${formatDurationMilliSeconds(
179 Constants.STOP_SIMULATOR_TIMEOUT
180 )} at stopping charging stations simulator`;
181 console
.warn(chalk
.yellow(message
));
183 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
186 await this.workerImplementation
?.stop();
187 this.workerImplementation
= null;
188 this.uiServer
?.stop();
189 await this.storage
?.close();
190 this.resetCounters();
191 this.initializedCounters
= false;
192 this.started
= false;
193 this.stopping
= false;
195 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
198 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
202 public async restart(): Promise
<void> {
207 private initializeWorkerImplementation(): void {
208 this.workerImplementation
=== null &&
209 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
211 Configuration
.getWorker().processType
,
213 workerStartDelay
: Configuration
.getWorker().startDelay
,
214 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
215 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
216 poolMinSize
: Configuration
.getWorker().poolMinSize
,
217 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
219 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
220 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
226 private messageHandler(
227 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
230 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
238 case ChargingStationWorkerMessageEvents
.started
:
239 this.workerEventStarted(msg
.data
as ChargingStationData
);
240 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
242 case ChargingStationWorkerMessageEvents
.stopped
:
243 this.workerEventStopped(msg
.data
as ChargingStationData
);
244 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
246 case ChargingStationWorkerMessageEvents
.updated
:
247 this.workerEventUpdated(msg
.data
as ChargingStationData
);
248 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
250 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
251 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
253 ChargingStationWorkerMessageEvents
.performanceStatistics
,
254 msg
.data
as Statistics
259 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
264 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
272 private workerEventStarted
= (data
: ChargingStationData
) => {
273 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
274 ++this.numberOfStartedChargingStations
;
276 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
277 data.stationInfo.chargingStationId
278 } (hashId: ${data.stationInfo.hashId}) started (${
279 this.numberOfStartedChargingStations
280 } started from ${this.numberOfChargingStations})`
284 private workerEventStopped
= (data
: ChargingStationData
) => {
285 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
286 --this.numberOfStartedChargingStations
;
288 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
289 data.stationInfo.chargingStationId
290 } (hashId: ${data.stationInfo.hashId}) stopped (${
291 this.numberOfStartedChargingStations
292 } started from ${this.numberOfChargingStations})`
296 private workerEventUpdated
= (data
: ChargingStationData
) => {
297 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
300 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
301 this.storage
.storePerformanceStatistics(data
) as void;
304 private initializeCounters() {
305 if (this.initializedCounters
=== false) {
306 this.resetCounters();
307 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
308 if (isNotEmptyArray(stationTemplateUrls
)) {
309 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
310 for (const stationTemplateUrl
of stationTemplateUrls
) {
311 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
315 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
317 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
319 if (this.numberOfChargingStations
=== 0) {
321 chalk
.yellow('No charging station template enabled in configuration, exiting')
323 process
.exit(exitCodes
.noChargingStationTemplates
);
325 this.initializedCounters
= true;
329 private resetCounters(): void {
330 this.numberOfChargingStationTemplates
= 0;
331 this.numberOfChargingStations
= 0;
332 this.numberOfStartedChargingStations
= 0;
335 private async startChargingStation(
337 stationTemplateUrl
: StationTemplateUrl
339 await this.workerImplementation
?.addElement({
342 dirname(fileURLToPath(import.meta
.url
)),
345 stationTemplateUrl
.file
350 private gracefulShutdown
= (): void => {
351 console
.info(`${chalk.green('Graceful shutdown')}`);
357 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
362 private logPrefix
= (): string => {
363 return logPrefix(' Bootstrap |');