cds-caching 3.0.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/app/dashboard/Component-preload.js +2 -2
- package/app/dashboard/Component-preload.js.map +1 -1
- package/app/dashboard/index.html +1 -1
- package/app/dashboard/manifest.json +3 -3
- package/app/dashboard/service/CacheStatisticsService-dbg.js +1 -1
- package/app/dashboard/service/CacheStatisticsService-dbg.js.map +1 -1
- package/app/dashboard/service/CacheStatisticsService.js +1 -1
- package/app/dashboard/service/CacheStatisticsService.js.map +1 -1
- package/app/dashboard-src/index.html +1 -1
- package/app/dashboard-src/manifest.json +3 -3
- package/app/dashboard-src/service/CacheStatisticsService.ts +1 -1
- package/db/statistics.cds +56 -0
- package/index.cds +5 -0
- package/lib/CachingService.d.ts +29 -0
- package/lib/CachingService.js +49 -0
- package/lib/add.js +1 -1
- package/lib/config-normalizer.js +2 -0
- package/lib/dashboard-bootstrap.js +1 -1
- package/lib/operations/AsyncOperations.js +52 -24
- package/lib/operations/BasicOperations.js +6 -3
- package/lib/operations/CapOperations.js +59 -27
- package/lib/support/CacheStatisticsHandler.js +258 -13
- package/lib/support/RuntimeConfigurationManager.js +21 -1
- package/lib/support/StatisticsPersistenceManager.js +133 -0
- package/package.json +2 -2
- package/srv/caching-api-service.js +25 -1
|
@@ -122,6 +122,22 @@ class AsyncOperations {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** @private */
|
|
126
|
+
_resolveTags(options, value) {
|
|
127
|
+
const resolver = this.cache?.tagResolver;
|
|
128
|
+
if (resolver) {
|
|
129
|
+
return resolver.resolveTags(options?.tags, value, {
|
|
130
|
+
user: cds.context?.user?.id,
|
|
131
|
+
tenant: cds.context?.tenant,
|
|
132
|
+
locale: cds.context?.locale,
|
|
133
|
+
}) || [];
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(options?.tags)) return [];
|
|
136
|
+
return options.tags
|
|
137
|
+
.map((t) => (typeof t === 'string' ? t : t?.value))
|
|
138
|
+
.filter((t) => typeof t === 'string' && t);
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
/** @private */
|
|
126
142
|
async _wrap(key, asyncFunction, options, args) {
|
|
127
143
|
// Create dynamic key based on function arguments
|
|
@@ -140,7 +156,8 @@ class AsyncOperations {
|
|
|
140
156
|
args: args,
|
|
141
157
|
cacheKey: cacheKey
|
|
142
158
|
}),
|
|
143
|
-
cacheOptions: JSON.stringify(options)
|
|
159
|
+
cacheOptions: JSON.stringify(options),
|
|
160
|
+
tags: this._resolveTags(options, undefined),
|
|
144
161
|
};
|
|
145
162
|
|
|
146
163
|
// Safely check if key exists in cache
|
|
@@ -156,16 +173,6 @@ class AsyncOperations {
|
|
|
156
173
|
if (hasKey) {
|
|
157
174
|
const latency = this.getElapsedMs(startTime);
|
|
158
175
|
|
|
159
|
-
// Safely record hit statistics
|
|
160
|
-
const hitStatsResult = await this.safeCacheOperation(
|
|
161
|
-
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
162
|
-
'recordHit',
|
|
163
|
-
{ key: cacheKey, latency }
|
|
164
|
-
);
|
|
165
|
-
if (!hitStatsResult.success) {
|
|
166
|
-
cacheErrors.push(hitStatsResult.error);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
176
|
// Safely get value from cache
|
|
170
177
|
const getResult = await this.safeCacheOperation(
|
|
171
178
|
() => this.cache.send("GET", { key: cacheKey }),
|
|
@@ -174,6 +181,19 @@ class AsyncOperations {
|
|
|
174
181
|
);
|
|
175
182
|
|
|
176
183
|
if (getResult.success && getResult.result?.value !== undefined) {
|
|
184
|
+
if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
|
|
185
|
+
metadata.tags = getResult.result.tags;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
189
|
+
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
190
|
+
'recordHit',
|
|
191
|
+
{ key: cacheKey, latency }
|
|
192
|
+
);
|
|
193
|
+
if (!hitStatsResult.success) {
|
|
194
|
+
cacheErrors.push(hitStatsResult.error);
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
return {
|
|
178
198
|
result: getResult.result.value,
|
|
179
199
|
cacheKey,
|
|
@@ -187,6 +207,8 @@ class AsyncOperations {
|
|
|
187
207
|
try {
|
|
188
208
|
const response = await asyncFunction(...args);
|
|
189
209
|
const latency = this.getElapsedMs(startTime);
|
|
210
|
+
const tags = this._resolveTags(options, response);
|
|
211
|
+
metadata.tags = tags;
|
|
190
212
|
|
|
191
213
|
// Safely record miss statistics
|
|
192
214
|
const missStatsResult = await this.safeCacheOperation(
|
|
@@ -201,7 +223,7 @@ class AsyncOperations {
|
|
|
201
223
|
// Safely store in cache
|
|
202
224
|
const wrappedValue = {
|
|
203
225
|
value: response,
|
|
204
|
-
tags
|
|
226
|
+
tags,
|
|
205
227
|
timestamp: Date.now()
|
|
206
228
|
};
|
|
207
229
|
|
|
@@ -281,7 +303,8 @@ class AsyncOperations {
|
|
|
281
303
|
args: args,
|
|
282
304
|
cacheKey: cacheKey
|
|
283
305
|
}),
|
|
284
|
-
cacheOptions: JSON.stringify(options)
|
|
306
|
+
cacheOptions: JSON.stringify(options),
|
|
307
|
+
tags: this._resolveTags(options, undefined),
|
|
285
308
|
};
|
|
286
309
|
|
|
287
310
|
// Safely check if key exists in cache
|
|
@@ -297,16 +320,6 @@ class AsyncOperations {
|
|
|
297
320
|
if (hasKey) {
|
|
298
321
|
const latency = this.getElapsedMs(startTime);
|
|
299
322
|
|
|
300
|
-
// Safely record hit statistics
|
|
301
|
-
const hitStatsResult = await this.safeCacheOperation(
|
|
302
|
-
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
303
|
-
'recordHit',
|
|
304
|
-
{ key: cacheKey, latency }
|
|
305
|
-
);
|
|
306
|
-
if (!hitStatsResult.success) {
|
|
307
|
-
cacheErrors.push(hitStatsResult.error);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
323
|
// Safely get value from cache
|
|
311
324
|
const getResult = await this.safeCacheOperation(
|
|
312
325
|
() => this.cache.send("GET", { key: cacheKey }),
|
|
@@ -315,6 +328,19 @@ class AsyncOperations {
|
|
|
315
328
|
);
|
|
316
329
|
|
|
317
330
|
if (getResult.success && getResult.result?.value !== undefined) {
|
|
331
|
+
if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
|
|
332
|
+
metadata.tags = getResult.result.tags;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
336
|
+
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
337
|
+
'recordHit',
|
|
338
|
+
{ key: cacheKey, latency }
|
|
339
|
+
);
|
|
340
|
+
if (!hitStatsResult.success) {
|
|
341
|
+
cacheErrors.push(hitStatsResult.error);
|
|
342
|
+
}
|
|
343
|
+
|
|
318
344
|
return {
|
|
319
345
|
result: getResult.result.value,
|
|
320
346
|
cacheKey,
|
|
@@ -328,6 +354,8 @@ class AsyncOperations {
|
|
|
328
354
|
try {
|
|
329
355
|
const response = await asyncFunction(...args);
|
|
330
356
|
const latency = this.getElapsedMs(startTime);
|
|
357
|
+
const tags = this._resolveTags(options, response);
|
|
358
|
+
metadata.tags = tags;
|
|
331
359
|
|
|
332
360
|
// Safely record miss statistics
|
|
333
361
|
const missStatsResult = await this.safeCacheOperation(
|
|
@@ -342,7 +370,7 @@ class AsyncOperations {
|
|
|
342
370
|
// Safely store in cache
|
|
343
371
|
const wrappedValue = {
|
|
344
372
|
value: response,
|
|
345
|
-
tags
|
|
373
|
+
tags,
|
|
346
374
|
timestamp: Date.now()
|
|
347
375
|
};
|
|
348
376
|
|
|
@@ -28,9 +28,10 @@ class BasicOperations {
|
|
|
28
28
|
* @returns {Promise<void>}
|
|
29
29
|
*/
|
|
30
30
|
async set(key, value, options = {}, tx = null) {
|
|
31
|
+
const tags = this.tagResolver.resolveTags(options.tags, value, options.params) || [];
|
|
31
32
|
const wrappedValue = {
|
|
32
33
|
value,
|
|
33
|
-
tags
|
|
34
|
+
tags,
|
|
34
35
|
timestamp: Date.now()
|
|
35
36
|
};
|
|
36
37
|
const createdKey = this.keyManager.createKey(key, {}, options.key);
|
|
@@ -48,7 +49,8 @@ class BasicOperations {
|
|
|
48
49
|
operation: 'SET',
|
|
49
50
|
operationType: 'BASIC',
|
|
50
51
|
metadata: JSON.stringify({ key: createdKey, ttl: options.ttl || 0 }),
|
|
51
|
-
cacheOptions: JSON.stringify(options)
|
|
52
|
+
cacheOptions: JSON.stringify(options),
|
|
53
|
+
tags,
|
|
52
54
|
};
|
|
53
55
|
this.statistics.recordNativeSet(createdKey, metadata);
|
|
54
56
|
|
|
@@ -84,7 +86,8 @@ class BasicOperations {
|
|
|
84
86
|
dataType: 'Operation',
|
|
85
87
|
operation: 'GET',
|
|
86
88
|
operationType: 'BASIC',
|
|
87
|
-
metadata: JSON.stringify({ key: createdKey })
|
|
89
|
+
metadata: JSON.stringify({ key: createdKey }),
|
|
90
|
+
tags: wrappedValue?.tags || [],
|
|
88
91
|
};
|
|
89
92
|
const isHit = wrappedValue !== undefined && wrappedValue !== null;
|
|
90
93
|
this.statistics.recordNativeGet(createdKey, isHit, metadata);
|
|
@@ -129,6 +129,13 @@ class CapOperations {
|
|
|
129
129
|
|
|
130
130
|
const key = this.keyManager.createKey(request, keyParts, requestOptions.key);
|
|
131
131
|
const startTime = process.hrtime();
|
|
132
|
+
const tagParams = {
|
|
133
|
+
...request.params,
|
|
134
|
+
user: request.user?.id,
|
|
135
|
+
tenant: request.tenant,
|
|
136
|
+
locale: request.locale,
|
|
137
|
+
hash: this.keyManager.createContentHash(request),
|
|
138
|
+
};
|
|
132
139
|
|
|
133
140
|
// Track the miss with total latency (cache lookup + backend operation)
|
|
134
141
|
const metadata = {
|
|
@@ -153,7 +160,9 @@ class CapOperations {
|
|
|
153
160
|
event: request.event,
|
|
154
161
|
headers: redactHeaders(request.headers),
|
|
155
162
|
}),
|
|
156
|
-
cacheOptions: JSON.stringify(requestOptions)
|
|
163
|
+
cacheOptions: JSON.stringify(requestOptions),
|
|
164
|
+
// Static tags resolve without a payload; data tags fill in on miss after the response.
|
|
165
|
+
tags: this.tagResolver.resolveTags(requestOptions.tags, undefined, tagParams) || [],
|
|
157
166
|
};
|
|
158
167
|
|
|
159
168
|
// Safely check if key exists in cache
|
|
@@ -169,17 +178,7 @@ class CapOperations {
|
|
|
169
178
|
if (hasKey) {
|
|
170
179
|
const latency = this.getElapsedMs(startTime);
|
|
171
180
|
|
|
172
|
-
// Safely
|
|
173
|
-
const hitStatsResult = await this.safeCacheOperation(
|
|
174
|
-
() => this.statistics.recordHit(latency, key, metadata),
|
|
175
|
-
'recordHit',
|
|
176
|
-
{ key, latency }
|
|
177
|
-
);
|
|
178
|
-
if (!hitStatsResult.success) {
|
|
179
|
-
cacheErrors.push(hitStatsResult.error);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Safely get value from cache
|
|
181
|
+
// Safely get value from cache (before recording so entry tags are available)
|
|
183
182
|
const getResult = await this.safeCacheOperation(
|
|
184
183
|
() => this.cache.send("GET", { key }),
|
|
185
184
|
'get',
|
|
@@ -187,6 +186,19 @@ class CapOperations {
|
|
|
187
186
|
);
|
|
188
187
|
|
|
189
188
|
if (getResult.success && getResult.result?.value !== undefined) {
|
|
189
|
+
if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
|
|
190
|
+
metadata.tags = getResult.result.tags;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
194
|
+
() => this.statistics.recordHit(latency, key, metadata),
|
|
195
|
+
'recordHit',
|
|
196
|
+
{ key, latency }
|
|
197
|
+
);
|
|
198
|
+
if (!hitStatsResult.success) {
|
|
199
|
+
cacheErrors.push(hitStatsResult.error);
|
|
200
|
+
}
|
|
201
|
+
|
|
190
202
|
return {
|
|
191
203
|
result: getResult.result.value,
|
|
192
204
|
cacheKey: key,
|
|
@@ -200,6 +212,8 @@ class CapOperations {
|
|
|
200
212
|
try {
|
|
201
213
|
const response = await service.send(request);
|
|
202
214
|
const totalLatency = this.getElapsedMs(startTime);
|
|
215
|
+
const tags = this.tagResolver.resolveTags(requestOptions.tags, response, tagParams) || [];
|
|
216
|
+
metadata.tags = tags;
|
|
203
217
|
|
|
204
218
|
// Safely record miss statistics
|
|
205
219
|
const missStatsResult = await this.safeCacheOperation(
|
|
@@ -214,7 +228,7 @@ class CapOperations {
|
|
|
214
228
|
// Safely store in cache
|
|
215
229
|
const wrappedValue = {
|
|
216
230
|
value: response,
|
|
217
|
-
tags
|
|
231
|
+
tags,
|
|
218
232
|
timestamp: Date.now()
|
|
219
233
|
};
|
|
220
234
|
|
|
@@ -311,9 +325,14 @@ class CapOperations {
|
|
|
311
325
|
const cacheHit = getResult.success && getResult.result?.value !== undefined;
|
|
312
326
|
const cacheLatency = this.getElapsedMs(startTime);
|
|
313
327
|
const metadata = this.extractMetadataFromRequest(req);
|
|
328
|
+
const tagParams = { ...req.params, hash: this.keyManager.createContentHash(req) };
|
|
314
329
|
const cacheErrors = [];
|
|
315
330
|
|
|
316
331
|
if (cacheHit) {
|
|
332
|
+
metadata.tags = (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0)
|
|
333
|
+
? getResult.result.tags
|
|
334
|
+
: (this.tagResolver.resolveTags(req.cacheOptions.tags, getResult.result.value, tagParams) || []);
|
|
335
|
+
|
|
317
336
|
// Cache hit
|
|
318
337
|
const hitStatsResult = await this.safeCacheOperation(
|
|
319
338
|
() => this.statistics.recordHit(cacheLatency, req.cacheKey, metadata),
|
|
@@ -336,6 +355,8 @@ class CapOperations {
|
|
|
336
355
|
try {
|
|
337
356
|
const response = await next();
|
|
338
357
|
const totalLatency = this.getElapsedMs(startTime);
|
|
358
|
+
const tags = this.tagResolver.resolveTags(req.cacheOptions.tags, response, tagParams) || [];
|
|
359
|
+
metadata.tags = tags;
|
|
339
360
|
|
|
340
361
|
// Safely record miss statistics
|
|
341
362
|
const missStatsResult = await this.safeCacheOperation(
|
|
@@ -352,7 +373,7 @@ class CapOperations {
|
|
|
352
373
|
// Safely store in cache
|
|
353
374
|
const wrappedValue = {
|
|
354
375
|
value: response,
|
|
355
|
-
tags
|
|
376
|
+
tags,
|
|
356
377
|
timestamp: Date.now()
|
|
357
378
|
};
|
|
358
379
|
|
|
@@ -407,6 +428,7 @@ class CapOperations {
|
|
|
407
428
|
|
|
408
429
|
const hasCachedValue = hasCachedValueResult.success && hasCachedValueResult.result;
|
|
409
430
|
const cacheLatency = this.getElapsedMs(startTime);
|
|
431
|
+
const tagParams = { ...query.params, hash: this.keyManager.createContentHash(query) };
|
|
410
432
|
const metadata = {
|
|
411
433
|
dataType: 'Query',
|
|
412
434
|
operation: 'SELECT',
|
|
@@ -423,21 +445,12 @@ class CapOperations {
|
|
|
423
445
|
serviceName: srv?.name,
|
|
424
446
|
path: query.path,
|
|
425
447
|
}),
|
|
426
|
-
cacheOptions: JSON.stringify(options)
|
|
448
|
+
cacheOptions: JSON.stringify(options),
|
|
449
|
+
tags: this.tagResolver.resolveTags(options.tags, undefined, tagParams) || [],
|
|
427
450
|
};
|
|
428
451
|
const cacheErrors = [];
|
|
429
452
|
|
|
430
453
|
if (hasCachedValue) {
|
|
431
|
-
// Cache hit
|
|
432
|
-
const hitStatsResult = await this.safeCacheOperation(
|
|
433
|
-
() => this.statistics.recordHit(cacheLatency, query.cacheKey, metadata),
|
|
434
|
-
'recordHit',
|
|
435
|
-
{ key: query.cacheKey, latency: cacheLatency }
|
|
436
|
-
);
|
|
437
|
-
if (!hitStatsResult.success) {
|
|
438
|
-
cacheErrors.push(hitStatsResult.error);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
454
|
const getResult = await this.safeCacheOperation(
|
|
442
455
|
() => this.cache.send("GET", { key: query.cacheKey }),
|
|
443
456
|
'get',
|
|
@@ -445,6 +458,19 @@ class CapOperations {
|
|
|
445
458
|
);
|
|
446
459
|
|
|
447
460
|
if (getResult.success && getResult.result?.value !== undefined) {
|
|
461
|
+
if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
|
|
462
|
+
metadata.tags = getResult.result.tags;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
466
|
+
() => this.statistics.recordHit(cacheLatency, query.cacheKey, metadata),
|
|
467
|
+
'recordHit',
|
|
468
|
+
{ key: query.cacheKey, latency: cacheLatency }
|
|
469
|
+
);
|
|
470
|
+
if (!hitStatsResult.success) {
|
|
471
|
+
cacheErrors.push(hitStatsResult.error);
|
|
472
|
+
}
|
|
473
|
+
|
|
448
474
|
return {
|
|
449
475
|
result: getResult.result.value,
|
|
450
476
|
cacheKey: query.cacheKey,
|
|
@@ -458,6 +484,8 @@ class CapOperations {
|
|
|
458
484
|
try {
|
|
459
485
|
const data = await srv.run(query);
|
|
460
486
|
const totalLatency = this.getElapsedMs(startTime);
|
|
487
|
+
const tags = this.tagResolver.resolveTags(options.tags, data, tagParams) || [];
|
|
488
|
+
metadata.tags = tags;
|
|
461
489
|
|
|
462
490
|
// Safely record miss statistics
|
|
463
491
|
const missStatsResult = await this.safeCacheOperation(
|
|
@@ -472,7 +500,7 @@ class CapOperations {
|
|
|
472
500
|
// Safely store in cache
|
|
473
501
|
const wrappedValue = {
|
|
474
502
|
value: data,
|
|
475
|
-
tags
|
|
503
|
+
tags,
|
|
476
504
|
timestamp: Date.now()
|
|
477
505
|
};
|
|
478
506
|
|
|
@@ -590,7 +618,11 @@ class CapOperations {
|
|
|
590
618
|
path: req.http?.req?.path,
|
|
591
619
|
url: req.http?.req?.url
|
|
592
620
|
}),
|
|
593
|
-
cacheOptions: JSON.stringify(req.cacheOptions)
|
|
621
|
+
cacheOptions: JSON.stringify(req.cacheOptions),
|
|
622
|
+
tags: this.tagResolver.resolveTags(req.cacheOptions?.tags, undefined, {
|
|
623
|
+
...req.params,
|
|
624
|
+
hash: this.keyManager.createContentHash(req),
|
|
625
|
+
}) || [],
|
|
594
626
|
};
|
|
595
627
|
|
|
596
628
|
return metadata;
|