cds-caching 1.2.0 → 1.3.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 +195 -1114
- package/app/dashboard/Component-dbg.js +59 -0
- package/app/dashboard/Component-dbg.js.map +1 -0
- package/app/dashboard/Component.js +2 -0
- package/app/dashboard/Component.js.map +1 -0
- package/app/dashboard/Component.ts +59 -0
- package/app/dashboard/controller/App-dbg.controller.js +132 -0
- package/app/dashboard/controller/App-dbg.controller.js.map +1 -0
- package/app/dashboard/controller/App.controller.js +2 -0
- package/app/dashboard/controller/App.controller.js.map +1 -0
- package/app/dashboard/controller/App.controller.ts +147 -0
- package/app/dashboard/controller/BaseController-dbg.js +75 -0
- package/app/dashboard/controller/BaseController-dbg.js.map +1 -0
- package/app/dashboard/controller/BaseController.js +2 -0
- package/app/dashboard/controller/BaseController.js.map +1 -0
- package/app/dashboard/controller/BaseController.ts +88 -0
- package/app/dashboard/controller/Cache-dbg.controller.js +421 -0
- package/app/dashboard/controller/Cache-dbg.controller.js.map +1 -0
- package/app/dashboard/controller/Cache.controller.js +2 -0
- package/app/dashboard/controller/Cache.controller.js.map +1 -0
- package/app/dashboard/controller/Cache.controller.ts +490 -0
- package/app/dashboard/controller/Main-dbg.controller.js +32 -0
- package/app/dashboard/controller/Main-dbg.controller.js.map +1 -0
- package/app/dashboard/controller/Main.controller.js +2 -0
- package/app/dashboard/controller/Main.controller.js.map +1 -0
- package/app/dashboard/controller/Main.controller.ts +31 -0
- package/app/dashboard/controller/SingleMetric-dbg.controller.js +306 -0
- package/app/dashboard/controller/SingleMetric-dbg.controller.js.map +1 -0
- package/app/dashboard/controller/SingleMetric.controller.js +2 -0
- package/app/dashboard/controller/SingleMetric.controller.js.map +1 -0
- package/app/dashboard/controller/SingleMetric.controller.ts +335 -0
- package/app/dashboard/i18n/i18n.properties +61 -0
- package/app/dashboard/i18n/i18n_de.properties +3 -0
- package/app/dashboard/i18n/i18n_en.properties +3 -0
- package/app/dashboard/index-cdn.html +28 -0
- package/app/dashboard/index.html +19 -0
- package/app/dashboard/manifest.json +145 -0
- package/app/dashboard/model/formatter-dbg.js +253 -0
- package/app/dashboard/model/formatter-dbg.js.map +1 -0
- package/app/dashboard/model/formatter.js +2 -0
- package/app/dashboard/model/formatter.js.map +1 -0
- package/app/dashboard/model/formatter.ts +275 -0
- package/app/dashboard/model/models-dbg.js +13 -0
- package/app/dashboard/model/models-dbg.js.map +1 -0
- package/app/dashboard/model/models.js +2 -0
- package/app/dashboard/model/models.js.map +1 -0
- package/app/dashboard/model/models.ts +13 -0
- package/app/dashboard/resources/sap-ui-custom-dbg.js +4695 -0
- package/app/dashboard/resources/sap-ui-custom-dbg.js.map +1 -0
- package/app/dashboard/resources/sap-ui-custom.js +5794 -0
- package/app/dashboard/resources/sap-ui-custom.js.map +1 -0
- package/app/dashboard/service/CacheStatisticsService-dbg.js +267 -0
- package/app/dashboard/service/CacheStatisticsService-dbg.js.map +1 -0
- package/app/dashboard/service/CacheStatisticsService.js +2 -0
- package/app/dashboard/service/CacheStatisticsService.js.map +1 -0
- package/app/dashboard/service/CacheStatisticsService.ts +397 -0
- package/app/dashboard/view/App.view.xml +18 -0
- package/app/dashboard/view/Cache.view.xml +777 -0
- package/app/dashboard/view/Main.view.xml +57 -0
- package/app/dashboard/view/SingleMetric.view.xml +337 -0
- package/cds-plugin.js +52 -0
- package/db/cache-store.cds +12 -0
- package/db/statistics.cds +128 -0
- package/index.cds +2 -128
- package/lib/CachingService.js +13 -0
- package/lib/add.js +17 -0
- package/lib/operations/AsyncOperations.js +162 -111
- package/lib/operations/BasicOperations.js +35 -0
- package/lib/operations/CapOperations.js +92 -40
- package/lib/support/CacheStatisticsHandler.js +35 -10
- package/lib/support/CacheStoreManager.js +29 -0
- package/lib/support/KeyvCDS.js +149 -0
- package/lib/support/MultitenancyDetector.js +25 -0
- package/lib/support/RuntimeConfigurationManager.js +69 -37
- package/lib/support/StatisticsPersistenceManager.js +18 -2
- package/lib/support/Telemetry.js +137 -0
- package/lib/util.js +29 -11
- package/package.json +44 -12
- package/srv/caching-api-service.js +47 -2
package/index.cds
CHANGED
|
@@ -1,132 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
entity Caches {
|
|
4
|
-
key name : String;
|
|
5
|
-
config : String;
|
|
6
|
-
metricsEnabled : Boolean default false;
|
|
7
|
-
keyMetricsEnabled : Boolean default false;
|
|
8
|
-
metrics : Composition of many Metrics
|
|
9
|
-
on metrics.cache = $self.name;
|
|
10
|
-
keyMetrics : Composition of many KeyMetrics
|
|
11
|
-
on keyMetrics.cache = $self.name;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
entity Metrics {
|
|
15
|
-
key ID : String; // e.g., 'daily:2024-03-20' or 'hourly:2024-03-20-15'
|
|
16
|
-
key cache : String;
|
|
17
|
-
timestamp : DateTime;
|
|
18
|
-
period : String enum {
|
|
19
|
-
hourly;
|
|
20
|
-
daily;
|
|
21
|
-
monthly;
|
|
22
|
-
}; // Granularity
|
|
23
|
-
|
|
24
|
-
// Read-through metrics (hits and misses only)
|
|
25
|
-
hits : Integer default 0;
|
|
26
|
-
misses : Integer default 0;
|
|
27
|
-
errors : Integer default 0;
|
|
28
|
-
totalRequests : Integer default 0;
|
|
29
|
-
// Read-through latency metrics
|
|
30
|
-
avgHitLatency : Double; // average hit latency in milliseconds
|
|
31
|
-
minHitLatency : Double; // minimum hit latency
|
|
32
|
-
maxHitLatency : Double; // maximum hit latency
|
|
33
|
-
avgMissLatency : Double; // average miss latency in milliseconds
|
|
34
|
-
minMissLatency : Double; // minimum miss latency
|
|
35
|
-
maxMissLatency : Double; // maximum miss latency
|
|
36
|
-
avgReadThroughLatency : Double; // average read through latency in milliseconds
|
|
37
|
-
|
|
38
|
-
// Read-through performance metrics
|
|
39
|
-
hitRatio : Double; // hit ratio as percentage
|
|
40
|
-
throughput : Double; // requests per second
|
|
41
|
-
errorRate : Double; // error rate as percentage
|
|
42
|
-
cacheEfficiency : Double; // ratio of miss latency to hit latency
|
|
43
|
-
|
|
44
|
-
// Native function metrics (basic counts only)
|
|
45
|
-
nativeSets : Integer default 0;
|
|
46
|
-
nativeGets : Integer default 0;
|
|
47
|
-
nativeDeletes : Integer default 0;
|
|
48
|
-
nativeClears : Integer default 0;
|
|
49
|
-
nativeDeleteByTags : Integer default 0;
|
|
50
|
-
nativeErrors : Integer default 0;
|
|
51
|
-
totalNativeOperations : Integer default 0;
|
|
52
|
-
// Native function performance metrics
|
|
53
|
-
nativeThroughput : Double; // native operations per second
|
|
54
|
-
nativeErrorRate : Double; // native operation error rate
|
|
55
|
-
|
|
56
|
-
// Common metrics
|
|
57
|
-
memoryUsage : Integer; // in bytes
|
|
58
|
-
itemCount : Integer;
|
|
59
|
-
uptimeMs : Integer; // uptime in milliseconds
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
entity KeyMetrics {
|
|
63
|
-
key ID : String;
|
|
64
|
-
key cache : String;
|
|
65
|
-
key keyName : String;
|
|
66
|
-
lastAccess : DateTime;
|
|
67
|
-
period : String enum {
|
|
68
|
-
current;
|
|
69
|
-
hourly;
|
|
70
|
-
daily;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Operation type tracking
|
|
74
|
-
operationType : String enum {
|
|
75
|
-
read_through;
|
|
76
|
-
native;
|
|
77
|
-
mixed;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Read-through metrics (hits and misses only)
|
|
81
|
-
hits : Integer default 0;
|
|
82
|
-
misses : Integer default 0;
|
|
83
|
-
errors : Integer default 0;
|
|
84
|
-
totalRequests : Integer default 0;
|
|
85
|
-
hitRatio : Double; // hit ratio as percentage
|
|
86
|
-
cacheEfficiency : Double; // ratio of miss latency to hit latency
|
|
87
|
-
|
|
88
|
-
// Read-through latency metrics
|
|
89
|
-
avgHitLatency : Double; // average hit latency in milliseconds
|
|
90
|
-
minHitLatency : Double; // minimum hit latency
|
|
91
|
-
maxHitLatency : Double; // maximum hit latency
|
|
92
|
-
avgMissLatency : Double; // average miss latency in milliseconds
|
|
93
|
-
minMissLatency : Double; // minimum miss latency
|
|
94
|
-
maxMissLatency : Double; // maximum miss latency
|
|
95
|
-
avgReadThroughLatency : Double; // average read through latency in milliseconds
|
|
96
|
-
|
|
97
|
-
// Read-through performance metrics
|
|
98
|
-
throughput : Double; // requests per second
|
|
99
|
-
errorRate : Double; // error rate as percentage
|
|
100
|
-
|
|
101
|
-
// Native function metrics (counts only)
|
|
102
|
-
nativeHits : Integer default 0;
|
|
103
|
-
nativeMisses : Integer default 0;
|
|
104
|
-
nativeSets : Integer default 0;
|
|
105
|
-
nativeDeletes : Integer default 0;
|
|
106
|
-
nativeClears : Integer default 0;
|
|
107
|
-
nativeDeleteByTags : Integer default 0;
|
|
108
|
-
nativeErrors : Integer default 0;
|
|
109
|
-
totalNativeOperations : Integer default 0;
|
|
110
|
-
// Native function performance metrics
|
|
111
|
-
nativeThroughput : Double; // native operations per second
|
|
112
|
-
nativeErrorRate : Double; // native operation error rate
|
|
113
|
-
|
|
114
|
-
// Metadata fields
|
|
115
|
-
dataType : String;
|
|
116
|
-
operation : String; // concatenated cache service operations (e.g., SET, GET, WRAP, RUN...)
|
|
117
|
-
metadata : LargeString; // JSON string for additional metadata
|
|
118
|
-
// Enhanced context information
|
|
119
|
-
context : String; // JSON string with detailed context
|
|
120
|
-
query : LargeString; // CQL query text if applicable
|
|
121
|
-
subject : LargeString; // JSON string with subject information
|
|
122
|
-
target : String; // Target information
|
|
123
|
-
tenant : String; // Tenant information
|
|
124
|
-
user : String; // User information
|
|
125
|
-
locale : String; // Locale information
|
|
126
|
-
timestamp : DateTime; // When this key was first accessed
|
|
127
|
-
cacheOptions : String; // JSON string with cache options
|
|
128
|
-
}
|
|
1
|
+
using from './db/statistics';
|
|
129
2
|
|
|
3
|
+
context plugin.cds_caching {
|
|
130
4
|
|
|
131
5
|
@impl: 'cds-caching/srv/caching-api-service'
|
|
132
6
|
service CachingApiService {
|
package/lib/CachingService.js
CHANGED
|
@@ -7,6 +7,8 @@ const CacheStatisticsHandler = require('./support/CacheStatisticsHandler');
|
|
|
7
7
|
const BasicOperations = require('./operations/BasicOperations');
|
|
8
8
|
const CapOperations = require('./operations/CapOperations');
|
|
9
9
|
const AsyncOperations = require('./operations/AsyncOperations');
|
|
10
|
+
const telemetry = require('./support/Telemetry');
|
|
11
|
+
const { isMultitenantMode } = require('./support/MultitenancyDetector');
|
|
10
12
|
|
|
11
13
|
class CachingService extends cds.Service {
|
|
12
14
|
|
|
@@ -36,7 +38,18 @@ class CachingService extends cds.Service {
|
|
|
36
38
|
// Create cache store
|
|
37
39
|
const { cache, cleanup } = this.storeManager.createStore(this.options, this.name);
|
|
38
40
|
this.cache = cache;
|
|
41
|
+
|
|
42
|
+
// Set default OTel attributes for all cache metrics
|
|
43
|
+
telemetry.setDefaultAttributes({
|
|
44
|
+
'cache.name': this.name,
|
|
45
|
+
'cache.store': this.options.store || 'memory',
|
|
46
|
+
'cache.namespace': this.options.namespace || this.name
|
|
47
|
+
});
|
|
48
|
+
|
|
39
49
|
this.log.info(`Caching service ${this.name} initialized with namespace ${this.options.namespace || this.name}`);
|
|
50
|
+
if (isMultitenantMode()) {
|
|
51
|
+
this.log.info(`Multi-tenant mode detected for ${this.name} — tenant-aware caching enabled`);
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
// Set up cleanup on shutdown
|
|
42
55
|
cds.once("shutdown", cleanup);
|
package/lib/add.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const cds = require("@sap/cds-dk");
|
|
2
|
+
const { copy, path, write } = cds.utils;
|
|
3
|
+
const { join } = path;
|
|
4
|
+
|
|
5
|
+
module.exports = class extends cds.add.Plugin {
|
|
6
|
+
async run() {
|
|
7
|
+
const src = join(__dirname, "..", "app", "dashboard");
|
|
8
|
+
await copy(src).to("app", "caching-dashboard", "webapp");
|
|
9
|
+
|
|
10
|
+
const cdsFile = join(cds.root, "srv", "caching-api.cds");
|
|
11
|
+
if (!cds.utils.fs.existsSync(cdsFile)) {
|
|
12
|
+
await write(
|
|
13
|
+
"using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';\n",
|
|
14
|
+
).to("srv/caching-api.cds");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const telemetry = require('../support/Telemetry');
|
|
1
2
|
/**
|
|
2
3
|
* Manages async cache operations for function wrapping and execution
|
|
3
4
|
*/
|
|
@@ -27,9 +28,10 @@ class AsyncOperations {
|
|
|
27
28
|
stack: error.stack,
|
|
28
29
|
context: context
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
telemetry.recordError({ 'cache.error_operation': operationName });
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
result: null,
|
|
33
35
|
error: {
|
|
34
36
|
message: error.message,
|
|
35
37
|
operation: operationName,
|
|
@@ -72,7 +74,7 @@ class AsyncOperations {
|
|
|
72
74
|
// Create argument placeholders
|
|
73
75
|
const argPlaceholders = args.map((_, index) => `{args[${index}]}`).join(':');
|
|
74
76
|
const template = `${baseKey}:${argPlaceholders}`;
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
return this.keyManager.createKey(
|
|
77
79
|
{ baseKey, args, functionName },
|
|
78
80
|
{
|
|
@@ -95,113 +97,138 @@ class AsyncOperations {
|
|
|
95
97
|
* @returns {function} - the wrapped function
|
|
96
98
|
*/
|
|
97
99
|
wrap(key, asyncFunction, options = {}) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
const self = this;
|
|
101
|
+
return async function (...args) {
|
|
102
|
+
const tracer = telemetry.getTracer('cds-caching');
|
|
103
|
+
if (tracer) {
|
|
104
|
+
return tracer.startActiveSpan('cds-caching - wrap', async (span) => {
|
|
105
|
+
try {
|
|
106
|
+
const result = await self._wrap(key, asyncFunction, options, args);
|
|
107
|
+
span.setAttribute('cache.hit', result.metadata.hit);
|
|
108
|
+
span.setAttribute('cache.key', result.cacheKey);
|
|
109
|
+
span.setAttribute('cache.operation', 'wrap');
|
|
110
|
+
span.setAttribute('cache.operation_type', 'read_through');
|
|
111
|
+
if (!result.metadata.hit) span.setAttribute('cache.ttl_ms', options.ttl || 0);
|
|
112
|
+
return result;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
span.setStatus({ code: telemetry.SpanStatusCode.ERROR, message: e.message });
|
|
115
|
+
throw e;
|
|
116
|
+
} finally {
|
|
117
|
+
span.end();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return self._wrap(key, asyncFunction, options, args);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{ key: cacheKey, functionName: asyncFunction.name || 'anonymous' }
|
|
123
|
-
);
|
|
125
|
+
/** @private */
|
|
126
|
+
async _wrap(key, asyncFunction, options, args) {
|
|
127
|
+
// Create dynamic key based on function arguments
|
|
128
|
+
const cacheKey = this.createDynamicKey(key, args, options, asyncFunction.name || 'anonymous');
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
const startTime = process.hrtime();
|
|
131
|
+
const metadata = {
|
|
132
|
+
dataType: 'Function',
|
|
133
|
+
operation: 'WRAP',
|
|
134
|
+
operationType: 'READ_THROUGH',
|
|
135
|
+
user: cds.context?.user?.id,
|
|
136
|
+
tenant: cds.context?.tenant,
|
|
137
|
+
locale: cds.context?.locale,
|
|
138
|
+
metadata: JSON.stringify({
|
|
139
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
140
|
+
args: args,
|
|
141
|
+
cacheKey: cacheKey
|
|
142
|
+
}),
|
|
143
|
+
cacheOptions: JSON.stringify(options)
|
|
144
|
+
};
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (getResult.success && getResult.result?.value !== undefined) {
|
|
149
|
-
return {
|
|
150
|
-
result: getResult.result.value,
|
|
151
|
-
cacheKey,
|
|
152
|
-
metadata: { hit: true, latency },
|
|
153
|
-
cacheErrors: cacheErrors
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
146
|
+
// Safely check if key exists in cache
|
|
147
|
+
const hasKeyResult = await this.safeCacheOperation(
|
|
148
|
+
() => this.cache.has(cacheKey),
|
|
149
|
+
'has',
|
|
150
|
+
{ key: cacheKey, functionName: asyncFunction.name || 'anonymous' }
|
|
151
|
+
);
|
|
157
152
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const response = await asyncFunction(...args);
|
|
161
|
-
const latency = this.getElapsedMs(startTime);
|
|
162
|
-
|
|
163
|
-
// Safely record miss statistics
|
|
164
|
-
const missStatsResult = await this.safeCacheOperation(
|
|
165
|
-
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
166
|
-
'recordMiss',
|
|
167
|
-
{ key: cacheKey, latency }
|
|
168
|
-
);
|
|
169
|
-
if (!missStatsResult.success) {
|
|
170
|
-
cacheErrors.push(missStatsResult.error);
|
|
171
|
-
}
|
|
153
|
+
const hasKey = hasKeyResult.success && hasKeyResult.result;
|
|
154
|
+
const cacheErrors = [];
|
|
172
155
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (!setResult.success) {
|
|
186
|
-
cacheErrors.push(setResult.error);
|
|
187
|
-
}
|
|
156
|
+
if (hasKey) {
|
|
157
|
+
const latency = this.getElapsedMs(startTime);
|
|
158
|
+
|
|
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
|
+
}
|
|
188
168
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
169
|
+
// Safely get value from cache
|
|
170
|
+
const getResult = await this.safeCacheOperation(
|
|
171
|
+
() => this.cache.send("GET", { key: cacheKey }),
|
|
172
|
+
'get',
|
|
173
|
+
{ key: cacheKey }
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
177
|
+
return {
|
|
178
|
+
result: getResult.result.value,
|
|
179
|
+
cacheKey,
|
|
180
|
+
metadata: { hit: true, latency },
|
|
193
181
|
cacheErrors: cacheErrors
|
|
194
182
|
};
|
|
195
|
-
} catch (functionError) {
|
|
196
|
-
// If the underlying function fails, throw the error
|
|
197
|
-
this.log.error('Function execution failed:', {
|
|
198
|
-
error: functionError.message,
|
|
199
|
-
functionName: asyncFunction.name || 'anonymous',
|
|
200
|
-
key: cacheKey
|
|
201
|
-
});
|
|
202
|
-
throw functionError;
|
|
203
183
|
}
|
|
204
184
|
}
|
|
185
|
+
|
|
186
|
+
// Cache miss or cache error - delegate to underlying function
|
|
187
|
+
try {
|
|
188
|
+
const response = await asyncFunction(...args);
|
|
189
|
+
const latency = this.getElapsedMs(startTime);
|
|
190
|
+
|
|
191
|
+
// Safely record miss statistics
|
|
192
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
193
|
+
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
194
|
+
'recordMiss',
|
|
195
|
+
{ key: cacheKey, latency }
|
|
196
|
+
);
|
|
197
|
+
if (!missStatsResult.success) {
|
|
198
|
+
cacheErrors.push(missStatsResult.error);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Safely store in cache
|
|
202
|
+
const wrappedValue = {
|
|
203
|
+
value: response,
|
|
204
|
+
tags: options.tags || [],
|
|
205
|
+
timestamp: Date.now()
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const setResult = await this.safeCacheOperation(
|
|
209
|
+
() => this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
210
|
+
'set',
|
|
211
|
+
{ key: cacheKey, ttl: options.ttl }
|
|
212
|
+
);
|
|
213
|
+
if (!setResult.success) {
|
|
214
|
+
cacheErrors.push(setResult.error);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
result: response,
|
|
219
|
+
cacheKey,
|
|
220
|
+
metadata: { hit: false, latency },
|
|
221
|
+
cacheErrors: cacheErrors
|
|
222
|
+
};
|
|
223
|
+
} catch (functionError) {
|
|
224
|
+
// If the underlying function fails, throw the error
|
|
225
|
+
this.log.error('Function execution failed:', {
|
|
226
|
+
error: functionError.message,
|
|
227
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
228
|
+
key: cacheKey
|
|
229
|
+
});
|
|
230
|
+
throw functionError;
|
|
231
|
+
}
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
/**
|
|
@@ -213,6 +240,30 @@ class AsyncOperations {
|
|
|
213
240
|
* @returns {Promise<any>} - the result
|
|
214
241
|
*/
|
|
215
242
|
async exec(key, asyncFunction, args = [], options = {}) {
|
|
243
|
+
const tracer = telemetry.getTracer('cds-caching');
|
|
244
|
+
if (tracer) {
|
|
245
|
+
return tracer.startActiveSpan('cds-caching - exec', async (span) => {
|
|
246
|
+
try {
|
|
247
|
+
const result = await this._exec(key, asyncFunction, args, options);
|
|
248
|
+
span.setAttribute('cache.hit', result.metadata.hit);
|
|
249
|
+
span.setAttribute('cache.key', result.cacheKey);
|
|
250
|
+
span.setAttribute('cache.operation', 'exec');
|
|
251
|
+
span.setAttribute('cache.operation_type', 'read_through');
|
|
252
|
+
if (!result.metadata.hit) span.setAttribute('cache.ttl_ms', options.ttl || 0);
|
|
253
|
+
return result;
|
|
254
|
+
} catch (e) {
|
|
255
|
+
span.setStatus({ code: telemetry.SpanStatusCode.ERROR, message: e.message });
|
|
256
|
+
throw e;
|
|
257
|
+
} finally {
|
|
258
|
+
span.end();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return this._exec(key, asyncFunction, args, options);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** @private */
|
|
266
|
+
async _exec(key, asyncFunction, args, options) {
|
|
216
267
|
// Create dynamic key based on function arguments
|
|
217
268
|
const cacheKey = this.createDynamicKey(key, args, options, asyncFunction.name || 'anonymous');
|
|
218
269
|
|
|
@@ -245,7 +296,7 @@ class AsyncOperations {
|
|
|
245
296
|
|
|
246
297
|
if (hasKey) {
|
|
247
298
|
const latency = this.getElapsedMs(startTime);
|
|
248
|
-
|
|
299
|
+
|
|
249
300
|
// Safely record hit statistics
|
|
250
301
|
const hitStatsResult = await this.safeCacheOperation(
|
|
251
302
|
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
@@ -264,9 +315,9 @@ class AsyncOperations {
|
|
|
264
315
|
);
|
|
265
316
|
|
|
266
317
|
if (getResult.success && getResult.result?.value !== undefined) {
|
|
267
|
-
return {
|
|
268
|
-
result: getResult.result.value,
|
|
269
|
-
cacheKey,
|
|
318
|
+
return {
|
|
319
|
+
result: getResult.result.value,
|
|
320
|
+
cacheKey,
|
|
270
321
|
metadata: { hit: true, latency },
|
|
271
322
|
cacheErrors: cacheErrors
|
|
272
323
|
};
|
|
@@ -277,7 +328,7 @@ class AsyncOperations {
|
|
|
277
328
|
try {
|
|
278
329
|
const response = await asyncFunction(...args);
|
|
279
330
|
const latency = this.getElapsedMs(startTime);
|
|
280
|
-
|
|
331
|
+
|
|
281
332
|
// Safely record miss statistics
|
|
282
333
|
const missStatsResult = await this.safeCacheOperation(
|
|
283
334
|
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
@@ -294,7 +345,7 @@ class AsyncOperations {
|
|
|
294
345
|
tags: options.tags || [],
|
|
295
346
|
timestamp: Date.now()
|
|
296
347
|
};
|
|
297
|
-
|
|
348
|
+
|
|
298
349
|
const setResult = await this.safeCacheOperation(
|
|
299
350
|
() => this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
300
351
|
'set',
|
|
@@ -304,9 +355,9 @@ class AsyncOperations {
|
|
|
304
355
|
cacheErrors.push(setResult.error);
|
|
305
356
|
}
|
|
306
357
|
|
|
307
|
-
return {
|
|
308
|
-
result: response,
|
|
309
|
-
cacheKey,
|
|
358
|
+
return {
|
|
359
|
+
result: response,
|
|
360
|
+
cacheKey,
|
|
310
361
|
metadata: { hit: false, latency },
|
|
311
362
|
cacheErrors: cacheErrors
|
|
312
363
|
};
|
|
@@ -332,4 +383,4 @@ class AsyncOperations {
|
|
|
332
383
|
}
|
|
333
384
|
}
|
|
334
385
|
|
|
335
|
-
module.exports = AsyncOperations;
|
|
386
|
+
module.exports = AsyncOperations;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const telemetry = require('../support/Telemetry');
|
|
1
2
|
/**
|
|
2
3
|
* Manages basic cache operations with statistics tracking
|
|
3
4
|
*/
|
|
@@ -50,6 +51,14 @@ class BasicOperations {
|
|
|
50
51
|
cacheOptions: JSON.stringify(options)
|
|
51
52
|
};
|
|
52
53
|
this.statistics.recordNativeSet(createdKey, metadata);
|
|
54
|
+
|
|
55
|
+
const span = telemetry.getActiveSpan();
|
|
56
|
+
if (span) {
|
|
57
|
+
span.setAttribute('cache.key', createdKey);
|
|
58
|
+
span.setAttribute('cache.operation', 'set');
|
|
59
|
+
span.setAttribute('cache.operation_type', 'basic');
|
|
60
|
+
span.setAttribute('cache.ttl_ms', options.ttl || 0);
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
@@ -80,6 +89,14 @@ class BasicOperations {
|
|
|
80
89
|
const isHit = wrappedValue !== undefined && wrappedValue !== null;
|
|
81
90
|
this.statistics.recordNativeGet(createdKey, isHit, metadata);
|
|
82
91
|
|
|
92
|
+
const span = telemetry.getActiveSpan();
|
|
93
|
+
if (span) {
|
|
94
|
+
span.setAttribute('cache.hit', isHit);
|
|
95
|
+
span.setAttribute('cache.key', createdKey);
|
|
96
|
+
span.setAttribute('cache.operation', 'get');
|
|
97
|
+
span.setAttribute('cache.operation_type', 'basic');
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
return wrappedValue?.value;
|
|
84
101
|
}
|
|
85
102
|
|
|
@@ -127,6 +144,13 @@ class BasicOperations {
|
|
|
127
144
|
};
|
|
128
145
|
this.statistics.recordNativeDelete(createdKey, metadata);
|
|
129
146
|
|
|
147
|
+
const span = telemetry.getActiveSpan();
|
|
148
|
+
if (span) {
|
|
149
|
+
span.setAttribute('cache.key', createdKey);
|
|
150
|
+
span.setAttribute('cache.operation', 'delete');
|
|
151
|
+
span.setAttribute('cache.operation_type', 'basic');
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
return result;
|
|
131
155
|
}
|
|
132
156
|
|
|
@@ -149,6 +173,11 @@ class BasicOperations {
|
|
|
149
173
|
};
|
|
150
174
|
this.statistics.recordNativeClear(metadata);
|
|
151
175
|
|
|
176
|
+
const span = telemetry.getActiveSpan();
|
|
177
|
+
if (span) {
|
|
178
|
+
span.setAttribute('cache.operation', 'clear');
|
|
179
|
+
span.setAttribute('cache.operation_type', 'basic');
|
|
180
|
+
}
|
|
152
181
|
}
|
|
153
182
|
|
|
154
183
|
/**
|
|
@@ -173,6 +202,12 @@ class BasicOperations {
|
|
|
173
202
|
};
|
|
174
203
|
this.statistics.recordNativeDeleteByTag(tag, metadata);
|
|
175
204
|
|
|
205
|
+
const span = telemetry.getActiveSpan();
|
|
206
|
+
if (span) {
|
|
207
|
+
span.setAttribute('cache.tag', tag);
|
|
208
|
+
span.setAttribute('cache.operation', 'deleteByTag');
|
|
209
|
+
span.setAttribute('cache.operation_type', 'basic');
|
|
210
|
+
}
|
|
176
211
|
}
|
|
177
212
|
|
|
178
213
|
/**
|