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 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
)
87 (this.uiServer
= UIServerFactory
.getUIServerImplementation(
88 Configuration
.getConfigurationSection
<UIServerConfiguration
>(ConfigurationSection
.uiServer
),
90 Configuration
.getConfigurationSection
<StorageConfiguration
>(
91 ConfigurationSection
.performanceStorage
,
93 (this.storage
= StorageFactory
.getStorage(
94 Configuration
.getConfigurationSection
<StorageConfiguration
>(
95 ConfigurationSection
.performanceStorage
,
97 Configuration
.getConfigurationSection
<StorageConfiguration
>(
98 ConfigurationSection
.performanceStorage
,
102 Configuration
.setConfigurationChangeCallback(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> {
114 throw new Error('Cannot start charging stations simulator from worker thread');
116 if (this.started
=== false) {
117 if (this.starting
=== false) {
118 this.starting
= true;
119 this.initializeCounters();
120 this.initializeWorkerImplementation();
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 ? `$
{Configuration
.getConfigurationSection
<WorkerConfiguration
>(
147 ConfigurationSection
.worker
,
148 ).poolMinSize
?.toString()}/`
150 }${this.workerImplementation?.size}${
151 Configuration.workerPoolInUse()
152 ? `/$
{Configuration
.getConfigurationSection
<WorkerConfiguration
>(
153 ConfigurationSection
.worker
,
154 ).poolMaxSize
?.toString()}`
156 } worker(s) concurrently running in '${
157 Configuration.getConfigurationSection<WorkerConfiguration>(
158 ConfigurationSection.worker,
161 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
162 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
167 Configuration
.workerDynamicPoolInUse() &&
170 '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',
173 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
175 this.starting
= false;
177 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
180 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
184 public async stop(): Promise
<void> {
186 throw new Error('Cannot stop charging stations simulator from worker thread');
188 if (this.started
=== true) {
189 if (this.stopping
=== false) {
190 this.stopping
= true;
191 await this.uiServer
?.sendInternalRequest(
192 this.uiServer
.buildProtocolRequest(
194 ProcedureName
.STOP_CHARGING_STATION
,
195 Constants
.EMPTY_FREEZED_OBJECT
,
199 waitChargingStationEvents(
201 ChargingStationWorkerMessageEvents
.stopped
,
202 this.numberOfChargingStations
,
204 new Promise
<string>((resolve
) => {
206 const message
= `Timeout reached ${formatDurationMilliSeconds(
207 Constants.STOP_SIMULATOR_TIMEOUT,
208 )} at stopping charging stations simulator`;
209 console
.warn(chalk
.yellow(message
));
211 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
214 await this.workerImplementation
?.stop();
215 this.workerImplementation
= null;
216 this.uiServer
?.stop();
217 await this.storage
?.close();
218 this.resetCounters();
219 this.initializedCounters
= false;
220 this.started
= false;
221 this.stopping
= false;
223 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
226 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
230 public async restart(): Promise
<void> {
235 private initializeWorkerImplementation(): void {
236 let elementsPerWorker
: number | undefined;
238 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
239 ?.elementsPerWorker
=== 'auto'
242 this.numberOfChargingStations
> availableParallelism()
243 ? Math.round(this.numberOfChargingStations
/ availableParallelism())
246 this.workerImplementation
=== null &&
247 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
249 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
252 workerStartDelay
: Configuration
.getConfigurationSection
<WorkerConfiguration
>(
253 ConfigurationSection
.worker
,
255 elementStartDelay
: Configuration
.getConfigurationSection
<WorkerConfiguration
>(
256 ConfigurationSection
.worker
,
258 poolMaxSize
: Configuration
.getConfigurationSection
<WorkerConfiguration
>(
259 ConfigurationSection
.worker
,
261 poolMinSize
: Configuration
.getConfigurationSection
<WorkerConfiguration
>(
262 ConfigurationSection
.worker
,
266 (Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
267 .elementsPerWorker
as number),
269 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
275 private messageHandler(
276 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
279 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
287 case ChargingStationWorkerMessageEvents
.started
:
288 this.workerEventStarted(msg
.data
as ChargingStationData
);
289 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
291 case ChargingStationWorkerMessageEvents
.stopped
:
292 this.workerEventStopped(msg
.data
as ChargingStationData
);
293 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
295 case ChargingStationWorkerMessageEvents
.updated
:
296 this.workerEventUpdated(msg
.data
as ChargingStationData
);
297 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
299 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
300 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
302 ChargingStationWorkerMessageEvents
.performanceStatistics
,
303 msg
.data
as Statistics
,
308 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`,
313 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
321 private workerEventStarted
= (data
: ChargingStationData
) => {
322 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
323 ++this.numberOfStartedChargingStations
;
325 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
326 data.stationInfo.chargingStationId
327 } (hashId: ${data.stationInfo.hashId}) started (${
328 this.numberOfStartedChargingStations
329 } started from ${this.numberOfChargingStations})`,
333 private workerEventStopped
= (data
: ChargingStationData
) => {
334 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
335 --this.numberOfStartedChargingStations
;
337 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
338 data.stationInfo.chargingStationId
339 } (hashId: ${data.stationInfo.hashId}) stopped (${
340 this.numberOfStartedChargingStations
341 } started from ${this.numberOfChargingStations})`,
345 private workerEventUpdated
= (data
: ChargingStationData
) => {
346 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
349 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
350 this.storage
.storePerformanceStatistics(data
) as void;
353 private initializeCounters() {
354 if (this.initializedCounters
=== false) {
355 this.resetCounters();
356 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
357 if (isNotEmptyArray(stationTemplateUrls
)) {
358 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
359 for (const stationTemplateUrl
of stationTemplateUrls
) {
360 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
364 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
366 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
368 if (this.numberOfChargingStations
=== 0) {
370 chalk
.yellow('No charging station template enabled in configuration, exiting'),
372 process
.exit(exitCodes
.noChargingStationTemplates
);
374 this.initializedCounters
= true;
378 private resetCounters(): void {
379 this.numberOfChargingStationTemplates
= 0;
380 this.numberOfChargingStations
= 0;
381 this.numberOfStartedChargingStations
= 0;
384 private async startChargingStation(
386 stationTemplateUrl
: StationTemplateUrl
,
388 await this.workerImplementation
?.addElement({
391 dirname(fileURLToPath(import.meta
.url
)),
394 stationTemplateUrl
.file
,
399 private gracefulShutdown
= (): void => {
400 console
.info(`${chalk.green('Graceful shutdown')}`);
406 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
411 private logPrefix
= (): string => {
412 return logPrefix(' Bootstrap |');