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 { waitChargingStationEvents
} 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 Configuration
.workerDynamicPoolInUse() &&
148 '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',
151 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
153 this.starting
= false;
155 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
158 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
162 public async stop(): Promise
<void> {
164 throw new Error('Cannot stop charging stations simulator from worker thread');
166 if (this.started
=== true) {
167 if (this.stopping
=== false) {
168 this.stopping
= true;
169 await this.uiServer
?.sendInternalRequest(
170 this.uiServer
.buildProtocolRequest(
172 ProcedureName
.STOP_CHARGING_STATION
,
173 Constants
.EMPTY_FREEZED_OBJECT
,
177 waitChargingStationEvents(
179 ChargingStationWorkerMessageEvents
.stopped
,
180 this.numberOfChargingStations
,
182 new Promise
<string>((resolve
) => {
184 const message
= `Timeout reached ${formatDurationMilliSeconds(
185 Constants.STOP_SIMULATOR_TIMEOUT,
186 )} at stopping charging stations simulator`;
187 console
.warn(chalk
.yellow(message
));
189 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
192 await this.workerImplementation
?.stop();
193 this.workerImplementation
= null;
194 this.uiServer
?.stop();
195 await this.storage
?.close();
196 this.resetCounters();
197 this.initializedCounters
= false;
198 this.started
= false;
199 this.stopping
= false;
201 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
204 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
208 public async restart(): Promise
<void> {
213 private initializeWorkerImplementation(): void {
214 this.workerImplementation
=== null &&
215 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
217 Configuration
.getWorker().processType
,
219 workerStartDelay
: Configuration
.getWorker().startDelay
,
220 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
221 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
222 poolMinSize
: Configuration
.getWorker().poolMinSize
,
223 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
225 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
226 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
232 private messageHandler(
233 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
236 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
244 case ChargingStationWorkerMessageEvents
.started
:
245 this.workerEventStarted(msg
.data
as ChargingStationData
);
246 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
248 case ChargingStationWorkerMessageEvents
.stopped
:
249 this.workerEventStopped(msg
.data
as ChargingStationData
);
250 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
252 case ChargingStationWorkerMessageEvents
.updated
:
253 this.workerEventUpdated(msg
.data
as ChargingStationData
);
254 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
256 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
257 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
259 ChargingStationWorkerMessageEvents
.performanceStatistics
,
260 msg
.data
as Statistics
,
265 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`,
270 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
278 private workerEventStarted
= (data
: ChargingStationData
) => {
279 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
280 ++this.numberOfStartedChargingStations
;
282 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
283 data.stationInfo.chargingStationId
284 } (hashId: ${data.stationInfo.hashId}) started (${
285 this.numberOfStartedChargingStations
286 } started from ${this.numberOfChargingStations})`,
290 private workerEventStopped
= (data
: ChargingStationData
) => {
291 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
292 --this.numberOfStartedChargingStations
;
294 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
295 data.stationInfo.chargingStationId
296 } (hashId: ${data.stationInfo.hashId}) stopped (${
297 this.numberOfStartedChargingStations
298 } started from ${this.numberOfChargingStations})`,
302 private workerEventUpdated
= (data
: ChargingStationData
) => {
303 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
306 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
307 this.storage
.storePerformanceStatistics(data
) as void;
310 private initializeCounters() {
311 if (this.initializedCounters
=== false) {
312 this.resetCounters();
313 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
314 if (isNotEmptyArray(stationTemplateUrls
)) {
315 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
316 for (const stationTemplateUrl
of stationTemplateUrls
) {
317 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
321 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
323 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
325 if (this.numberOfChargingStations
=== 0) {
327 chalk
.yellow('No charging station template enabled in configuration, exiting'),
329 process
.exit(exitCodes
.noChargingStationTemplates
);
331 this.initializedCounters
= true;
335 private resetCounters(): void {
336 this.numberOfChargingStationTemplates
= 0;
337 this.numberOfChargingStations
= 0;
338 this.numberOfStartedChargingStations
= 0;
341 private async startChargingStation(
343 stationTemplateUrl
: StationTemplateUrl
,
345 await this.workerImplementation
?.addElement({
348 dirname(fileURLToPath(import.meta
.url
)),
351 stationTemplateUrl
.file
,
356 private gracefulShutdown
= (): void => {
357 console
.info(`${chalk.green('Graceful shutdown')}`);
363 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
368 private logPrefix
= (): string => {
369 return logPrefix(' Bootstrap |');