Skip to content

Commit a3a3c08

Browse files
authored
Caching 20230213 (#386)
* Prevent parallel FetchEnvs requests for the same kube resource, for optimal cache utilization * linting * package updates * replace p-limit with basic async promises * typofix * combine duplicate code in private function * Correct HIT/MISS logging
1 parent a5f0f3f commit a3a3c08

File tree

2 files changed

+901
-514
lines changed

2 files changed

+901
-514
lines changed

lib/FetchEnvs.js

Lines changed: 97 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ const KIND_MAP = new Map([
2929

3030
const LRU = require('lru-cache');
3131
const LruOptions = {
32-
maxSize: 100000, // the max cache size
32+
maxSize: parseInt(process.env.FETCHENVS_CACHE_SIZE) || 100000, // the max cache size
3333
sizeCalculation: (r) => { return( JSON.stringify(r).length ); }, // how to determine the size of a resource added to the cache
3434
ttl: 1000 * 60 * 3, // max time to cache (LRU does not directly enforce, but maxSize will eventually push them out)
3535
updateAgeOnGet: false, // Don't update ttl when an item is retrieved from cache
3636
updateAgeOnHas: false, // Don't update ttl when an item is checked in cache
3737
};
3838
const globalResourceCache = new LRU( LruOptions );
3939
const globalResourceCacheUsers = new Set();
40+
const singleResourceQueryCache = {};
4041

4142
module.exports = class FetchEnvs {
4243

@@ -58,39 +59,26 @@ module.exports = class FetchEnvs {
5859
(() => { log.debug('\'updateRazeeLogs()\' not passed to fetchEnvs. will not update razeeLogs on failure to fetch envs'); });
5960

6061
const user = this.data?.object?.spec?.clusterAuth?.impersonateUser;
61-
if( process.env.INSTANCE_FETCHENVS_CACHE_ONLY ) {
62-
// Using `user` is not technically necessary for an instance-specific cache, but used for consistency
63-
log.info( 'FetchEnvs.constructor using instance-specific resource cache' );
64-
this.instanceCache = {};
65-
this.resourceCache = {
66-
has: (key) => {
67-
const hit = Object.prototype.hasOwnProperty.call(this.instanceCache, `${user}/${key}`);
68-
log.info( `FetchEnvs cache ${hit?'HIT':'MISS'}: '${user}/${key}'` );
69-
return hit;
70-
},
71-
set: (key, value) => { this.instanceCache[`${user}/${key}`] = value; },
72-
get: (key) => { return this.instanceCache[`${user}/${key}`]; },
73-
};
74-
}
75-
else {
76-
log.info( `FetchEnvs.constructor using global resource cache, ${globalResourceCache.size} resources currently cached (may be TTL expired)` );
77-
this.resourceCache = {
78-
has: (key) => {
79-
const hit = globalResourceCache.has(`${user}/${key}`);
80-
log.info( `FetchEnvs cache ${hit?'HIT':'MISS'}: '${user}/${key}'` );
81-
return hit;
82-
},
83-
set: (key, value) => {
84-
// When setting a key, keep track of users to allow later deletion
85-
globalResourceCacheUsers.add( user );
86-
globalResourceCache.set(`${user}/${key}`, value);
87-
log.info( `FetchEnvs cached '${user}/${key}'` );
88-
},
89-
get: (key) => {
90-
return globalResourceCache.get(`${user}/${key}`);
91-
},
92-
};
93-
}
62+
this.resourceCache = {
63+
has: (key) => {
64+
const hit = globalResourceCache.has(`${user}/${key}`);
65+
if( hit ) log.info( `FetchEnvs cache HIT: '${user}/${key}'` );
66+
return hit;
67+
},
68+
set: (key, value) => {
69+
log.info( `FetchEnvs cache MISS: '${user}/${key}'` );
70+
// When setting a key, keep track of users to allow later deletion
71+
globalResourceCacheUsers.add( user );
72+
globalResourceCache.set(`${user}/${key}`, value);
73+
log.info( `FetchEnvs cached '${user}/${key}'` );
74+
},
75+
get: (key) => {
76+
if( globalResourceCache.has(`${user}/${key}`) ) {
77+
log.info( `FetchEnvs cache HIT: '${user}/${key}'` );
78+
}
79+
return globalResourceCache.get(`${user}/${key}`);
80+
},
81+
};
9482
}
9583

9684
// This function needs to be called any time a watch on a potentially cached item is triggered by creation/update/poll, e.g. in the ReferencedResourceManager
@@ -139,6 +127,45 @@ module.exports = class FetchEnvs {
139127
return this.#genericKeyRef(conf, 'configMapKeyRef');
140128
}
141129

130+
/*
131+
Single-resource queries are cacheable. If it's in the cache, use it.
132+
If not in the cache, start an api call to populate the cache if needed, wait for it to finish, then use it from the cache.
133+
*/
134+
async #getSingleResource( resource ) {
135+
const { apiVersion, kind, namespace, name } = resource;
136+
const cacheKey = [apiVersion, kind, namespace, name].join('/');
137+
138+
// Single-resource queries are cacheable. If it's in the cache, use it.
139+
if( this.resourceCache.has( cacheKey ) ) {
140+
resource = this.resourceCache.get( cacheKey );
141+
}
142+
// Single-resource queries are cacheable. If not in the cache, start an api call to populate the cache if needed, wait for it to finish, then use it from the cache.
143+
else {
144+
if( !singleResourceQueryCache[cacheKey] ) {
145+
singleResourceQueryCache[cacheKey] = ( async () => {
146+
try {
147+
const krm = await this.kubeClass.getKubeResourceMeta( apiVersion, kind, 'update' );
148+
if (krm) {
149+
resource = await krm.get( name, namespace );
150+
if( resource ) {
151+
this.resourceCache.set( cacheKey, resource ); // Cache this resource
152+
}
153+
}
154+
}
155+
finally {
156+
delete singleResourceQueryCache[cacheKey];
157+
}
158+
} )();
159+
}
160+
161+
await singleResourceQueryCache[cacheKey];
162+
163+
resource = this.resourceCache.get( cacheKey );
164+
}
165+
166+
return resource;
167+
}
168+
142169
/*
143170
@param[I] conf An object like `{ configMapRef: { name: 'asdf', namespace: 'asdf' } }`.
144171
@param[I] valueFrom The name of the conf attribute containing resource details, e.g. `configMapRef`.
@@ -147,8 +174,6 @@ module.exports = class FetchEnvs {
147174
@return An object like { configMapRef: { name: 'asdf', namespace: 'asdf' }, data: { key1: val1, ... } }
148175
*/
149176
async #genericMapRef(conf, valueFrom = 'genericMapRef', decode = false) {
150-
let resource;
151-
let kubeError = ERR_NODATA;
152177
const ref = conf[valueFrom];
153178
const optional = !!conf.optional;
154179

@@ -159,23 +184,14 @@ module.exports = class FetchEnvs {
159184
name
160185
} = ref;
161186

162-
const cacheKey = [apiVersion, kind, namespace, name].join('/');
163-
if( this.resourceCache.has( cacheKey ) ) {
164-
resource = this.resourceCache.get( cacheKey );
187+
let kubeError = ERR_NODATA;
188+
let resource;
189+
// Get single resource from cache or api call (with cache addition)
190+
try {
191+
resource = await this.#getSingleResource( { apiVersion, kind, namespace, name } );
165192
}
166-
else {
167-
const krm = await this.kubeClass.getKubeResourceMeta(apiVersion, kind, 'update');
168-
169-
if (krm) {
170-
try {
171-
resource = await krm.get(name, namespace);
172-
if( resource ) {
173-
this.resourceCache.set( cacheKey, resource ); // Cache this resource
174-
}
175-
} catch (error) {
176-
kubeError = error;
177-
}
178-
}
193+
catch( error ) {
194+
kubeError = error;
179195
}
180196

181197
const data = resource?.data;
@@ -209,46 +225,55 @@ module.exports = class FetchEnvs {
209225
@return The discovered value
210226
*/
211227
async #genericKeyRef(conf, valueFrom = 'genericKeyRef', decode = false) {
212-
let response;
213-
let kubeError = ERR_NODATA;
214228
const optional = !!conf.optional;
215229
const defaultValue = conf.default;
216230
const ref = conf.valueFrom[valueFrom];
217231
const strategy = conf.overrideStrategy;
218232
const {
233+
apiVersion = 'v1',
234+
kind = KIND_MAP.get(valueFrom),
235+
namespace = this.namespace,
219236
name,
220-
key,
221237
matchLabels,
238+
key,
222239
type,
223-
namespace = this.namespace,
224-
kind = KIND_MAP.get(valueFrom),
225-
apiVersion = 'v1'
226240
} = ref;
227241

228242
const matchLabelsQS = labelSelectors(matchLabels);
229243

230-
const cacheKey = [apiVersion, kind, namespace, name].join('/');
231-
// Note: Using `matchLabels` will always result in a kube api call, label-based queries cannot use the resourceCache
232-
if( !matchLabelsQS && this.resourceCache.has(cacheKey) ) {
233-
response = this.resourceCache.get(cacheKey);
234-
}
235-
else {
236-
const krm = await this.kubeClass.getKubeResourceMeta(apiVersion, kind, 'update');
244+
let kubeError = ERR_NODATA;
245+
let response;
246+
if( typeof matchLabelsQS === OBJECT ) {
247+
// Get multiple resources that match the specified labels
248+
// MatchLabels queries are not cached (though the resulting resources are cached)
249+
try {
250+
const krm = await this.kubeClass.getKubeResourceMeta(apiVersion, kind, 'update');
237251

238-
if (krm) {
239-
try {
252+
if (krm) {
240253
response = await this.api({
241254
uri: krm.uri({ namespace, name }),
242255
json: true,
243256
qs: matchLabelsQS
244257
});
245-
// Note: cache here only if getting a single resource
246-
if( response?.data && !response?.items ) {
247-
this.resourceCache.set(cacheKey, response);
258+
// Cache multiple resources
259+
if( response?.items ) {
260+
response.items.forEach(function (item) {
261+
const cacheKey = [item.apiVersion, item.kind, item.metadata.namespace, item.metadata.name].join('/');
262+
this.resourceCache.set(cacheKey, item);
263+
}, this);
248264
}
249-
} catch (error) {
250-
kubeError = error;
251265
}
266+
} catch (error) {
267+
kubeError = error;
268+
}
269+
}
270+
else {
271+
// Get single resource from cache or api call (with cache addition)
272+
try {
273+
response = await this.#getSingleResource( { apiVersion, kind, namespace, name } );
274+
}
275+
catch( error ) {
276+
kubeError = error;
252277
}
253278
}
254279

@@ -257,13 +282,6 @@ module.exports = class FetchEnvs {
257282
// If matching by labels, there can be multiple matching resources.
258283
// Reduce to a single value via the specified strategy ('merge' combines objects, otherwise a single value is picked).
259284
if (typeof matchLabelsQS === OBJECT) {
260-
// Cache here if there are multiple retrieved resources
261-
if( response?.items ) {
262-
response.items.forEach(function (item) {
263-
const cacheKey = [item.apiVersion, item.kind, item.metadata.namespace, item.metadata.name].join('/');
264-
this.resourceCache.set(cacheKey, item);
265-
}, this);
266-
}
267285
const output = response?.items.reduce(
268286
reduceItemList(ref, strategy, decode),
269287
Object.create(null)

0 commit comments

Comments
 (0)