perf: fine tune default pool size for load tests
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import { type FSWatcher, readFileSync, watch } from 'node:fs';
2 import { dirname, join, resolve } from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import chalk from 'chalk';
6 import merge from 'just-merge';
7
8 import { Constants } from './Constants';
9 import { hasOwnProp, isCFEnvironment, isNotEmptyString, isUndefined } from './Utils';
10 import {
11 ApplicationProtocol,
12 type ConfigurationData,
13 ConfigurationSection,
14 FileType,
15 type LogConfiguration,
16 type StationTemplateUrl,
17 type StorageConfiguration,
18 StorageType,
19 SupervisionUrlDistribution,
20 type UIServerConfiguration,
21 type WorkerConfiguration,
22 } from '../types';
23 import { WorkerConstants, WorkerProcessType } from '../worker';
24
25 type ConfigurationSectionType =
26 | LogConfiguration
27 | StorageConfiguration
28 | WorkerConfiguration
29 | UIServerConfiguration;
30
31 export class Configuration {
32 private static configurationFile = join(
33 dirname(fileURLToPath(import.meta.url)),
34 'assets',
35 'config.json',
36 );
37
38 private static configurationFileWatcher?: FSWatcher;
39 private static configurationData?: ConfigurationData;
40 private static configurationSectionCache = new Map<
41 ConfigurationSection,
42 ConfigurationSectionType
43 >([
44 [ConfigurationSection.log, Configuration.buildLogSection()],
45 [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
46 [ConfigurationSection.worker, Configuration.buildWorkerSection()],
47 [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
48 ]);
49
50 private static configurationChangeCallback: () => Promise<void>;
51
52 private constructor() {
53 // This is intentional
54 }
55
56 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
57 Configuration.configurationChangeCallback = cb;
58 }
59
60 public static getConfigurationSection<T extends ConfigurationSectionType>(
61 sectionName: ConfigurationSection,
62 ): T {
63 if (!Configuration.configurationSectionCache.has(sectionName)) {
64 switch (sectionName) {
65 case ConfigurationSection.log:
66 Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection());
67 break;
68 case ConfigurationSection.performanceStorage:
69 Configuration.configurationSectionCache.set(
70 sectionName,
71 Configuration.buildPerformanceStorageSection(),
72 );
73 break;
74 case ConfigurationSection.worker:
75 Configuration.configurationSectionCache.set(
76 sectionName,
77 Configuration.buildWorkerSection(),
78 );
79 break;
80 case ConfigurationSection.uiServer:
81 Configuration.configurationSectionCache.set(
82 sectionName,
83 Configuration.buildUIServerSection(),
84 );
85 break;
86 default:
87 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
88 throw new Error(`Unknown configuration section '${sectionName}'`);
89 }
90 }
91 return Configuration.configurationSectionCache.get(sectionName) as T;
92 }
93
94 public static getAutoReconnectMaxRetries(): number | undefined {
95 Configuration.warnDeprecatedConfigurationKey(
96 'autoReconnectTimeout',
97 undefined,
98 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
99 );
100 Configuration.warnDeprecatedConfigurationKey(
101 'connectionTimeout',
102 undefined,
103 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
104 );
105 Configuration.warnDeprecatedConfigurationKey(
106 'autoReconnectMaxRetries',
107 undefined,
108 'Use it in charging station template instead',
109 );
110 if (hasOwnProp(Configuration.getConfigurationData(), 'autoReconnectMaxRetries')) {
111 return Configuration.getConfigurationData()?.autoReconnectMaxRetries;
112 }
113 }
114
115 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
116 Configuration.warnDeprecatedConfigurationKey(
117 'stationTemplateURLs',
118 undefined,
119 "Use 'stationTemplateUrls' instead",
120 );
121 // eslint-disable-next-line @typescript-eslint/dot-notation
122 !isUndefined(
123 Configuration.getConfigurationData()!['stationTemplateURLs' as keyof ConfigurationData],
124 ) &&
125 (Configuration.getConfigurationData()!.stationTemplateUrls =
126 Configuration.getConfigurationData()![
127 // eslint-disable-next-line @typescript-eslint/dot-notation
128 'stationTemplateURLs' as keyof ConfigurationData
129 ] as StationTemplateUrl[]);
130 Configuration.getConfigurationData()!.stationTemplateUrls.forEach(
131 (stationTemplateUrl: StationTemplateUrl) => {
132 // eslint-disable-next-line @typescript-eslint/dot-notation
133 if (!isUndefined(stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl])) {
134 console.error(
135 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
136 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`,
137 )}`,
138 );
139 }
140 },
141 );
142 return Configuration.getConfigurationData()?.stationTemplateUrls;
143 }
144
145 public static getSupervisionUrls(): string | string[] | undefined {
146 Configuration.warnDeprecatedConfigurationKey(
147 'supervisionURLs',
148 undefined,
149 "Use 'supervisionUrls' instead",
150 );
151 // eslint-disable-next-line @typescript-eslint/dot-notation
152 if (
153 !isUndefined(
154 Configuration.getConfigurationData()!['supervisionURLs' as keyof ConfigurationData],
155 )
156 ) {
157 Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
158 // eslint-disable-next-line @typescript-eslint/dot-notation
159 'supervisionURLs' as keyof ConfigurationData
160 ] as string | string[];
161 }
162 return Configuration.getConfigurationData()?.supervisionUrls;
163 }
164
165 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
166 Configuration.warnDeprecatedConfigurationKey(
167 'distributeStationToTenantEqually',
168 undefined,
169 "Use 'supervisionUrlDistribution' instead",
170 );
171 Configuration.warnDeprecatedConfigurationKey(
172 'distributeStationsToTenantsEqually',
173 undefined,
174 "Use 'supervisionUrlDistribution' instead",
175 );
176 return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
177 ? Configuration.getConfigurationData()?.supervisionUrlDistribution
178 : SupervisionUrlDistribution.ROUND_ROBIN;
179 }
180
181 public static workerPoolInUse(): boolean {
182 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
183 Configuration.buildWorkerSection().processType!,
184 );
185 }
186
187 public static workerDynamicPoolInUse(): boolean {
188 return Configuration.buildWorkerSection().processType === WorkerProcessType.dynamicPool;
189 }
190
191 private static buildUIServerSection(): UIServerConfiguration {
192 if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) {
193 console.error(
194 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
195 `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`,
196 )}`,
197 );
198 }
199 let uiServerConfiguration: UIServerConfiguration = {
200 enabled: false,
201 type: ApplicationProtocol.WS,
202 options: {
203 host: Constants.DEFAULT_UI_SERVER_HOST,
204 port: Constants.DEFAULT_UI_SERVER_PORT,
205 },
206 };
207 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
208 uiServerConfiguration = merge<UIServerConfiguration>(
209 uiServerConfiguration,
210 Configuration.getConfigurationData()!.uiServer!,
211 );
212 }
213 if (isCFEnvironment() === true) {
214 delete uiServerConfiguration.options?.host;
215 uiServerConfiguration.options!.port = parseInt(process.env.PORT!);
216 }
217 return uiServerConfiguration;
218 }
219
220 private static buildPerformanceStorageSection(): StorageConfiguration {
221 Configuration.warnDeprecatedConfigurationKey(
222 'URI',
223 ConfigurationSection.performanceStorage,
224 "Use 'uri' instead",
225 );
226 let storageConfiguration: StorageConfiguration = {
227 enabled: false,
228 type: StorageType.JSON_FILE,
229 uri: Configuration.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
230 };
231 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
232 storageConfiguration = {
233 ...storageConfiguration,
234 ...Configuration.getConfigurationData()?.performanceStorage,
235 ...(Configuration.getConfigurationData()?.performanceStorage?.type ===
236 StorageType.JSON_FILE &&
237 Configuration.getConfigurationData()?.performanceStorage?.uri && {
238 uri: Configuration.buildPerformanceUriFilePath(
239 new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname,
240 ),
241 }),
242 };
243 }
244 return storageConfiguration;
245 }
246
247 private static buildLogSection(): LogConfiguration {
248 Configuration.warnDeprecatedConfigurationKey(
249 'logEnabled',
250 undefined,
251 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
252 );
253 Configuration.warnDeprecatedConfigurationKey(
254 'logFile',
255 undefined,
256 `Use '${ConfigurationSection.log}' section to define the log file instead`,
257 );
258 Configuration.warnDeprecatedConfigurationKey(
259 'logErrorFile',
260 undefined,
261 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
262 );
263 Configuration.warnDeprecatedConfigurationKey(
264 'logConsole',
265 undefined,
266 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
267 );
268 Configuration.warnDeprecatedConfigurationKey(
269 'logStatisticsInterval',
270 undefined,
271 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
272 );
273 Configuration.warnDeprecatedConfigurationKey(
274 'logLevel',
275 undefined,
276 `Use '${ConfigurationSection.log}' section to define the log level instead`,
277 );
278 Configuration.warnDeprecatedConfigurationKey(
279 'logFormat',
280 undefined,
281 `Use '${ConfigurationSection.log}' section to define the log format instead`,
282 );
283 Configuration.warnDeprecatedConfigurationKey(
284 'logRotate',
285 undefined,
286 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
287 );
288 Configuration.warnDeprecatedConfigurationKey(
289 'logMaxFiles',
290 undefined,
291 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
292 );
293 Configuration.warnDeprecatedConfigurationKey(
294 'logMaxSize',
295 undefined,
296 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
297 );
298 const defaultLogConfiguration: LogConfiguration = {
299 enabled: true,
300 file: 'logs/combined.log',
301 errorFile: 'logs/error.log',
302 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
303 level: 'info',
304 format: 'simple',
305 rotate: true,
306 };
307 const deprecatedLogConfiguration: LogConfiguration = {
308 ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
309 enabled: Configuration.getConfigurationData()?.logEnabled,
310 }),
311 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
312 file: Configuration.getConfigurationData()?.logFile,
313 }),
314 ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
315 errorFile: Configuration.getConfigurationData()?.logErrorFile,
316 }),
317 ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
318 statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval,
319 }),
320 ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
321 level: Configuration.getConfigurationData()?.logLevel,
322 }),
323 ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
324 console: Configuration.getConfigurationData()?.logConsole,
325 }),
326 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
327 format: Configuration.getConfigurationData()?.logFormat,
328 }),
329 ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
330 rotate: Configuration.getConfigurationData()?.logRotate,
331 }),
332 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
333 maxFiles: Configuration.getConfigurationData()?.logMaxFiles,
334 }),
335 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
336 maxSize: Configuration.getConfigurationData()?.logMaxSize,
337 }),
338 };
339 const logConfiguration: LogConfiguration = {
340 ...defaultLogConfiguration,
341 ...deprecatedLogConfiguration,
342 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
343 Configuration.getConfigurationData()?.log),
344 };
345 return logConfiguration;
346 }
347
348 private static buildWorkerSection(): WorkerConfiguration {
349 Configuration.warnDeprecatedConfigurationKey(
350 'useWorkerPool',
351 undefined,
352 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
353 );
354 Configuration.warnDeprecatedConfigurationKey(
355 'workerProcess',
356 undefined,
357 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
358 );
359 Configuration.warnDeprecatedConfigurationKey(
360 'workerStartDelay',
361 undefined,
362 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
363 );
364 Configuration.warnDeprecatedConfigurationKey(
365 'chargingStationsPerWorker',
366 undefined,
367 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
368 );
369 Configuration.warnDeprecatedConfigurationKey(
370 'elementStartDelay',
371 undefined,
372 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
373 );
374 Configuration.warnDeprecatedConfigurationKey(
375 'workerPoolMinSize',
376 undefined,
377 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
378 );
379 Configuration.warnDeprecatedConfigurationKey(
380 'workerPoolSize;',
381 undefined,
382 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
383 );
384 Configuration.warnDeprecatedConfigurationKey(
385 'workerPoolMaxSize;',
386 undefined,
387 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
388 );
389 Configuration.warnDeprecatedConfigurationKey(
390 'workerPoolStrategy;',
391 undefined,
392 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
393 );
394 const defaultWorkerConfiguration: WorkerConfiguration = {
395 processType: WorkerProcessType.workerSet,
396 startDelay: WorkerConstants.DEFAULT_WORKER_START_DELAY,
397 elementsPerWorker: 'auto',
398 elementStartDelay: WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
399 poolMinSize: WorkerConstants.DEFAULT_POOL_MIN_SIZE,
400 poolMaxSize: WorkerConstants.DEFAULT_POOL_MAX_SIZE,
401 };
402 hasOwnProp(Configuration.getConfigurationData(), 'workerPoolStrategy') &&
403 delete Configuration.getConfigurationData()?.workerPoolStrategy;
404 const deprecatedWorkerConfiguration: WorkerConfiguration = {
405 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
406 processType: Configuration.getConfigurationData()?.workerProcess,
407 }),
408 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerStartDelay') && {
409 startDelay: Configuration.getConfigurationData()?.workerStartDelay,
410 }),
411 ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
412 elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker,
413 }),
414 ...(hasOwnProp(Configuration.getConfigurationData(), 'elementStartDelay') && {
415 elementStartDelay: Configuration.getConfigurationData()?.elementStartDelay,
416 }),
417 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
418 poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize,
419 }),
420 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMaxSize') && {
421 poolMaxSize: Configuration.getConfigurationData()?.workerPoolMaxSize,
422 }),
423 };
424 Configuration.warnDeprecatedConfigurationKey(
425 'poolStrategy',
426 ConfigurationSection.worker,
427 'Not publicly exposed to end users',
428 );
429 const workerConfiguration: WorkerConfiguration = {
430 ...defaultWorkerConfiguration,
431 ...deprecatedWorkerConfiguration,
432 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) &&
433 Configuration.getConfigurationData()?.worker),
434 };
435 if (!Object.values(WorkerProcessType).includes(workerConfiguration.processType!)) {
436 throw new SyntaxError(
437 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
438 );
439 }
440 return workerConfiguration;
441 }
442
443 private static logPrefix = (): string => {
444 return `${new Date().toLocaleString()} Simulator configuration |`;
445 };
446
447 private static warnDeprecatedConfigurationKey(
448 key: string,
449 sectionName?: string,
450 logMsgToAppend = '',
451 ) {
452 if (
453 sectionName &&
454 !isUndefined(Configuration.getConfigurationData()![sectionName as keyof ConfigurationData]) &&
455 !isUndefined(
456 (
457 Configuration.getConfigurationData()![sectionName as keyof ConfigurationData] as Record<
458 string,
459 unknown
460 >
461 )[key],
462 )
463 ) {
464 console.error(
465 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
466 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
467 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
468 }`,
469 )}`,
470 );
471 } else if (
472 !isUndefined(Configuration.getConfigurationData()![key as keyof ConfigurationData])
473 ) {
474 console.error(
475 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
476 `Deprecated configuration key '${key}' usage${
477 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
478 }`,
479 )}`,
480 );
481 }
482 }
483
484 private static getConfigurationData(): ConfigurationData | undefined {
485 if (!Configuration.configurationData) {
486 try {
487 Configuration.configurationData = JSON.parse(
488 readFileSync(Configuration.configurationFile, 'utf8'),
489 ) as ConfigurationData;
490 if (!Configuration.configurationFileWatcher) {
491 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
492 }
493 } catch (error) {
494 Configuration.handleFileException(
495 Configuration.configurationFile,
496 FileType.Configuration,
497 error as NodeJS.ErrnoException,
498 Configuration.logPrefix(),
499 );
500 }
501 }
502 return Configuration.configurationData;
503 }
504
505 private static getConfigurationFileWatcher(): FSWatcher | undefined {
506 try {
507 return watch(Configuration.configurationFile, (event, filename): void => {
508 if (filename!.trim()!.length > 0 && event === 'change') {
509 delete Configuration.configurationData;
510 Configuration.configurationSectionCache.clear();
511 if (!isUndefined(Configuration.configurationChangeCallback)) {
512 Configuration.configurationChangeCallback().catch((error) => {
513 throw typeof error === 'string' ? new Error(error) : error;
514 });
515 }
516 }
517 });
518 } catch (error) {
519 Configuration.handleFileException(
520 Configuration.configurationFile,
521 FileType.Configuration,
522 error as NodeJS.ErrnoException,
523 Configuration.logPrefix(),
524 );
525 }
526 }
527
528 private static handleFileException(
529 file: string,
530 fileType: FileType,
531 error: NodeJS.ErrnoException,
532 logPrefix: string,
533 ): void {
534 const prefix = isNotEmptyString(logPrefix) ? `${logPrefix} ` : '';
535 let logMsg: string;
536 switch (error.code) {
537 case 'ENOENT':
538 logMsg = `${fileType} file ${file} not found:`;
539 break;
540 case 'EEXIST':
541 logMsg = `${fileType} file ${file} already exists:`;
542 break;
543 case 'EACCES':
544 logMsg = `${fileType} file ${file} access denied:`;
545 break;
546 case 'EPERM':
547 logMsg = `${fileType} file ${file} permission denied:`;
548 break;
549 default:
550 logMsg = `${fileType} file ${file} error:`;
551 }
552 console.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
553 throw error;
554 }
555
556 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
557 switch (storageType) {
558 case StorageType.JSON_FILE:
559 return Configuration.buildPerformanceUriFilePath(
560 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
561 );
562 case StorageType.SQLITE:
563 return Configuration.buildPerformanceUriFilePath(
564 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
565 );
566 default:
567 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
568 }
569 }
570
571 private static buildPerformanceUriFilePath(file: string) {
572 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;
573 }
574 }