perf(simulator): use content addressable cache for idtag distribution
[e-mobility-charging-stations-simulator.git] / src / charging-station / AuthorizedTagsCache.ts
1 import fs from 'node:fs';
2
3 import { type ChargingStation, ChargingStationUtils } from './internal';
4 import { FileType, IdTagDistribution } from '../types';
5 import { FileUtils, Utils, logger } from '../utils';
6
7 type TagsCacheValueType = {
8 tags: string[];
9 tagsFileWatcher: fs.FSWatcher | undefined;
10 };
11
12 export class AuthorizedTagsCache {
13 private static instance: AuthorizedTagsCache | null = null;
14 private readonly tagsCaches: Map<string, TagsCacheValueType>;
15 private readonly tagsCachesAddressableIndexes: Map<string, number>;
16
17 private constructor() {
18 this.tagsCaches = new Map<string, TagsCacheValueType>();
19 this.tagsCachesAddressableIndexes = new Map<string, number>();
20 }
21
22 public static getInstance(): AuthorizedTagsCache {
23 if (AuthorizedTagsCache.instance === null) {
24 AuthorizedTagsCache.instance = new AuthorizedTagsCache();
25 }
26 return AuthorizedTagsCache.instance;
27 }
28
29 public getIdTag(
30 distribution: IdTagDistribution,
31 chargingStation: ChargingStation,
32 connectorId: number
33 ): string {
34 const hashId = chargingStation.stationInfo.hashId;
35 const authorizationFile = ChargingStationUtils.getAuthorizationFile(
36 chargingStation.stationInfo
37 );
38 switch (distribution) {
39 case IdTagDistribution.RANDOM:
40 return this.getRandomIdTag(hashId, authorizationFile);
41 case IdTagDistribution.ROUND_ROBIN:
42 return this.getRoundRobinIdTag(hashId, authorizationFile);
43 case IdTagDistribution.CONNECTOR_AFFINITY:
44 return this.getConnectorAffinityIdTag(chargingStation, connectorId);
45 default:
46 return this.getRoundRobinIdTag(hashId, authorizationFile);
47 }
48 }
49
50 public getAuthorizedTags(file: string): string[] | undefined {
51 if (this.hasTags(file) === false) {
52 this.setTags(file, this.getAuthorizedTagsFromFile(file));
53 }
54 return this.getTags(file);
55 }
56
57 public deleteAuthorizedTags(file: string): boolean {
58 return this.deleteTags(file);
59 }
60
61 private getRandomIdTag(hashId: string, file: string): string {
62 const tags = this.getAuthorizedTags(file);
63 this.tagsCachesAddressableIndexes.set(
64 file + hashId,
65 Math.floor(Utils.secureRandom() * tags.length)
66 );
67 return tags[this.tagsCachesAddressableIndexes.get(file + hashId)];
68 }
69
70 private getRoundRobinIdTag(hashId: string, file: string): string {
71 const tags = this.getAuthorizedTags(file);
72 const idTagIndex = this.tagsCachesAddressableIndexes.get(file + hashId) ?? 0;
73 const idTag = tags[idTagIndex];
74 this.tagsCachesAddressableIndexes.set(
75 file + hashId,
76 idTagIndex === tags.length - 1 ? 0 : idTagIndex + 1
77 );
78 return idTag;
79 }
80
81 private getConnectorAffinityIdTag(chargingStation: ChargingStation, connectorId: number): string {
82 const file = ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo);
83 const tags = this.getAuthorizedTags(file);
84 const hashId = chargingStation.stationInfo.hashId;
85 this.tagsCachesAddressableIndexes.set(
86 file + hashId,
87 (chargingStation.index - 1 + (connectorId - 1)) % tags.length
88 );
89 return tags[this.tagsCachesAddressableIndexes.get(file + hashId)];
90 }
91
92 private hasTags(file: string): boolean {
93 return this.tagsCaches.has(file);
94 }
95
96 private setTags(file: string, tags: string[]) {
97 return this.tagsCaches.set(file, {
98 tags,
99 tagsFileWatcher: FileUtils.watchJsonFile(
100 file,
101 FileType.Authorization,
102 this.logPrefix(file),
103 undefined,
104 (event, filename) => {
105 if (Utils.isNotEmptyString(filename) && event === 'change') {
106 try {
107 logger.debug(
108 `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`
109 );
110 this.deleteTags(file);
111 this.deleteTagsIndexes(file);
112 } catch (error) {
113 FileUtils.handleFileException(
114 file,
115 FileType.Authorization,
116 error as NodeJS.ErrnoException,
117 this.logPrefix(file),
118 {
119 throwError: false,
120 }
121 );
122 }
123 }
124 }
125 ),
126 });
127 }
128
129 private getTags(file: string): string[] | undefined {
130 return this.tagsCaches.get(file)?.tags;
131 }
132
133 private deleteTags(file: string): boolean {
134 this.tagsCaches.get(file)?.tagsFileWatcher?.close();
135 return this.tagsCaches.delete(file);
136 }
137
138 private deleteTagsIndexes(file: string): void {
139 for (const [key] of this.tagsCachesAddressableIndexes) {
140 if (key.startsWith(file)) {
141 this.tagsCachesAddressableIndexes.delete(key);
142 }
143 }
144 }
145
146 private getAuthorizedTagsFromFile(file: string): string[] {
147 let authorizedTags: string[] = [];
148 if (file) {
149 try {
150 // Load authorization file
151 authorizedTags = JSON.parse(fs.readFileSync(file, 'utf8')) as string[];
152 } catch (error) {
153 FileUtils.handleFileException(
154 file,
155 FileType.Authorization,
156 error as NodeJS.ErrnoException,
157 this.logPrefix(file)
158 );
159 }
160 } else {
161 logger.info(`${this.logPrefix(file)} No authorization file given`);
162 }
163 return authorizedTags;
164 }
165
166 private logPrefix = (file: string): string => {
167 return Utils.logPrefix(` Authorized tags cache for authorization file '${file}' |`);
168 };
169 }