Commit | Line | Data |
---|---|---|
d972af76 | 1 | import { type FSWatcher, readFileSync } from 'node:fs'; |
f911a4af | 2 | |
4c3c0d59 | 3 | import type { ChargingStation } from './ChargingStation'; |
08b58f00 | 4 | import { getIdTagsFile } from './Helpers'; |
f911a4af | 5 | import { FileType, IdTagDistribution } from '../types'; |
9bf0ef23 JB |
6 | import { |
7 | handleFileException, | |
8 | isNotEmptyString, | |
9 | logPrefix, | |
10 | logger, | |
11 | secureRandom, | |
12 | watchJsonFile, | |
13 | } from '../utils'; | |
f911a4af | 14 | |
e1d9a0f4 | 15 | interface IdTagsCacheValueType { |
f911a4af | 16 | idTags: string[]; |
d972af76 | 17 | idTagsFileWatcher: FSWatcher | undefined; |
e1d9a0f4 | 18 | } |
f911a4af JB |
19 | |
20 | export class IdTagsCache { | |
21 | private static instance: IdTagsCache | null = null; | |
22 | private readonly idTagsCaches: Map<string, IdTagsCacheValueType>; | |
23 | private readonly idTagsCachesAddressableIndexes: Map<string, number>; | |
24 | ||
25 | private constructor() { | |
26 | this.idTagsCaches = new Map<string, IdTagsCacheValueType>(); | |
27 | this.idTagsCachesAddressableIndexes = new Map<string, number>(); | |
28 | } | |
29 | ||
30 | public static getInstance(): IdTagsCache { | |
31 | if (IdTagsCache.instance === null) { | |
32 | IdTagsCache.instance = new IdTagsCache(); | |
33 | } | |
34 | return IdTagsCache.instance; | |
35 | } | |
36 | ||
7b5dbe91 | 37 | /** |
361c98f5 | 38 | * Gets one idtag from the cache given the distribution |
7b5dbe91 JB |
39 | * Must be called after checking the cache is not an empty array |
40 | * | |
4c8782ee JB |
41 | * @param distribution - |
42 | * @param chargingStation - | |
43 | * @param connectorId - | |
7b5dbe91 JB |
44 | * @returns |
45 | */ | |
f911a4af JB |
46 | public getIdTag( |
47 | distribution: IdTagDistribution, | |
48 | chargingStation: ChargingStation, | |
5edd8ba0 | 49 | connectorId: number, |
f911a4af JB |
50 | ): string { |
51 | const hashId = chargingStation.stationInfo.hashId; | |
e1d9a0f4 | 52 | const idTagsFile = getIdTagsFile(chargingStation.stationInfo)!; |
f911a4af JB |
53 | switch (distribution) { |
54 | case IdTagDistribution.RANDOM: | |
55 | return this.getRandomIdTag(hashId, idTagsFile); | |
56 | case IdTagDistribution.ROUND_ROBIN: | |
57 | return this.getRoundRobinIdTag(hashId, idTagsFile); | |
58 | case IdTagDistribution.CONNECTOR_AFFINITY: | |
59 | return this.getConnectorAffinityIdTag(chargingStation, connectorId); | |
60 | default: | |
61 | return this.getRoundRobinIdTag(hashId, idTagsFile); | |
62 | } | |
63 | } | |
64 | ||
7b5dbe91 | 65 | /** |
361c98f5 | 66 | * Gets all idtags from the cache |
7b5dbe91 JB |
67 | * Must be called after checking the cache is not an empty array |
68 | * | |
4c8782ee | 69 | * @param file - |
7b5dbe91 JB |
70 | * @returns |
71 | */ | |
f911a4af JB |
72 | public getIdTags(file: string): string[] | undefined { |
73 | if (this.hasIdTagsCache(file) === false) { | |
6082281f | 74 | this.setIdTagsCache(file, this.getIdTagsFromFile(file)); |
f911a4af JB |
75 | } |
76 | return this.getIdTagsCache(file); | |
77 | } | |
78 | ||
79 | public deleteIdTags(file: string): boolean { | |
26a17d93 | 80 | return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file); |
f911a4af JB |
81 | } |
82 | ||
83 | private getRandomIdTag(hashId: string, file: string): string { | |
e1d9a0f4 | 84 | const idTags = this.getIdTags(file)!; |
32e8c8a5 | 85 | const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId); |
f911a4af JB |
86 | this.idTagsCachesAddressableIndexes.set( |
87 | addressableKey, | |
5edd8ba0 | 88 | Math.floor(secureRandom() * idTags.length), |
f911a4af | 89 | ); |
e1d9a0f4 | 90 | return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]; |
f911a4af JB |
91 | } |
92 | ||
93 | private getRoundRobinIdTag(hashId: string, file: string): string { | |
e1d9a0f4 | 94 | const idTags = this.getIdTags(file)!; |
32e8c8a5 | 95 | const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId); |
f911a4af JB |
96 | const idTagIndex = this.idTagsCachesAddressableIndexes.get(addressableKey) ?? 0; |
97 | const idTag = idTags[idTagIndex]; | |
98 | this.idTagsCachesAddressableIndexes.set( | |
99 | addressableKey, | |
5edd8ba0 | 100 | idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1, |
f911a4af JB |
101 | ); |
102 | return idTag; | |
103 | } | |
104 | ||
105 | private getConnectorAffinityIdTag(chargingStation: ChargingStation, connectorId: number): string { | |
e1d9a0f4 JB |
106 | const file = getIdTagsFile(chargingStation.stationInfo)!; |
107 | const idTags = this.getIdTags(file)!; | |
728e01f0 JB |
108 | const addressableKey = this.getIdTagsCacheIndexesAddressableKey( |
109 | file, | |
5edd8ba0 | 110 | chargingStation.stationInfo.hashId, |
728e01f0 | 111 | ); |
f911a4af JB |
112 | this.idTagsCachesAddressableIndexes.set( |
113 | addressableKey, | |
5edd8ba0 | 114 | (chargingStation.index - 1 + (connectorId - 1)) % idTags.length, |
f911a4af | 115 | ); |
e1d9a0f4 | 116 | return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]; |
f911a4af JB |
117 | } |
118 | ||
119 | private hasIdTagsCache(file: string): boolean { | |
120 | return this.idTagsCaches.has(file); | |
121 | } | |
122 | ||
123 | private setIdTagsCache(file: string, idTags: string[]) { | |
124 | return this.idTagsCaches.set(file, { | |
125 | idTags, | |
fa5995d6 | 126 | idTagsFileWatcher: watchJsonFile( |
f911a4af JB |
127 | file, |
128 | FileType.Authorization, | |
129 | this.logPrefix(file), | |
130 | undefined, | |
131 | (event, filename) => { | |
9bf0ef23 | 132 | if (isNotEmptyString(filename) && event === 'change') { |
f911a4af JB |
133 | try { |
134 | logger.debug( | |
5edd8ba0 | 135 | `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`, |
f911a4af JB |
136 | ); |
137 | this.deleteIdTagsCache(file); | |
138 | this.deleteIdTagsCacheIndexes(file); | |
139 | } catch (error) { | |
fa5995d6 | 140 | handleFileException( |
f911a4af JB |
141 | file, |
142 | FileType.Authorization, | |
143 | error as NodeJS.ErrnoException, | |
144 | this.logPrefix(file), | |
145 | { | |
146 | throwError: false, | |
5edd8ba0 | 147 | }, |
f911a4af JB |
148 | ); |
149 | } | |
150 | } | |
5edd8ba0 | 151 | }, |
f911a4af JB |
152 | ), |
153 | }); | |
154 | } | |
155 | ||
156 | private getIdTagsCache(file: string): string[] | undefined { | |
157 | return this.idTagsCaches.get(file)?.idTags; | |
158 | } | |
159 | ||
160 | private deleteIdTagsCache(file: string): boolean { | |
161 | this.idTagsCaches.get(file)?.idTagsFileWatcher?.close(); | |
162 | return this.idTagsCaches.delete(file); | |
163 | } | |
164 | ||
26a17d93 | 165 | private deleteIdTagsCacheIndexes(file: string): boolean { |
e1d9a0f4 | 166 | const deleted: boolean[] = []; |
f911a4af JB |
167 | for (const [key] of this.idTagsCachesAddressableIndexes) { |
168 | if (key.startsWith(file)) { | |
26a17d93 | 169 | deleted.push(this.idTagsCachesAddressableIndexes.delete(key)); |
f911a4af JB |
170 | } |
171 | } | |
26a17d93 | 172 | return !deleted.some((value) => value === false); |
f911a4af JB |
173 | } |
174 | ||
32e8c8a5 | 175 | private getIdTagsCacheIndexesAddressableKey(prefix: string, uid: string): string { |
7af183e7 JB |
176 | return `${prefix}${uid}`; |
177 | } | |
178 | ||
f911a4af | 179 | private getIdTagsFromFile(file: string): string[] { |
9bf0ef23 | 180 | if (isNotEmptyString(file)) { |
f911a4af | 181 | try { |
d972af76 | 182 | return JSON.parse(readFileSync(file, 'utf8')) as string[]; |
f911a4af | 183 | } catch (error) { |
fa5995d6 | 184 | handleFileException( |
f911a4af JB |
185 | file, |
186 | FileType.Authorization, | |
187 | error as NodeJS.ErrnoException, | |
5edd8ba0 | 188 | this.logPrefix(file), |
f911a4af JB |
189 | ); |
190 | } | |
f911a4af | 191 | } |
7b5dbe91 | 192 | return []; |
f911a4af JB |
193 | } |
194 | ||
195 | private logPrefix = (file: string): string => { | |
9bf0ef23 | 196 | return logPrefix(` Id tags cache for id tags file '${file}' |`); |
f911a4af JB |
197 | }; |
198 | } |