cds-caching 1.2.1 → 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 +43 -11
- package/srv/caching-api-service.js +47 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
|
+
const { isMultitenantMode, hasTenantContext } = require('./MultitenancyDetector');
|
|
3
|
+
const { isPluginModelAvailable } = require('../util');
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Manages runtime configuration for cache services
|
|
@@ -10,25 +12,50 @@ class RuntimeConfigurationManager {
|
|
|
10
12
|
this.options = options;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the effective keyManagement settings.
|
|
17
|
+
* In MTX mode, isTenantAware defaults to true unless explicitly set to false.
|
|
18
|
+
* @returns {object} keyManagement object
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
_resolveKeyManagement() {
|
|
22
|
+
const keyManagementConfig = this.options?.keyManagement || {};
|
|
23
|
+
return {
|
|
24
|
+
isUserAware: keyManagementConfig.isUserAware === true,
|
|
25
|
+
isTenantAware: keyManagementConfig.isTenantAware !== undefined
|
|
26
|
+
? keyManagementConfig.isTenantAware === true
|
|
27
|
+
: isMultitenantMode(),
|
|
28
|
+
isLocaleAware: keyManagementConfig.isLocaleAware === true
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/**
|
|
14
33
|
* Load runtime configuration from database and package.json
|
|
15
34
|
* @returns {Promise<object>} Configuration object
|
|
16
35
|
*/
|
|
17
36
|
async loadRuntimeConfiguration() {
|
|
37
|
+
const keyManagement = this._resolveKeyManagement();
|
|
38
|
+
|
|
39
|
+
if (!isPluginModelAvailable()) {
|
|
40
|
+
return { enableKeyTracking: false, enableStatistics: false, keyManagement };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// In MTX mode without tenant context, skip DB access — return defaults
|
|
44
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
45
|
+
this.log.info(`Loading runtime configuration for cache ${this.cacheName} (MTX mode, no tenant context — using defaults)`);
|
|
46
|
+
return {
|
|
47
|
+
enableKeyTracking: false,
|
|
48
|
+
enableStatistics: false,
|
|
49
|
+
keyManagement
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
try {
|
|
19
54
|
this.log.info(`Loading runtime configuration for cache ${this.cacheName}...`);
|
|
20
55
|
|
|
21
56
|
const cacheConfig = await SELECT.one.from("plugin_cds_caching_Caches")
|
|
22
57
|
.where({ name: this.cacheName });
|
|
23
58
|
|
|
24
|
-
// Get key management configuration from package.json options (defaults to false)
|
|
25
|
-
const keyManagementConfig = this.options?.keyManagement || {};
|
|
26
|
-
const keyManagement = {
|
|
27
|
-
isUserAware: keyManagementConfig.isUserAware === true, // Default to false
|
|
28
|
-
isTenantAware: keyManagementConfig.isTenantAware === true, // Default to false
|
|
29
|
-
isLocaleAware: keyManagementConfig.isLocaleAware === true // Default to false
|
|
30
|
-
};
|
|
31
|
-
|
|
32
59
|
if (cacheConfig) {
|
|
33
60
|
return {
|
|
34
61
|
enableKeyTracking: cacheConfig.enableKeyTracking || false,
|
|
@@ -48,11 +75,7 @@ class RuntimeConfigurationManager {
|
|
|
48
75
|
return {
|
|
49
76
|
enableKeyTracking: false,
|
|
50
77
|
enableStatistics: false,
|
|
51
|
-
keyManagement
|
|
52
|
-
isUserAware: false,
|
|
53
|
-
isTenantAware: false,
|
|
54
|
-
isLocaleAware: false
|
|
55
|
-
}
|
|
78
|
+
keyManagement
|
|
56
79
|
};
|
|
57
80
|
}
|
|
58
81
|
}
|
|
@@ -62,6 +85,11 @@ class RuntimeConfigurationManager {
|
|
|
62
85
|
* @param {boolean} enabled - Whether metrics should be enabled
|
|
63
86
|
*/
|
|
64
87
|
async setMetricsEnabled(enabled) {
|
|
88
|
+
if (!isPluginModelAvailable()) return;
|
|
89
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
90
|
+
this.log.debug(`Skipping setMetricsEnabled in MTX mode (no tenant context)`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
65
93
|
await UPDATE('plugin_cds_caching_Caches')
|
|
66
94
|
.set({ metricsEnabled: enabled })
|
|
67
95
|
.where({ name: this.cacheName });
|
|
@@ -74,6 +102,11 @@ class RuntimeConfigurationManager {
|
|
|
74
102
|
* @param {boolean} enabled - Whether key metrics should be enabled
|
|
75
103
|
*/
|
|
76
104
|
async setKeyMetricsEnabled(enabled) {
|
|
105
|
+
if (!isPluginModelAvailable()) return;
|
|
106
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
107
|
+
this.log.debug(`Skipping setKeyMetricsEnabled in MTX mode (no tenant context)`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
77
110
|
await UPDATE('plugin_cds_caching_Caches')
|
|
78
111
|
.set({ keyMetricsEnabled: enabled })
|
|
79
112
|
.where({ name: this.cacheName });
|
|
@@ -88,18 +121,26 @@ class RuntimeConfigurationManager {
|
|
|
88
121
|
* @returns {Promise<object>} Current configuration
|
|
89
122
|
*/
|
|
90
123
|
async getRuntimeConfiguration() {
|
|
124
|
+
const keyManagement = this._resolveKeyManagement();
|
|
125
|
+
|
|
126
|
+
if (!isPluginModelAvailable()) {
|
|
127
|
+
return { metricsEnabled: false, keyMetricsEnabled: false, keyManagement, throwOnErrors: this.options.throwOnErrors };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// In MTX mode without tenant context, skip DB access — return defaults
|
|
131
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
132
|
+
return {
|
|
133
|
+
metricsEnabled: false,
|
|
134
|
+
keyMetricsEnabled: false,
|
|
135
|
+
keyManagement,
|
|
136
|
+
throwOnErrors: this.options.throwOnErrors
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
91
140
|
try {
|
|
92
141
|
const cacheConfig = await SELECT.one.from("plugin_cds_caching_Caches")
|
|
93
142
|
.where({ name: this.cacheName });
|
|
94
143
|
|
|
95
|
-
// Get key management configuration from package.json options (defaults to false)
|
|
96
|
-
const keyManagementConfig = this.options?.keyManagement || {};
|
|
97
|
-
const keyManagement = {
|
|
98
|
-
isUserAware: keyManagementConfig.isUserAware === true,
|
|
99
|
-
isTenantAware: keyManagementConfig.isTenantAware === true,
|
|
100
|
-
isLocaleAware: keyManagementConfig.isLocaleAware === true
|
|
101
|
-
};
|
|
102
|
-
|
|
103
144
|
return {
|
|
104
145
|
metricsEnabled: cacheConfig?.metricsEnabled === true || cacheConfig?.metricsEnabled === 1 || false,
|
|
105
146
|
keyMetricsEnabled: cacheConfig?.keyMetricsEnabled === true || cacheConfig?.keyMetricsEnabled === 1 || false,
|
|
@@ -111,36 +152,27 @@ class RuntimeConfigurationManager {
|
|
|
111
152
|
return {
|
|
112
153
|
metricsEnabled: false,
|
|
113
154
|
keyMetricsEnabled: false,
|
|
114
|
-
keyManagement
|
|
115
|
-
isUserAware: false,
|
|
116
|
-
isTenantAware: false,
|
|
117
|
-
isLocaleAware: false
|
|
118
|
-
},
|
|
155
|
+
keyManagement,
|
|
119
156
|
throwOnErrors: this.options.throwOnErrors
|
|
120
157
|
};
|
|
121
158
|
}
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
/**
|
|
125
|
-
* Get default key template based on runtime configuration
|
|
162
|
+
* Get default key template based on runtime configuration.
|
|
163
|
+
* In MTX mode, isTenantAware is automatically enabled unless explicitly disabled.
|
|
126
164
|
* @returns {string} Default template string
|
|
127
165
|
*/
|
|
128
166
|
getDefaultKeyTemplate() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const keyManagement = {
|
|
132
|
-
isUserAware: keyManagementConfig.isUserAware === true, // Default to false
|
|
133
|
-
isTenantAware: keyManagementConfig.isTenantAware === true, // Default to false
|
|
134
|
-
isLocaleAware: keyManagementConfig.isLocaleAware === true // Default to false
|
|
135
|
-
};
|
|
136
|
-
|
|
167
|
+
const keyManagement = this._resolveKeyManagement();
|
|
168
|
+
|
|
137
169
|
const parts = [];
|
|
138
|
-
|
|
170
|
+
|
|
139
171
|
if (keyManagement.isTenantAware) parts.push('{tenant}');
|
|
140
172
|
if (keyManagement.isUserAware) parts.push('{user}');
|
|
141
173
|
if (keyManagement.isLocaleAware) parts.push('{locale}');
|
|
142
174
|
parts.push('{hash}');
|
|
143
|
-
|
|
175
|
+
|
|
144
176
|
return parts.join(':');
|
|
145
177
|
}
|
|
146
178
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
|
+
const { isMultitenantMode, hasTenantContext } = require('./MultitenancyDetector');
|
|
3
|
+
const { isPluginModelAvailable } = require('../util');
|
|
2
4
|
|
|
3
5
|
// Helper to merge min values, treating 0 as unset
|
|
4
6
|
function mergeMin(a, b) {
|
|
@@ -25,7 +27,13 @@ class StatisticsPersistenceManager {
|
|
|
25
27
|
* @param {boolean} enabled - Whether main metrics are enabled
|
|
26
28
|
*/
|
|
27
29
|
async persistHourlyStats(stats, hourlyId, hourlyTimestamp, enabled) {
|
|
28
|
-
if (!enabled) return;
|
|
30
|
+
if (!enabled || !isPluginModelAvailable()) return;
|
|
31
|
+
|
|
32
|
+
// In MTX mode, only persist when we have a tenant context
|
|
33
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
34
|
+
this.log.debug(`Skipping hourly stats persistence in MTX mode (no tenant context)`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
try {
|
|
31
39
|
const existingHourly = await SELECT.one.from("plugin_cds_caching_Metrics")
|
|
@@ -47,7 +55,13 @@ class StatisticsPersistenceManager {
|
|
|
47
55
|
* @param {boolean} keyMetricsEnabled - Whether key metrics are enabled
|
|
48
56
|
*/
|
|
49
57
|
async persistKeyMetrics(keyAccess, keyMetricsEnabled) {
|
|
50
|
-
if (!keyMetricsEnabled || keyAccess.size === 0) return;
|
|
58
|
+
if (!keyMetricsEnabled || keyAccess.size === 0 || !isPluginModelAvailable()) return;
|
|
59
|
+
|
|
60
|
+
// In MTX mode, only persist when we have a tenant context
|
|
61
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
62
|
+
this.log.debug(`Skipping key metrics persistence in MTX mode (no tenant context)`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
51
65
|
|
|
52
66
|
this.log.debug(`Persisting ${keyAccess.size} key access records for cache ${this.cacheName}`);
|
|
53
67
|
|
|
@@ -64,6 +78,7 @@ class StatisticsPersistenceManager {
|
|
|
64
78
|
* Delete metrics for this cache
|
|
65
79
|
*/
|
|
66
80
|
async deleteMetrics() {
|
|
81
|
+
if (!isPluginModelAvailable()) return;
|
|
67
82
|
await DELETE('plugin_cds_caching_Metrics')
|
|
68
83
|
.where({ cache: this.cacheName });
|
|
69
84
|
this.log.debug(`Deleted metrics for cache ${this.cacheName}`);
|
|
@@ -73,6 +88,7 @@ class StatisticsPersistenceManager {
|
|
|
73
88
|
* Delete key metrics for this cache
|
|
74
89
|
*/
|
|
75
90
|
async deleteKeyMetrics() {
|
|
91
|
+
if (!isPluginModelAvailable()) return;
|
|
76
92
|
await DELETE('plugin_cds_caching_KeyMetrics')
|
|
77
93
|
.where({ cache: this.cacheName });
|
|
78
94
|
this.log.debug(`Deleted key metrics for cache ${this.cacheName}`);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional OpenTelemetry integration for cds-caching.
|
|
5
|
+
*
|
|
6
|
+
* If `@opentelemetry/api` is installed (e.g. via `@cap-js/telemetry`), this
|
|
7
|
+
* module exposes helpers to enrich auto-created spans and emit cache-specific
|
|
8
|
+
* OTel metrics. When the dependency is absent everything is a safe no-op —
|
|
9
|
+
* no runtime errors, no performance impact.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let otel;
|
|
13
|
+
try {
|
|
14
|
+
otel = require('@opentelemetry/api');
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err && (err.code === 'MODULE_NOT_FOUND' || err.code === 'ERR_MODULE_NOT_FOUND')) {
|
|
17
|
+
otel = null;
|
|
18
|
+
} else {
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Default attributes (set once during CachingService.init)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
let _defaults = {};
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Lazy metric instances — created on first use so the MeterProvider set up
|
|
30
|
+
// by @cap-js/telemetry is already in place.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
let _meter;
|
|
33
|
+
let _hitCounter;
|
|
34
|
+
let _missCounter;
|
|
35
|
+
let _setCounter;
|
|
36
|
+
let _deleteCounter;
|
|
37
|
+
let _errorCounter;
|
|
38
|
+
let _latencyHistogram;
|
|
39
|
+
|
|
40
|
+
function ensureMetrics() {
|
|
41
|
+
if (_meter) return;
|
|
42
|
+
if (!otel) return;
|
|
43
|
+
_meter = otel.metrics.getMeter('cds-caching');
|
|
44
|
+
_hitCounter = _meter.createCounter('cds_caching.hits', {
|
|
45
|
+
description: 'Number of cache hits'
|
|
46
|
+
});
|
|
47
|
+
_missCounter = _meter.createCounter('cds_caching.misses', {
|
|
48
|
+
description: 'Number of cache misses'
|
|
49
|
+
});
|
|
50
|
+
_setCounter = _meter.createCounter('cds_caching.sets', {
|
|
51
|
+
description: 'Number of cache set operations'
|
|
52
|
+
});
|
|
53
|
+
_deleteCounter = _meter.createCounter('cds_caching.deletes', {
|
|
54
|
+
description: 'Number of cache delete operations'
|
|
55
|
+
});
|
|
56
|
+
_errorCounter = _meter.createCounter('cds_caching.errors', {
|
|
57
|
+
description: 'Number of cache operation errors'
|
|
58
|
+
});
|
|
59
|
+
_latencyHistogram = _meter.createHistogram('cds_caching.latency', {
|
|
60
|
+
description: 'Cache operation latency in milliseconds',
|
|
61
|
+
unit: 'ms'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function attrs(extra) {
|
|
69
|
+
return { ..._defaults, ...extra };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Public API
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
module.exports = {
|
|
76
|
+
|
|
77
|
+
/** Whether `@opentelemetry/api` could be loaded. */
|
|
78
|
+
isAvailable() { return !!otel; },
|
|
79
|
+
|
|
80
|
+
/** Return the currently active span, or `undefined`. */
|
|
81
|
+
getActiveSpan() { return otel?.trace?.getActiveSpan(); },
|
|
82
|
+
|
|
83
|
+
/** Return a Tracer scoped to the given name. */
|
|
84
|
+
getTracer(name) { return otel?.trace?.getTracer(name); },
|
|
85
|
+
|
|
86
|
+
/** Return a Meter scoped to the given name. */
|
|
87
|
+
getMeter(name) { return otel?.metrics?.getMeter(name); },
|
|
88
|
+
|
|
89
|
+
/** Convenience re-export so callers don't need to require `@opentelemetry/api`. */
|
|
90
|
+
SpanStatusCode: otel?.SpanStatusCode ?? { ERROR: 2, OK: 1 },
|
|
91
|
+
|
|
92
|
+
// -- configuration ------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set attributes that will be merged into every metric recording.
|
|
96
|
+
* Typically called once during `CachingService.init()`.
|
|
97
|
+
*/
|
|
98
|
+
setDefaultAttributes(attributes) {
|
|
99
|
+
_defaults = { ..._defaults, ...attributes };
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// -- metric convenience methods -----------------------------------------
|
|
103
|
+
|
|
104
|
+
recordHit(latency, extra = {}) {
|
|
105
|
+
if (!otel) return;
|
|
106
|
+
ensureMetrics();
|
|
107
|
+
const a = attrs({ 'cache.operation': 'hit', ...extra });
|
|
108
|
+
_hitCounter.add(1, a);
|
|
109
|
+
if (latency != null) _latencyHistogram.record(latency, a);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
recordMiss(latency, extra = {}) {
|
|
113
|
+
if (!otel) return;
|
|
114
|
+
ensureMetrics();
|
|
115
|
+
const a = attrs({ 'cache.operation': 'miss', ...extra });
|
|
116
|
+
_missCounter.add(1, a);
|
|
117
|
+
if (latency != null) _latencyHistogram.record(latency, a);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
recordSet(extra = {}) {
|
|
121
|
+
if (!otel) return;
|
|
122
|
+
ensureMetrics();
|
|
123
|
+
_setCounter.add(1, attrs({ 'cache.operation': 'set', ...extra }));
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
recordDelete(extra = {}) {
|
|
127
|
+
if (!otel) return;
|
|
128
|
+
ensureMetrics();
|
|
129
|
+
_deleteCounter.add(1, attrs({ 'cache.operation': 'delete', ...extra }));
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
recordError(extra = {}) {
|
|
133
|
+
if (!otel) return;
|
|
134
|
+
ensureMetrics();
|
|
135
|
+
_errorCounter.add(1, attrs({ 'cache.operation': 'error', ...extra }));
|
|
136
|
+
}
|
|
137
|
+
};
|
package/lib/util.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
const cds = require("@sap/cds")
|
|
2
|
+
const { isMultitenantMode } = require("./support/MultitenancyDetector")
|
|
3
|
+
|
|
4
|
+
let _modelAvailable = null;
|
|
5
|
+
|
|
6
|
+
const isPluginModelAvailable = () => {
|
|
7
|
+
if (_modelAvailable !== null) return _modelAvailable;
|
|
8
|
+
_modelAvailable = !!cds.model?.definitions?.['plugin.cds_caching.Caches'];
|
|
9
|
+
return _modelAvailable;
|
|
10
|
+
};
|
|
2
11
|
|
|
3
12
|
const extractCacheProperties = (entity, prefix) => {
|
|
4
13
|
const result = {};
|
|
@@ -91,24 +100,20 @@ const bindEntity = async (service, entity) => {
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
const createCacheEntry = async (cacheName, serviceConfig = {}) => {
|
|
103
|
+
if (!isPluginModelAvailable()) return;
|
|
94
104
|
try {
|
|
95
105
|
const db = await cds.connect.to('db');
|
|
96
106
|
const { Caches } = cds.entities('plugin.cds_caching');
|
|
97
107
|
|
|
98
|
-
// Check if cache entry already exists
|
|
99
108
|
const existingCache = await db.read(Caches).where({ name: cacheName });
|
|
100
109
|
|
|
101
110
|
if (existingCache.length === 0) {
|
|
102
|
-
|
|
103
|
-
const cacheEntry = {
|
|
111
|
+
await db.create(Caches).entries({
|
|
104
112
|
name: cacheName,
|
|
105
113
|
config: JSON.stringify(serviceConfig)
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
await db.create(Caches).entries(cacheEntry);
|
|
114
|
+
});
|
|
109
115
|
cds.log('cds-caching').info(`Created cache entry for: ${cacheName}`);
|
|
110
116
|
} else {
|
|
111
|
-
// Update existing cache entry
|
|
112
117
|
await db.update(Caches)
|
|
113
118
|
.set({ config: JSON.stringify(serviceConfig) })
|
|
114
119
|
.where({ name: cacheName });
|
|
@@ -122,6 +127,14 @@ const createCacheEntry = async (cacheName, serviceConfig = {}) => {
|
|
|
122
127
|
const scanCachingAnnotations = async (srvs) => {
|
|
123
128
|
LOG = cds.log('cds-caching')
|
|
124
129
|
|
|
130
|
+
if (!isPluginModelAvailable()) {
|
|
131
|
+
LOG.warn(
|
|
132
|
+
`Plugin data model not compiled into cds.model. Database features (dashboard, metrics persistence, CDS store) are disabled. ` +
|
|
133
|
+
`The model auto-loads when 'statistics' is configured or 'store: cds' is used. ` +
|
|
134
|
+
`For the dashboard, add: using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
// Grep all app services
|
|
126
139
|
const services = [];
|
|
127
140
|
for (const [name, config] of Object.entries(srvs)) {
|
|
@@ -192,10 +205,15 @@ const scanCachingAnnotations = async (srvs) => {
|
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
// In MTX mode, skip DB persistence at startup — no tenant context available.
|
|
209
|
+
// Cache entries are informational (mirrors package.json config) and will be
|
|
210
|
+
// created lazily via CachingApiService when a tenant accesses the dashboard.
|
|
211
|
+
if (!isMultitenantMode()) {
|
|
212
|
+
const cachingServices = getCachingServicesFromConfig(srvs);
|
|
213
|
+
for (const service of cachingServices) {
|
|
214
|
+
await createCacheEntry(service.name, service);
|
|
215
|
+
}
|
|
198
216
|
}
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
module.exports = { scanCachingAnnotations }
|
|
219
|
+
module.exports = { scanCachingAnnotations, isPluginModelAvailable }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-caching",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A caching plugin for SAP CAP applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"cds",
|
|
13
13
|
"caching",
|
|
14
14
|
"redis",
|
|
15
|
+
"hana",
|
|
15
16
|
"keyv"
|
|
16
17
|
],
|
|
17
18
|
"author": "Mike Zaschka <opensource@mikezaschka.com>",
|
|
@@ -23,26 +24,38 @@
|
|
|
23
24
|
"main": "cds-plugin.js",
|
|
24
25
|
"types": "lib/CachingService.d.ts",
|
|
25
26
|
"files": [
|
|
27
|
+
"db/**",
|
|
26
28
|
"srv/**",
|
|
27
29
|
"index.cds",
|
|
28
|
-
"lib/**"
|
|
30
|
+
"lib/**",
|
|
31
|
+
"app/dashboard/**"
|
|
29
32
|
],
|
|
30
33
|
"scripts": {
|
|
31
34
|
"test": "jest --runInBand --silent",
|
|
32
35
|
"test:watch": "jest --watch",
|
|
36
|
+
"test:docker:start": "docker compose -f test/docker/docker-compose.yaml up -d --wait",
|
|
37
|
+
"test:docker:stop": "docker compose -f test/docker/docker-compose.yaml down",
|
|
38
|
+
"test:all": "npm run test:docker:start && npm test; status=$?; npm run test:docker:stop; exit $status",
|
|
33
39
|
"lint": "eslint .",
|
|
34
|
-
"release": "release-it"
|
|
40
|
+
"release": "release-it",
|
|
41
|
+
"prepublishOnly": "cd examples/dashboard && npx ui5 build self-contained --clean-dest --dest ../../app/dashboard"
|
|
35
42
|
},
|
|
36
43
|
"peerDependencies": {
|
|
37
|
-
"@sap/cds": ">=8",
|
|
38
44
|
"@keyv/compress-gzip": "^2.0.3",
|
|
39
45
|
"@keyv/compress-lz4": "^1.0.1",
|
|
46
|
+
"@keyv/postgres": "^4.0.5",
|
|
40
47
|
"@keyv/redis": "^5.0.0",
|
|
41
48
|
"@keyv/sqlite": "^4.0.5",
|
|
49
|
+
"@opentelemetry/api": "^1.0.0",
|
|
42
50
|
"@resolid/keyv-sqlite": "^4.0.5",
|
|
43
|
-
"@
|
|
51
|
+
"@sap/cds": ">=8",
|
|
52
|
+
"@sap/cds-dk": ">=9",
|
|
53
|
+
"keyv-hana": ">=1.0.0"
|
|
44
54
|
},
|
|
45
55
|
"peerDependenciesMeta": {
|
|
56
|
+
"@opentelemetry/api": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
46
59
|
"@keyv/redis": {
|
|
47
60
|
"optional": true
|
|
48
61
|
},
|
|
@@ -60,6 +73,9 @@
|
|
|
60
73
|
},
|
|
61
74
|
"@keyv/postgres": {
|
|
62
75
|
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"keyv-hana": {
|
|
78
|
+
"optional": true
|
|
63
79
|
}
|
|
64
80
|
},
|
|
65
81
|
"workspaces": [
|
|
@@ -73,18 +89,34 @@
|
|
|
73
89
|
},
|
|
74
90
|
"devDependencies": {
|
|
75
91
|
"@cap-js/cds-test": "^0.4.1",
|
|
92
|
+
"@keyv/compress-gzip": "~2.0.3",
|
|
93
|
+
"@keyv/compress-lz4": "~1.0.1",
|
|
94
|
+
"@keyv/postgres": "~2.2.3",
|
|
95
|
+
"@keyv/sqlite": "~4.0.8",
|
|
96
|
+
"@opentelemetry/api": "^1.9.1",
|
|
97
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
98
|
+
"@opentelemetry/sdk-trace-base": "^1.30.1",
|
|
99
|
+
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
|
76
100
|
"@release-it/conventional-changelog": "^10.0.5",
|
|
101
|
+
"@resolid/keyv-sqlite": "~5.0.1",
|
|
77
102
|
"eslint": "^10.0.0",
|
|
78
103
|
"husky": "^9.1.7",
|
|
79
104
|
"jest": "^30.2.0",
|
|
80
|
-
"release-it": "^19.2.4"
|
|
81
|
-
"@resolid/keyv-sqlite": "~5.0.1",
|
|
82
|
-
"@keyv/sqlite": "~4.0.8",
|
|
83
|
-
"@keyv/compress-lz4": "~1.0.1",
|
|
84
|
-
"@keyv/compress-gzip": "~2.0.3",
|
|
85
|
-
"@keyv/postgres": "~2.2.3"
|
|
105
|
+
"release-it": "^19.2.4"
|
|
86
106
|
},
|
|
87
107
|
"engines": {
|
|
88
108
|
"node": ">=20"
|
|
109
|
+
},
|
|
110
|
+
"cds": {
|
|
111
|
+
"requires": {
|
|
112
|
+
"kinds": {
|
|
113
|
+
"cds-caching": {
|
|
114
|
+
"impl": "cds-caching"
|
|
115
|
+
},
|
|
116
|
+
"caching": {
|
|
117
|
+
"impl": "cds-caching"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
89
121
|
}
|
|
90
122
|
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
|
+
const { isMultitenantMode } = require('../lib/support/MultitenancyDetector')
|
|
3
|
+
const { isPluginModelAvailable } = require('../lib/util')
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TENANT = '_default';
|
|
2
6
|
|
|
3
7
|
class CachingApiService extends cds.ApplicationService {
|
|
4
8
|
log = cds.log('cds-caching');
|
|
5
9
|
|
|
6
10
|
async init() {
|
|
7
11
|
|
|
12
|
+
// In MTX mode, lazily create cache entries on first dashboard access
|
|
13
|
+
// (skipped at startup because there's no tenant context)
|
|
14
|
+
if (isMultitenantMode()) {
|
|
15
|
+
this.before('READ', 'Caches', async () => {
|
|
16
|
+
await this._ensureCacheEntries();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
// Handle setMetricsEnabled action
|
|
9
21
|
this.on('setMetricsEnabled', async (req) => {
|
|
10
22
|
const { enabled } = req.data
|
|
@@ -117,8 +129,41 @@ class CachingApiService extends cds.ApplicationService {
|
|
|
117
129
|
await super.init()
|
|
118
130
|
}
|
|
119
131
|
|
|
120
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Ensure cache entries exist in the current tenant's DB.
|
|
134
|
+
* Called lazily in MTX mode on first dashboard access.
|
|
135
|
+
*/
|
|
136
|
+
async _ensureCacheEntries() {
|
|
137
|
+
const tenant = cds.context?.tenant ?? DEFAULT_TENANT;
|
|
138
|
+
if (!this._cacheEntriesInitialized) this._cacheEntriesInitialized = new Set();
|
|
139
|
+
if (this._cacheEntriesInitialized.has(tenant) || !isPluginModelAvailable()) return;
|
|
121
140
|
|
|
141
|
+
try {
|
|
142
|
+
const { Caches } = cds.entities('plugin.cds_caching');
|
|
143
|
+
const requires = cds.env.requires || {};
|
|
144
|
+
|
|
145
|
+
for (const [name, config] of Object.entries(requires)) {
|
|
146
|
+
if (config.impl === 'cds-caching') {
|
|
147
|
+
const existing = await SELECT.one.from(Caches).where({ name });
|
|
148
|
+
if (!existing) {
|
|
149
|
+
await INSERT.into(Caches).entries({
|
|
150
|
+
name,
|
|
151
|
+
config: JSON.stringify({
|
|
152
|
+
impl: config.impl,
|
|
153
|
+
store: config.store || 'memory',
|
|
154
|
+
namespace: config.namespace || name
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
this.log.info(`Created cache entry for: ${name} (lazy init)`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this._cacheEntriesInitialized.add(tenant);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.log.warn('Failed to lazily initialize cache entries:', error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
122
167
|
}
|
|
123
168
|
|
|
124
|
-
module.exports = CachingApiService
|
|
169
|
+
module.exports = CachingApiService
|