cds-caching 3.0.0 → 3.1.0
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/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/config-normalizer.js +2 -0
- 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 +137 -4
- package/package.json +1 -1
- package/srv/caching-api-service.js +25 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ This uses the in-memory store — no additional setup needed for development.
|
|
|
77
77
|
|
|
78
78
|
The plugin ships CDS entity definitions for database-backed features. These load **conditionally** based on your configuration — no manual `model` property needed.
|
|
79
79
|
|
|
80
|
-
> **Full guide:** [Feature Activation](docs/feature-activation.md) — decision tree, BTP/MTX
|
|
80
|
+
> **Full guide:** [Feature Activation](docs/feature-activation.md) — decision tree, BTP/MTX guidance, and rules for avoiding duplicate model loading.
|
|
81
81
|
|
|
82
82
|
| Option | Entities loaded | Purpose |
|
|
83
83
|
|--------|-----------------|---------|
|
|
@@ -258,7 +258,7 @@ For detailed key configuration and deployment instructions, see [Key Management]
|
|
|
258
258
|
|
|
259
259
|
The plugin includes `CachingApiService`, an OData service for managing caches, browsing entries, and viewing metrics. It powers the [dashboard](docs/dashboard.md) and can be consumed by any OData client.
|
|
260
260
|
|
|
261
|
-
See **[Feature Activation](docs/feature-activation.md)** for reuse vs own activation, BTP/MTX
|
|
261
|
+
See **[Feature Activation](docs/feature-activation.md)** for reuse vs own activation, BTP/MTX guidance, and API authorization.
|
|
262
262
|
|
|
263
263
|
**Quick reference:**
|
|
264
264
|
|
package/db/statistics.cds
CHANGED
|
@@ -5,10 +5,13 @@ entity Caches {
|
|
|
5
5
|
config : String;
|
|
6
6
|
metricsEnabled : Boolean default false;
|
|
7
7
|
keyMetricsEnabled : Boolean default false;
|
|
8
|
+
tagMetricsEnabled : Boolean default false;
|
|
8
9
|
metrics : Composition of many Metrics
|
|
9
10
|
on metrics.cache = $self.name;
|
|
10
11
|
keyMetrics : Composition of many KeyMetrics
|
|
11
12
|
on keyMetrics.cache = $self.name;
|
|
13
|
+
tagMetrics : Composition of many TagMetrics
|
|
14
|
+
on tagMetrics.cache = $self.name;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
entity Metrics {
|
|
@@ -126,3 +129,56 @@ entity KeyMetrics {
|
|
|
126
129
|
timestamp : DateTime; // When this key was first accessed
|
|
127
130
|
cacheOptions : String; // JSON string with cache options
|
|
128
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Per-tag metrics. Opt-in via `tagMetricsEnabled` / `metrics.tagMetricsEnabled`.
|
|
135
|
+
*
|
|
136
|
+
* An entry can carry several tags, so a single hit increments every matching tag
|
|
137
|
+
* row. Tag totals can therefore exceed cache-level `Metrics` totals — that is
|
|
138
|
+
* expected, not a bug. Query by the resolved tag string (exact match), e.g.
|
|
139
|
+
* `tag eq 'federation:Airports'`.
|
|
140
|
+
*/
|
|
141
|
+
entity TagMetrics {
|
|
142
|
+
key ID : String;
|
|
143
|
+
key cache : String;
|
|
144
|
+
key tag : String;
|
|
145
|
+
lastAccess : DateTime;
|
|
146
|
+
period : String enum {
|
|
147
|
+
current;
|
|
148
|
+
hourly;
|
|
149
|
+
daily;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Read-through metrics (hits and misses only)
|
|
153
|
+
hits : Integer default 0;
|
|
154
|
+
misses : Integer default 0;
|
|
155
|
+
errors : Integer default 0;
|
|
156
|
+
totalRequests : Integer default 0;
|
|
157
|
+
hitRatio : Double;
|
|
158
|
+
cacheEfficiency : Double;
|
|
159
|
+
|
|
160
|
+
// Read-through latency metrics
|
|
161
|
+
avgHitLatency : Double;
|
|
162
|
+
minHitLatency : Double;
|
|
163
|
+
maxHitLatency : Double;
|
|
164
|
+
avgMissLatency : Double;
|
|
165
|
+
minMissLatency : Double;
|
|
166
|
+
maxMissLatency : Double;
|
|
167
|
+
avgReadThroughLatency : Double;
|
|
168
|
+
|
|
169
|
+
// Read-through performance metrics
|
|
170
|
+
throughput : Double;
|
|
171
|
+
errorRate : Double;
|
|
172
|
+
|
|
173
|
+
// Native function metrics (counts only)
|
|
174
|
+
nativeHits : Integer default 0;
|
|
175
|
+
nativeMisses : Integer default 0;
|
|
176
|
+
nativeSets : Integer default 0;
|
|
177
|
+
nativeDeletes : Integer default 0;
|
|
178
|
+
nativeErrors : Integer default 0;
|
|
179
|
+
totalNativeOperations : Integer default 0;
|
|
180
|
+
nativeThroughput : Double;
|
|
181
|
+
nativeErrorRate : Double;
|
|
182
|
+
|
|
183
|
+
timestamp : DateTime;
|
|
184
|
+
}
|
package/index.cds
CHANGED
|
@@ -39,8 +39,10 @@ context plugin.cds_caching {
|
|
|
39
39
|
action clear() returns Boolean;
|
|
40
40
|
action clearMetrics() returns Boolean;
|
|
41
41
|
action clearKeyMetrics() returns Boolean;
|
|
42
|
+
action clearTagMetrics() returns Boolean;
|
|
42
43
|
action setMetricsEnabled(enabled : Boolean) returns Boolean;
|
|
43
44
|
action setKeyMetricsEnabled(enabled : Boolean) returns Boolean;
|
|
45
|
+
action setTagMetricsEnabled(enabled : Boolean) returns Boolean;
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
@readonly
|
|
@@ -49,5 +51,8 @@ context plugin.cds_caching {
|
|
|
49
51
|
@readonly
|
|
50
52
|
entity KeyMetrics as projection on plugin.cds_caching.KeyMetrics;
|
|
51
53
|
|
|
54
|
+
@readonly
|
|
55
|
+
entity TagMetrics as projection on plugin.cds_caching.TagMetrics;
|
|
56
|
+
|
|
52
57
|
}
|
|
53
58
|
}
|
package/lib/CachingService.d.ts
CHANGED
|
@@ -58,6 +58,15 @@ export interface CacheKeyMetrics {
|
|
|
58
58
|
lastAccessed: Date;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export interface CacheTagMetrics {
|
|
62
|
+
tag: string;
|
|
63
|
+
hits: number;
|
|
64
|
+
misses: number;
|
|
65
|
+
totalRequests: number;
|
|
66
|
+
hitRatio: number;
|
|
67
|
+
lastAccess: Date;
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
export interface CachableFunctionOptions {
|
|
62
71
|
'@cache.ttl'?: number;
|
|
63
72
|
'@cache.key'?: string | object;
|
|
@@ -280,6 +289,16 @@ export declare class CachingService extends Service {
|
|
|
280
289
|
*/
|
|
281
290
|
getCurrentKeyMetrics(): Promise<CacheKeyMetrics>;
|
|
282
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Get tag-specific metrics for a specific period
|
|
294
|
+
*/
|
|
295
|
+
getTagMetrics(tag: string, from: Date, to: Date): Promise<CacheTagMetrics>;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get current tag metrics
|
|
299
|
+
*/
|
|
300
|
+
getCurrentTagMetrics(): Promise<Map<string, CacheTagMetrics> | null>;
|
|
301
|
+
|
|
283
302
|
/**
|
|
284
303
|
* Clear all metrics
|
|
285
304
|
*/
|
|
@@ -290,6 +309,11 @@ export declare class CachingService extends Service {
|
|
|
290
309
|
*/
|
|
291
310
|
clearKeyMetrics(): Promise<void>;
|
|
292
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Clear tag metrics
|
|
314
|
+
*/
|
|
315
|
+
clearTagMetrics(): Promise<void>;
|
|
316
|
+
|
|
293
317
|
/**
|
|
294
318
|
* Enable or disable statistics at runtime
|
|
295
319
|
*/
|
|
@@ -300,6 +324,11 @@ export declare class CachingService extends Service {
|
|
|
300
324
|
*/
|
|
301
325
|
setKeyMetricsEnabled(enabled: boolean): Promise<void>;
|
|
302
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Enable or disable tag tracking at runtime
|
|
329
|
+
*/
|
|
330
|
+
setTagMetricsEnabled(enabled: boolean): Promise<void>;
|
|
331
|
+
|
|
303
332
|
/**
|
|
304
333
|
* Persist metrics to database
|
|
305
334
|
*/
|
package/lib/CachingService.js
CHANGED
|
@@ -129,12 +129,16 @@ class CachingService extends cds.Service {
|
|
|
129
129
|
|
|
130
130
|
let metricsEnabled = config.metricsEnabled;
|
|
131
131
|
let keyMetricsEnabled = config.keyMetricsEnabled;
|
|
132
|
+
let tagMetricsEnabled = config.tagMetricsEnabled;
|
|
132
133
|
if (normalized.metrics?.enabled === true) {
|
|
133
134
|
metricsEnabled = true;
|
|
134
135
|
}
|
|
135
136
|
if (normalized.metrics?.keyMetricsEnabled === true) {
|
|
136
137
|
keyMetricsEnabled = true;
|
|
137
138
|
}
|
|
139
|
+
if (normalized.metrics?.tagMetricsEnabled === true) {
|
|
140
|
+
tagMetricsEnabled = true;
|
|
141
|
+
}
|
|
138
142
|
|
|
139
143
|
// Initialize statistics with runtime configuration support
|
|
140
144
|
this.statistics = new CacheStatisticsHandler({
|
|
@@ -142,6 +146,7 @@ class CachingService extends cds.Service {
|
|
|
142
146
|
...statsOpts,
|
|
143
147
|
});
|
|
144
148
|
this.statistics.enableKeyMetrics(keyMetricsEnabled);
|
|
149
|
+
this.statistics.enableTagMetrics(tagMetricsEnabled);
|
|
145
150
|
this.statistics.enableMetrics(metricsEnabled);
|
|
146
151
|
|
|
147
152
|
if (normalized.metrics?.enabled === true && isPluginModelAvailable()) {
|
|
@@ -158,6 +163,13 @@ class CachingService extends cds.Service {
|
|
|
158
163
|
this.log.warn(`Failed to persist keyMetricsEnabled from config for ${this.name}:`, error);
|
|
159
164
|
}
|
|
160
165
|
}
|
|
166
|
+
if (normalized.metrics?.tagMetricsEnabled === true && isPluginModelAvailable()) {
|
|
167
|
+
try {
|
|
168
|
+
await this.runtimeConfigManager.setTagMetricsEnabled(true);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.log.warn(`Failed to persist tagMetricsEnabled from config for ${this.name}:`, error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
161
173
|
|
|
162
174
|
// Initialize operation managers
|
|
163
175
|
this.basicOperations = new BasicOperations(
|
|
@@ -402,6 +414,25 @@ class CachingService extends cds.Service {
|
|
|
402
414
|
return this.statistics.getCurrentKeyMetrics();
|
|
403
415
|
}
|
|
404
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Get tag-specific metrics for a specific period
|
|
419
|
+
* @param {string} tag - resolved tag string
|
|
420
|
+
* @param {Date} from
|
|
421
|
+
* @param {Date} to
|
|
422
|
+
* @returns {Promise<object>}
|
|
423
|
+
*/
|
|
424
|
+
async getTagMetrics(tag, from, to) {
|
|
425
|
+
return this.statistics.getTagMetrics(tag, from, to);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get current tag metrics
|
|
430
|
+
* @returns {Promise<Map|null>}
|
|
431
|
+
*/
|
|
432
|
+
async getCurrentTagMetrics() {
|
|
433
|
+
return this.statistics.getCurrentTagMetrics();
|
|
434
|
+
}
|
|
435
|
+
|
|
405
436
|
|
|
406
437
|
|
|
407
438
|
/**
|
|
@@ -420,6 +451,14 @@ class CachingService extends cds.Service {
|
|
|
420
451
|
return this.statistics.clearKeyMetrics();
|
|
421
452
|
}
|
|
422
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Clear tag metrics
|
|
456
|
+
* @returns {Promise<void>}
|
|
457
|
+
*/
|
|
458
|
+
async clearTagMetrics() {
|
|
459
|
+
return this.statistics.clearTagMetrics();
|
|
460
|
+
}
|
|
461
|
+
|
|
423
462
|
/**
|
|
424
463
|
* Enable or disable statistics at runtime
|
|
425
464
|
* @param {boolean} enabled - whether to enable statistics
|
|
@@ -440,6 +479,16 @@ class CachingService extends cds.Service {
|
|
|
440
479
|
this.statistics.enableKeyMetrics(enabled);
|
|
441
480
|
}
|
|
442
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Enable or disable tag tracking at runtime
|
|
484
|
+
* @param {boolean} enabled - whether to enable tag tracking
|
|
485
|
+
* @returns {Promise<void>}
|
|
486
|
+
*/
|
|
487
|
+
async setTagMetricsEnabled(enabled) {
|
|
488
|
+
await this.runtimeConfigManager.setTagMetricsEnabled(enabled);
|
|
489
|
+
this.statistics.enableTagMetrics(enabled);
|
|
490
|
+
}
|
|
491
|
+
|
|
443
492
|
/**
|
|
444
493
|
* Persist metrics to database
|
|
445
494
|
* @returns {Promise<void>}
|
package/lib/config-normalizer.js
CHANGED
|
@@ -97,9 +97,11 @@ function getStatisticsHandlerOptions(metrics) {
|
|
|
97
97
|
if (metrics.persistenceInterval != null) opts.persistenceInterval = metrics.persistenceInterval
|
|
98
98
|
if (metrics.maxLatencies != null) opts.maxLatencies = metrics.maxLatencies
|
|
99
99
|
if (metrics.maxKeyMetrics != null) opts.maxKeyMetrics = metrics.maxKeyMetrics
|
|
100
|
+
if (metrics.maxTagMetrics != null) opts.maxTagMetrics = metrics.maxTagMetrics
|
|
100
101
|
if (metrics.maxMetricFieldLength != null) opts.maxMetricFieldLength = metrics.maxMetricFieldLength
|
|
101
102
|
if (metrics.enabled === true) opts.metricsEnabled = true
|
|
102
103
|
if (metrics.keyMetricsEnabled === true) opts.keyMetricsEnabled = true
|
|
104
|
+
if (metrics.tagMetricsEnabled === true) opts.tagMetricsEnabled = true
|
|
103
105
|
return opts
|
|
104
106
|
}
|
|
105
107
|
|
|
@@ -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;
|