cds-caching 1.2.1 → 1.3.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 +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 +24 -10
- package/lib/add.js +17 -0
- package/lib/operations/AsyncOperations.js +162 -111
- package/lib/operations/BasicOperations.js +42 -4
- package/lib/operations/CapOperations.js +92 -40
- package/lib/support/CacheStatisticsHandler.js +35 -10
- package/lib/support/CacheStoreManager.js +29 -0
- package/lib/support/KeyManager.js +40 -2
- 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
package/README.md
CHANGED
|
@@ -4,233 +4,119 @@
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A caching plugin for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap/docs/) that improves performance by caching slow remote service calls, complex operations, and queries.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- Slow remote service calls
|
|
11
|
-
- Complex operations
|
|
12
|
-
- Slow queries
|
|
13
|
-
- Other performance bottle necks
|
|
14
|
-
|
|
15
|
-
While cds-caching can be a big helper, an additional caching layer also adds complexity and should be used judiciously.
|
|
16
|
-
|
|
17
|
-
Please also read the introduction blog post in the SAP Community: [Boosting performance in SAP Cloud Application Programming Model (CAP) applications with cds-caching](https://community.sap.com/t5/technology-blogs-by-members/boosting-performance-in-sap-cloud-application-programming-model-cap/ba-p/14002015).
|
|
9
|
+
Please also read the introduction blog post: [Boosting performance in SAP Cloud Application Programming Model (CAP) applications with cds-caching](https://community.sap.com/t5/technology-blogs-by-members/boosting-performance-in-sap-cloud-application-programming-model-cap/ba-p/14002015).
|
|
18
10
|
|
|
19
11
|
### Key Features
|
|
20
12
|
|
|
21
|
-
* **
|
|
22
|
-
* **
|
|
23
|
-
* **
|
|
24
|
-
* **
|
|
25
|
-
* **
|
|
26
|
-
* **
|
|
27
|
-
* **
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Upgrading from 1.1.0 to 1.2.0
|
|
13
|
+
* **Read-Through Caching** – Transparently cache CQN queries, CAP requests, or function calls
|
|
14
|
+
* **Pluggable Storage** – In-memory, SQLite, Redis, PostgreSQL, SAP HANA, or CDS database
|
|
15
|
+
* **Multi-Tenancy** – Automatic tenant isolation for SAP BTP MTX deployments
|
|
16
|
+
* **TTL & Tag Support** – Time-based expiry and tag-based invalidation
|
|
17
|
+
* **Compression** – LZ4 or GZIP compression for cached data
|
|
18
|
+
* **Metrics & Monitoring** – Hit rates, latencies, key-level tracking, and an OData API
|
|
19
|
+
* **Annotations** – Declarative caching via `@cache` annotations on entities and functions
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
| Guide | Description |
|
|
24
|
+
|-------|-------------|
|
|
25
|
+
| [Programmatic API](docs/programmatic-api.md) | Full API reference for cache operations |
|
|
26
|
+
| [Key Management](docs/key-management.md) | Key templates, context awareness, custom keys |
|
|
27
|
+
| [Metrics Guide](docs/metrics-guide.md) | Statistics, monitoring, and performance tracking |
|
|
28
|
+
| [OpenTelemetry Integration](docs/telemetry.md) | Distributed tracing and metrics export |
|
|
29
|
+
| [OData API](docs/odata-api.md) | REST endpoints for management and monitoring |
|
|
30
|
+
| [Dashboard](docs/dashboard.md) | Setup and usage of the monitoring dashboard |
|
|
31
|
+
| [Deployment Guide](docs/deployment-guide.md) | SAP BTP deployment for Redis, PostgreSQL, HANA, CDS |
|
|
32
|
+
| [Migration Guide](docs/migration-guide.md) | Upgrading from 0.x to 1.x, 1.1 to 1.2, and 1.2.x to 1.3.0 |
|
|
33
|
+
| [Example Application](docs/example-app.md) | Sample app with caching patterns |
|
|
34
|
+
|
|
35
|
+
## Getting Started
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
### Required adapter packages (add to your app's `package.json`)
|
|
49
|
-
|
|
50
|
-
Install the package(s) matching your configured `store` / `compression`:
|
|
51
|
-
|
|
52
|
-
| Config | Value | Install in your app |
|
|
53
|
-
|---|---|---|
|
|
54
|
-
| `store` | `"redis"` | `@keyv/redis` |
|
|
55
|
-
| `store` | `"sqlite"` | `@resolid/keyv-sqlite` (recommended) **or** `@keyv/sqlite` |
|
|
56
|
-
| `store` | `"postgres"` | `@keyv/postgres` |
|
|
57
|
-
| `compression` | `"lz4"` | `@keyv/compress-lz4` |
|
|
58
|
-
| `compression` | `"gzip"` | `@keyv/compress-gzip` |
|
|
59
|
-
|
|
60
|
-
Example:
|
|
37
|
+
### Installation
|
|
61
38
|
|
|
62
39
|
```bash
|
|
63
|
-
npm
|
|
64
|
-
# or: npm i @resolid/keyv-sqlite
|
|
65
|
-
# and optionally: npm i @keyv/compress-gzip
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Upgrading from 0.x to 1.x
|
|
69
|
-
|
|
70
|
-
### 🔄 API Changes for read-through methods
|
|
71
|
-
|
|
72
|
-
Version 1.x introduces new methods that provide more insights into the read-through caching as they also directly return the genrated cache `key` and some caching `metadata`. The should be preferrably used instead of the old methods.
|
|
73
|
-
|
|
74
|
-
| **Old Method** | **New Method** | **Key Differences** |
|
|
75
|
-
|----------------|----------------|---------------------|
|
|
76
|
-
| `cache.run()` | `cache.rt.run()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
|
|
77
|
-
| `cache.send()` | `cache.rt.send()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
|
|
78
|
-
| `cache.wrap()` | `cache.rt.wrap()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
|
|
79
|
-
| `cache.exec()` | `cache.rt.exec()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
|
|
80
|
-
|
|
81
|
-
### 🔑 Key Template Changes
|
|
82
|
-
|
|
83
|
-
**Before (0.x):**
|
|
84
|
-
```javascript
|
|
85
|
-
// Old syntax - object with template property
|
|
86
|
-
await cache.set(query, result, {
|
|
87
|
-
key: { template: "user:{user}:{hash}" }
|
|
88
|
-
})
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**After (1.x):**
|
|
92
|
-
```javascript
|
|
93
|
-
// New syntax - direct string template
|
|
94
|
-
await cache.set(query, result, {
|
|
95
|
-
key: "user:{user}:{hash}"
|
|
96
|
-
})
|
|
40
|
+
npm install cds-caching
|
|
97
41
|
```
|
|
98
42
|
|
|
99
|
-
###
|
|
100
|
-
|
|
101
|
-
**Default Behavior Changed:**
|
|
102
|
-
- **0.x:** Context (user, tenant, locale) was automatically included in some cache keys (ODataRequests)
|
|
103
|
-
- **1.x:** Context is **disabled by default** and can be enabled for **ALL** keys (unless overwritten)
|
|
43
|
+
### Minimal Configuration
|
|
104
44
|
|
|
105
|
-
**To Enable Context Awareness:**
|
|
106
45
|
```json
|
|
107
46
|
{
|
|
108
47
|
"cds": {
|
|
109
48
|
"requires": {
|
|
110
49
|
"caching": {
|
|
111
|
-
|
|
112
|
-
"keyManagement": {
|
|
113
|
-
"isUserAware": true, // Include user context in cache keys
|
|
114
|
-
"isTenantAware": true, // Include tenant context in cache keys
|
|
115
|
-
"isLocaleAware": false // Include locale context in cache keys
|
|
116
|
-
}
|
|
50
|
+
"impl": "cds-caching"
|
|
117
51
|
}
|
|
118
52
|
}
|
|
119
53
|
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 📚 Migration Examples
|
|
124
|
-
|
|
125
|
-
**Example 1: Basic Caching**
|
|
126
|
-
```javascript
|
|
127
|
-
// ❌ Old way (deprecated, but will still work)
|
|
128
|
-
const result = await cache.run(query, db)
|
|
129
|
-
|
|
130
|
-
// ✅ New way
|
|
131
|
-
const { result, cacheKey, metadata } = await cache.rt.run(query, db)
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Example 2: Function Wrapping**
|
|
135
|
-
```javascript
|
|
136
|
-
// ❌ Old way (deprecated, but will still work)
|
|
137
|
-
const cachedFn = cache.wrap("key", expensiveOperation)
|
|
138
|
-
const result = await cachedFn("param1", "param2")
|
|
139
|
-
|
|
140
|
-
// ✅ New way
|
|
141
|
-
const cachedFn = cache.rt.wrap("key", expensiveOperation)
|
|
142
|
-
const { result, cacheKey, metadata } = await cachedFn("param1", "param2")
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Example 3: Custom Key Templates**
|
|
146
|
-
```javascript
|
|
147
|
-
// ❌ Old way (will not work anymore)
|
|
148
|
-
await cache.set(data, value, {
|
|
149
|
-
key: { template: "user:{user}:{hash}" }
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
// ✅ New way
|
|
153
|
-
await cache.set(data, value, {
|
|
154
|
-
key: "user:{user}:{hash}"
|
|
155
|
-
})
|
|
54
|
+
}
|
|
156
55
|
```
|
|
157
56
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- **Enhanced Metadata:** All read-through operations now return cache keys and performance metadata
|
|
161
|
-
- **Better Performance:** Context awareness is opt-in, reducing unnecessary key complexity
|
|
162
|
-
- **Improved Debugging:** Access to generated cache keys for troubleshooting
|
|
163
|
-
- **Flexible Configuration:** Global and per-operation key template control
|
|
57
|
+
This uses the in-memory store — no additional setup needed for development.
|
|
164
58
|
|
|
165
|
-
|
|
59
|
+
### Data Model
|
|
166
60
|
|
|
167
|
-
|
|
61
|
+
The plugin ships CDS entity definitions for database-backed features. These are **auto-loaded conditionally** based on your configuration — no manual `model` property or `using from` needed:
|
|
168
62
|
|
|
169
|
-
|
|
63
|
+
| Condition | Entities loaded | Purpose |
|
|
64
|
+
|-----------|----------------|---------|
|
|
65
|
+
| `statistics` block present | `Caches`, `Metrics`, `KeyMetrics` | Persist metrics and runtime config to the database |
|
|
66
|
+
| `store: 'cds'` | `CacheStore` | Key-value table used by the CDS store adapter |
|
|
67
|
+
| `using from 'cds-caching/index.cds'` | `CachingApiService` + statistics entities | OData API for the [dashboard](docs/dashboard.md) |
|
|
68
|
+
| None of the above | Nothing | Plugin works with external stores only |
|
|
170
69
|
|
|
171
|
-
|
|
70
|
+
The auto-loading works by injecting the relevant CDS files into `cds.env.roots` at plugin load time, before CAP compiles the model. This means `cds deploy` and `cds build` automatically pick up the required tables.
|
|
172
71
|
|
|
173
|
-
|
|
174
|
-
- **Backend Application** (`examples/app/`) - A CAP application showing annotation-based and programmatic caching patterns
|
|
175
|
-
- **Dashboard** (`examples/dashboard/`) - A UI5-based monitoring interface with real-time metrics, key-level analytics, and historical data
|
|
72
|
+
### Basic Usage
|
|
176
73
|
|
|
177
|
-
|
|
74
|
+
```javascript
|
|
75
|
+
const cache = await cds.connect.to("caching")
|
|
178
76
|
|
|
77
|
+
// Key-value operations
|
|
78
|
+
await cache.set("bp:1000001", businessPartnerData, { ttl: 60000 })
|
|
79
|
+
const data = await cache.get("bp:1000001")
|
|
179
80
|
|
|
180
|
-
|
|
81
|
+
// Read-through caching for CQN queries
|
|
82
|
+
const { result } = await cache.rt.run(query, db, { ttl: 30000 })
|
|
181
83
|
|
|
182
|
-
|
|
84
|
+
// Read-through caching for remote services
|
|
85
|
+
const { result } = await cache.rt.send(request, remoteService, { ttl: 10000 })
|
|
183
86
|
|
|
184
|
-
|
|
185
|
-
|
|
87
|
+
// Function caching
|
|
88
|
+
const cachedFn = cache.rt.wrap("expensive-op", expensiveFunction, { ttl: 3600 })
|
|
89
|
+
const { result } = await cachedFn("param1")
|
|
186
90
|
```
|
|
187
91
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`cds-caching` only ships with the in-memory store. If you configure a different store or compression, you must install the corresponding adapter package **in your consuming CAP project**:
|
|
92
|
+
### Annotation-Based Caching
|
|
191
93
|
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
# SQLite store
|
|
197
|
-
npm install @resolid/keyv-sqlite # Preferred in CAP because of better-sqlite3 usage
|
|
198
|
-
npm install @keyv/sqlite # Alternative if you want to rely on the official adapter
|
|
94
|
+
```cds
|
|
95
|
+
service MyService {
|
|
96
|
+
@cache: { ttl: 10000 }
|
|
97
|
+
entity Products as projection on db.Products;
|
|
199
98
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
99
|
+
@cache: { ttl: 60000 }
|
|
100
|
+
function getRecommendations() returns array of Products;
|
|
101
|
+
}
|
|
203
102
|
```
|
|
204
103
|
|
|
205
|
-
|
|
104
|
+
## Configuration
|
|
206
105
|
|
|
207
|
-
###
|
|
106
|
+
### Store Types
|
|
208
107
|
|
|
209
|
-
|
|
108
|
+
| Store | Config | Use Case | Adapter Package |
|
|
109
|
+
|-------|--------|----------|-----------------|
|
|
110
|
+
| In-Memory | `"memory"` | Development, small-scale | Built-in |
|
|
111
|
+
| SQLite | `"sqlite"` | Medium-size, single instance | `@resolid/keyv-sqlite` or `@keyv/sqlite` |
|
|
112
|
+
| Redis | `"redis"` | Production, distributed | `@keyv/redis` |
|
|
113
|
+
| PostgreSQL | `"postgres"` | Production, when Redis unavailable | `@keyv/postgres` |
|
|
114
|
+
| CDS Database | `"cds"` | Production, HANA, multi-tenant | None (uses app's DB) |
|
|
115
|
+
| SAP HANA | `"hana"` | Direct HANA connection | `keyv-hana` |
|
|
210
116
|
|
|
211
|
-
|
|
117
|
+
> **Recommendation**: Use `store: 'cds'` for CAP applications on SAP HANA — it reuses your app's DB connection, requires no extra packages, and supports multi-tenancy automatically. Use `store: 'redis'` for best performance in distributed setups.
|
|
212
118
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```json
|
|
216
|
-
{
|
|
217
|
-
"cds": {
|
|
218
|
-
"requires": {
|
|
219
|
-
"caching": {
|
|
220
|
-
"impl": "cds-caching",
|
|
221
|
-
"namespace": "caching"
|
|
222
|
-
},
|
|
223
|
-
// Recommended: Define a specific caching service for different caching requirements
|
|
224
|
-
"bp-caching": {
|
|
225
|
-
"impl": "cds-caching",
|
|
226
|
-
"namespace": "bp-caching"
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
**Advanced configuration** with all options:
|
|
119
|
+
### Full Configuration Options
|
|
234
120
|
|
|
235
121
|
```json
|
|
236
122
|
{
|
|
@@ -239,21 +125,19 @@ The cds-caching plugin supports comprehensive configuration through `package.jso
|
|
|
239
125
|
"caching": {
|
|
240
126
|
"impl": "cds-caching",
|
|
241
127
|
"namespace": "caching",
|
|
242
|
-
"store": "
|
|
243
|
-
"compression": "lz4",
|
|
244
|
-
"throwOnErrors": false,
|
|
245
|
-
"transactionalOperations": false,
|
|
246
|
-
"credentials": {
|
|
247
|
-
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
"table": "cache",
|
|
256
|
-
"busyTimeout": 10000
|
|
128
|
+
"store": "redis",
|
|
129
|
+
"compression": "lz4",
|
|
130
|
+
"throwOnErrors": false,
|
|
131
|
+
"transactionalOperations": false,
|
|
132
|
+
"credentials": { },
|
|
133
|
+
"statistics": {
|
|
134
|
+
"enabled": true,
|
|
135
|
+
"persistenceInterval": 60000
|
|
136
|
+
},
|
|
137
|
+
"keyManagement": {
|
|
138
|
+
"isUserAware": false,
|
|
139
|
+
"isTenantAware": false,
|
|
140
|
+
"isLocaleAware": false
|
|
257
141
|
}
|
|
258
142
|
}
|
|
259
143
|
}
|
|
@@ -261,11 +145,21 @@ The cds-caching plugin supports comprehensive configuration through `package.jso
|
|
|
261
145
|
}
|
|
262
146
|
```
|
|
263
147
|
|
|
264
|
-
|
|
148
|
+
| Option | Default | Description |
|
|
149
|
+
|--------|---------|-------------|
|
|
150
|
+
| `store` | `"memory"` | Storage backend (`memory`, `sqlite`, `redis`, `postgres`, `hana`, `cds`) |
|
|
151
|
+
| `namespace` | service name | Key prefix for store isolation |
|
|
152
|
+
| `compression` | none | `"lz4"` or `"gzip"` |
|
|
153
|
+
| `throwOnErrors` | `false` | Whether basic operations throw on cache errors |
|
|
154
|
+
| `transactionalOperations` | `false` | Isolate basic ops in dedicated cache transactions |
|
|
155
|
+
| `statistics` | none | When present, auto-loads the statistics data model and enables persistence (see [Statistics & Monitoring](#statistics--monitoring)) |
|
|
156
|
+
| `statistics.enabled` | `false` | Enable metrics collection |
|
|
157
|
+
| `statistics.persistenceInterval` | `60000` | Interval (ms) for persisting hourly stats to the database |
|
|
158
|
+
| `keyManagement.isTenantAware` | `false` (auto `true` in MTX) | Include tenant in cache keys |
|
|
159
|
+
| `keyManagement.isUserAware` | `false` | Include user in cache keys |
|
|
160
|
+
| `keyManagement.isLocaleAware` | `false` | Include locale in cache keys |
|
|
265
161
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
To isolate **basic cache operations** (`get`, `set`, `delete`, `clear`, `deleteByTag`, `metadata`, `tags`, `getRaw`) from the request transaction, enable:
|
|
162
|
+
### Environment-Specific Configuration
|
|
269
163
|
|
|
270
164
|
```json
|
|
271
165
|
{
|
|
@@ -273,1018 +167,205 @@ To isolate **basic cache operations** (`get`, `set`, `delete`, `clear`, `deleteB
|
|
|
273
167
|
"requires": {
|
|
274
168
|
"caching": {
|
|
275
169
|
"impl": "cds-caching",
|
|
276
|
-
"
|
|
170
|
+
"store": "redis",
|
|
171
|
+
"[development]": {
|
|
172
|
+
"credentials": { "host": "localhost", "port": 6379 }
|
|
173
|
+
},
|
|
174
|
+
"[production]": {
|
|
175
|
+
"credentials": { "url": "redis://production-redis:6379" }
|
|
176
|
+
}
|
|
277
177
|
}
|
|
278
178
|
}
|
|
279
179
|
}
|
|
280
180
|
}
|
|
281
181
|
```
|
|
282
182
|
|
|
283
|
-
|
|
183
|
+
For detailed key configuration and deployment instructions, see [Key Management](docs/key-management.md) and [Deployment Guide](docs/deployment-guide.md).
|
|
284
184
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Configure default key templates for read-through operations:
|
|
288
|
-
|
|
289
|
-
```json
|
|
290
|
-
{
|
|
291
|
-
"cds": {
|
|
292
|
-
"requires": {
|
|
293
|
-
"caching": {
|
|
294
|
-
...
|
|
295
|
-
"keyManagement": {
|
|
296
|
-
"isUserAware": true, // Include user context in cache keys
|
|
297
|
-
"isTenantAware": true, // Include tenant context in cache keys
|
|
298
|
-
"isLocaleAware": false // Include locale context in cache keys
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
```
|
|
185
|
+
### Service Integration
|
|
305
186
|
|
|
306
|
-
|
|
187
|
+
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.
|
|
307
188
|
|
|
308
|
-
|
|
189
|
+
The easiest way to set this up is `cds add caching-dashboard`, which creates both the service exposure and the dashboard UI. To expose only the service without the dashboard, reference it in one of your `.cds` files:
|
|
309
190
|
|
|
310
|
-
|
|
191
|
+
```cds
|
|
192
|
+
using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';
|
|
311
193
|
|
|
312
|
-
|
|
313
|
-
{
|
|
314
|
-
"cds": {
|
|
315
|
-
"requires": {
|
|
316
|
-
"caching": {
|
|
317
|
-
...
|
|
318
|
-
"throwOnErrors": true, // Basic operations (set, get, delete, has) throw errors
|
|
319
|
-
// Default: false - operations return undefined/null instead of throwing
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
194
|
+
annotate CachingApiService with @requires: 'authenticated-user';
|
|
324
195
|
```
|
|
325
196
|
|
|
326
|
-
|
|
197
|
+
This automatically loads the required database entities (`Caches`, `Metrics`, `KeyMetrics`) via a transitive `using from` dependency — no additional configuration needed. Without this step, the service won't be served by CAP and the dashboard won't work.
|
|
327
198
|
|
|
328
|
-
-
|
|
329
|
-
- When `throwOnErrors: false` (default): Operations return `undefined`/`null` on errors
|
|
330
|
-
- When `throwOnErrors: true`: Operations throw errors for connection issues, etc.
|
|
199
|
+
## Multi-Tenancy (MTX)
|
|
331
200
|
|
|
332
|
-
-
|
|
333
|
-
- Never throw errors, regardless of `throwOnErrors` setting
|
|
334
|
-
- Include `cacheErrors` array in response when errors occur
|
|
335
|
-
- Always fetch from remote service when cache operations fail
|
|
336
|
-
- Log errors for monitoring and debugging
|
|
201
|
+
cds-caching supports SAP BTP multi-tenant applications using `@sap/cds-mtxs`. When multitenancy is detected, the plugin automatically:
|
|
337
202
|
|
|
338
|
-
|
|
203
|
+
- Enables **tenant-aware cache keys** (`{tenant}:{hash}`)
|
|
204
|
+
- **Defers database operations** to request-time (avoids startup crashes without tenant context)
|
|
205
|
+
- **Guards statistics persistence** to only run within a tenant request context
|
|
339
206
|
|
|
340
|
-
|
|
207
|
+
### Recommended Setup
|
|
341
208
|
|
|
342
209
|
```json
|
|
343
210
|
{
|
|
344
211
|
"cds": {
|
|
345
212
|
"requires": {
|
|
213
|
+
"multitenancy": true,
|
|
346
214
|
"caching": {
|
|
347
215
|
"impl": "cds-caching",
|
|
348
|
-
"store": "
|
|
349
|
-
"[development]": {
|
|
350
|
-
"credentials": {
|
|
351
|
-
"host": "localhost",
|
|
352
|
-
"port": 6379
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
"[production]": {
|
|
356
|
-
"credentials": {
|
|
357
|
-
"url": "redis://production-redis:6379"
|
|
358
|
-
}
|
|
359
|
-
}
|
|
216
|
+
"store": "cds"
|
|
360
217
|
}
|
|
361
218
|
}
|
|
362
219
|
}
|
|
363
220
|
}
|
|
364
221
|
```
|
|
365
222
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
### Service Definition
|
|
369
|
-
|
|
370
|
-
Add the following cds definition to your data model:
|
|
371
|
-
|
|
372
|
-
```
|
|
373
|
-
using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';
|
|
374
|
-
|
|
375
|
-
// Don't forget to protect the service, e.g.
|
|
376
|
-
annotate CachingApiService with @requires: 'authenticated-user';
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Real-World Usage and Deployment
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
#### Storage Options
|
|
383
|
-
|
|
384
|
-
cds-caching provides 3 storage options:
|
|
385
|
-
|
|
386
|
-
##### In-Memory Cache (for development / small-scale uses)
|
|
387
|
-
- Simple and fast, but not persistent
|
|
388
|
-
- Not suitable for production since Node.js runtime memory is limited
|
|
389
|
-
- Data is lost when the application restarts
|
|
390
|
-
- Memory on SAP BTP Cloud Foundry is limited (up to 16 GB) and produces costs
|
|
391
|
-
|
|
392
|
-
##### SQLite (for medium-size use uses)
|
|
393
|
-
- Requires installing the adapter package: `@keyv/sqlite` (in your project)
|
|
394
|
-
- Data is stored in local SQLite database
|
|
395
|
-
- Data is persited next to SAP BTP application with disk-quota up to 10 GB
|
|
396
|
-
- Cache will be removed after each deployment to SAP BTP
|
|
397
|
-
- No distributed cache between application instances (horizontal scaling)
|
|
223
|
+
With `store: 'cds'`, each tenant's cache data lives in its own HDI container — fully isolated by CAP's Service Manager.
|
|
398
224
|
|
|
399
|
-
|
|
400
|
-
- Requires installing the adapter package: `@keyv/redis` (in your project)
|
|
401
|
-
- Persistent and supports distributed caching
|
|
402
|
-
- Works across multiple app instances, making it ideal for scalable applications
|
|
403
|
-
- Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
|
|
404
|
-
- Even trial accounts provide Redis access
|
|
405
|
-
- Redis will be non-blocking
|
|
225
|
+
Alternatively, use `store: 'redis'` for shared Redis with automatic tenant-prefixed keys:
|
|
406
226
|
|
|
407
|
-
#### Redis Development Setup
|
|
408
|
-
|
|
409
|
-
##### Running Redis Locally via Docker
|
|
410
|
-
For local development, Redis can be quickly set up using Docker. A simple docker-compose configuration provides a lightweight caching environment:
|
|
411
|
-
|
|
412
|
-
1. Create a `docker-compose.yml` file:
|
|
413
|
-
```yaml
|
|
414
|
-
services:
|
|
415
|
-
redis:
|
|
416
|
-
image: redis:latest
|
|
417
|
-
container_name: local-redis
|
|
418
|
-
ports:
|
|
419
|
-
- "6379:6379"
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
2. Run Redis with:
|
|
423
|
-
```bash
|
|
424
|
-
docker compose up -d
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
3. Modify the `package.json` configuration to connect to the local Redis instance:
|
|
428
227
|
```json
|
|
429
228
|
{
|
|
430
229
|
"cds": {
|
|
431
230
|
"requires": {
|
|
231
|
+
"multitenancy": true,
|
|
432
232
|
"caching": {
|
|
433
233
|
"impl": "cds-caching",
|
|
434
|
-
"namespace": "myCache",
|
|
435
234
|
"store": "redis",
|
|
436
|
-
"
|
|
437
|
-
"credentials": {
|
|
438
|
-
"host": "localhost",
|
|
439
|
-
"port": 6379
|
|
440
|
-
}
|
|
441
|
-
}
|
|
235
|
+
"credentials": { "socket": { "host": "localhost", "port": 6379 } }
|
|
442
236
|
}
|
|
443
237
|
}
|
|
444
238
|
}
|
|
445
239
|
}
|
|
446
240
|
```
|
|
447
241
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
#### Production Deployment on SAP BTP
|
|
242
|
+
> `isTenantAware` is automatically set to `true` in MTX mode. Set `"isTenantAware": false` in `keyManagement` to explicitly opt out.
|
|
451
243
|
|
|
452
|
-
|
|
244
|
+
## Usage Patterns
|
|
453
245
|
|
|
454
|
-
|
|
246
|
+
> **Deprecation Notice**: `cache.run()`, `cache.send()`, `cache.wrap()`, `cache.exec()` are deprecated since v1.0. Use `cache.rt.run()`, `cache.rt.send()`, `cache.rt.wrap()`, `cache.rt.exec()` instead. See [Migration Guide](docs/migration-guide.md).
|
|
455
247
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
```yaml
|
|
459
|
-
modules:
|
|
460
|
-
- name: cap-app-srv
|
|
461
|
-
# ... other module configuration ...
|
|
462
|
-
requires:
|
|
463
|
-
- name: redis-cache
|
|
464
|
-
|
|
465
|
-
resources:
|
|
466
|
-
- name: redis-cache
|
|
467
|
-
type: org.cloudfoundry.managed-service
|
|
468
|
-
parameters:
|
|
469
|
-
service: redis-cache
|
|
470
|
-
service-plan: trial
|
|
471
|
-
service-tags:
|
|
472
|
-
# Must match the kind property in the package.json
|
|
473
|
-
- cds-caching
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
> 👉 **Tip**: There is a detailed [blog series on Redis in SAP BTP](https://community.sap.com/t5/technology-blogs-by-sap/redis-on-sap-btp-understanding-service-entitlements-and-metrics/ba-p/13738371) explaining how to set up Redis and connect via SSH for local/hybrid testing, as this is by default not possible.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
### Usage Patterns
|
|
481
|
-
|
|
482
|
-
> ⚠️ **Deprecation Notice**: The following methods are deprecated since version 1.0 and will be removed in a future version:
|
|
483
|
-
> - `cache.run()` - use `cache.rt.run()` instead
|
|
484
|
-
> - `cache.exec()` - use `cache.rt.exec()` instead
|
|
485
|
-
> - `cache.wrap()` - use `cache.rt.wrap()` instead
|
|
486
|
-
> - `cache.send()` - use `cache.rt.send()` instead
|
|
487
|
-
>
|
|
488
|
-
> The `rt.xxx` methods provide enhanced functionality including:
|
|
489
|
-
> - **Read-through metadata**: Information about cache hits/misses and latency
|
|
490
|
-
> - **Consistent return format**: All methods return `{ result, cacheKey, metadata }` by default
|
|
491
|
-
>
|
|
492
|
-
> **Migration**: Simply replace `cache.method()` with `cache.rt.method()` and access the result via `.result` property if needed.
|
|
493
|
-
|
|
494
|
-
The caching service provides a flexible API for caching data in CAP applications ([full API](docs/programmatic-api.md)). Here are the key usage patterns:
|
|
495
|
-
#### 1. Low-Level Key-Value API for Read-Aside Caching
|
|
496
|
-
|
|
497
|
-
The most basic way to use cds-caching is through its key-value API:
|
|
248
|
+
### Read-Through Query Caching
|
|
498
249
|
|
|
499
250
|
```javascript
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// Retrieve the value
|
|
507
|
-
await cache.get("bp:1000001") // => businessPartnerData
|
|
508
|
-
|
|
509
|
-
// Check if the key exists
|
|
510
|
-
await cache.has("bp:1000001") // => true/false
|
|
511
|
-
|
|
512
|
-
// Delete the key
|
|
513
|
-
await cache.delete("bp:1000001")
|
|
514
|
-
|
|
515
|
-
// Clear the whole cache
|
|
516
|
-
await cache.clear()
|
|
251
|
+
const { result } = await cache.rt.run(
|
|
252
|
+
SELECT.from(BusinessPartners).where({ type: '2' }),
|
|
253
|
+
db,
|
|
254
|
+
{ ttl: 30000 }
|
|
255
|
+
)
|
|
517
256
|
```
|
|
518
257
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
```javascript
|
|
522
|
-
// With throwOnErrors: false (default)
|
|
523
|
-
try {
|
|
524
|
-
const value = await cache.get("bp:1000001")
|
|
525
|
-
if (value === undefined) {
|
|
526
|
-
// Handle cache miss or error
|
|
527
|
-
console.log("Value not found or cache error occurred")
|
|
528
|
-
}
|
|
529
|
-
} catch (error) {
|
|
530
|
-
// Only thrown for non-cache related errors
|
|
531
|
-
console.error("Unexpected error:", error)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// With throwOnErrors: true
|
|
535
|
-
try {
|
|
536
|
-
const value = await cache.get("bp:1000001")
|
|
537
|
-
// Value will be undefined if not found, but errors will be thrown
|
|
538
|
-
} catch (error) {
|
|
539
|
-
// Errors thrown for connection issues, etc.
|
|
540
|
-
console.error("Cache error:", error)
|
|
541
|
-
}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
#### 2. CQN Query Caching
|
|
545
|
-
|
|
546
|
-
For more advanced CAP integration, cache CAP's CQN queries directly. By passing in the query, a dynamic key is generated based on the CQN structure of the query. Note, that passing in queries with dynamic parameters (e.g. `SELECT.from(Foo).where({id: 1})`) will result in a different key for each query execution.
|
|
547
|
-
|
|
548
|
-
```javascript
|
|
549
|
-
// Create and execute a CQN query
|
|
550
|
-
const query = SELECT.from(BusinessPartners).where({ businessPartnerType: '2' })
|
|
551
|
-
const result = await db.run(query)
|
|
552
|
-
|
|
553
|
-
// Cache the result
|
|
554
|
-
await cache.set(query, result)
|
|
555
|
-
|
|
556
|
-
// Retrieve from cache using the same query
|
|
557
|
-
const cachedResult = await cache.get(query)
|
|
558
|
-
```
|
|
559
|
-
Handling the cache manually via read-aside pattern is possible, but the caching service provides a more convenient way to cache and retrieve CQN queries. By using the `rt.run` method, the caching service will transparently cache the result of the query and return the cached result if available for all further requests.
|
|
560
|
-
|
|
561
|
-
```javascript
|
|
562
|
-
const query = SELECT.from(BusinessPartners).where({ businessPartnerType: '2' })
|
|
563
|
-
|
|
564
|
-
// Runs the query internally and caches the result
|
|
565
|
-
const { result } = await cache.rt.run(query, db)
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
Because the cache key has been dynamically created at runtime, it will also be returned:
|
|
570
|
-
|
|
571
|
-
```javascript
|
|
572
|
-
// Access the cacheKey for later usage
|
|
573
|
-
const { result, cacheKey } = await cache.rt.run(query, db)
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
**Error Handling for Read-Through Operations:**
|
|
577
|
-
|
|
578
|
-
Read-through operations never throw errors, even when cache operations fail. Instead, they include error information in the response:
|
|
579
|
-
|
|
580
|
-
```javascript
|
|
581
|
-
// Read-through operations always return a result, even on cache errors
|
|
582
|
-
const { result, cacheKey, metadata, cacheErrors } = await cache.rt.run(query, db)
|
|
583
|
-
|
|
584
|
-
if (cacheErrors && cacheErrors.length > 0) {
|
|
585
|
-
console.log("Cache errors occurred:", cacheErrors)
|
|
586
|
-
// Result will be fetched from remote service despite cache errors
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// The result is always available, regardless of cache errors
|
|
590
|
-
return result
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
#### 3. RemoteService Request-Level Caching
|
|
594
|
-
|
|
595
|
-
Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls or even application services. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale (if not configured otherwise).
|
|
596
|
-
|
|
597
|
-
```javascript
|
|
598
|
-
// Cache the requests to an exposed external entity
|
|
599
|
-
this.on('READ', BusinessPartners, async (req, next) => {
|
|
600
|
-
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
601
|
-
let value = await cache.get(req)
|
|
602
|
-
if(!value) {
|
|
603
|
-
value = await bupa.run(req)
|
|
604
|
-
await cache.set(req, value, { ttl: 30000 })
|
|
605
|
-
}
|
|
606
|
-
return value
|
|
607
|
-
})
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
Alternatively use read-through caching via the `rt.run` method to let the caching service handle the caching transparently:
|
|
258
|
+
### Read-Through Remote Service Caching
|
|
611
259
|
|
|
612
260
|
```javascript
|
|
613
261
|
this.on('READ', BusinessPartners, async (req, next) => {
|
|
614
262
|
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
|
|
615
|
-
const { result } = await cache.rt.run(req, bupa)
|
|
616
|
-
return result
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
This will transparently cache the result of the request and return the cached result if available for all further requests.
|
|
622
|
-
|
|
623
|
-
### 4. ApplicationService Request-Level Caching
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
> Caching an entire entity should be used with caution, as it will cache all permutations of requests ($select, $filter, $expand, $orderby, etc.) on the entity, which may lead to a huge number of cache entries. Use this only for entities where you can guarantee a low number of different queries.
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
But not only external services can be cached, it's also possible to cache requests against an ApplicationService.
|
|
630
|
-
Here, you should make use of the [`prepend`](https://cap.cloud.sap/docs/node.js/core-services#srv-prepend) function, to register the `on` handler before the default handler. Thus, it is possible to first check for the cache entries and only execute the default behavior if necessary.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
```javascript
|
|
634
|
-
class MyService extends cds.ApplicationService {
|
|
635
|
-
async init() {
|
|
636
|
-
|
|
637
|
-
// Read-through caching for the full entity
|
|
638
|
-
this.prepend(() => {
|
|
639
|
-
const { MyEntity } = this.entities;
|
|
640
|
-
this.on('READ', MyEntity, async (req, next) => {
|
|
641
|
-
const cache = cds.connect.to("caching");
|
|
642
|
-
const { result } = await cache.rt.run(req, next)
|
|
643
|
-
return result;
|
|
644
|
-
});
|
|
645
|
-
});
|
|
646
|
-
return super.init()
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
#### 5. ApplicationService Request-Level Caching with Annotations
|
|
652
|
-
|
|
653
|
-
Alternatively to doing this via code, you can use annotations to enable caching on service entities or OData functions. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
|
|
654
|
-
|
|
655
|
-
```
|
|
656
|
-
service MyService {
|
|
657
|
-
@cache: {
|
|
658
|
-
ttl: 10000 // 10 seconds
|
|
659
|
-
}
|
|
660
|
-
entity BusinessPartners as projection on BusinessPartner {
|
|
661
|
-
// ... entity definition
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
@cache: {
|
|
666
|
-
ttl: 100000, // 10 seconds
|
|
667
|
-
tags: [{
|
|
668
|
-
template: 'user-{user}'
|
|
669
|
-
}]
|
|
670
|
-
}
|
|
671
|
-
function getUserPreferences() returns array of Preferences;
|
|
672
|
-
}
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
#### 5. Function Caching
|
|
676
|
-
|
|
677
|
-
While not directly related to CAP functionality, the caching service provides two methods for read-through caching of JavaScript functions:
|
|
678
|
-
|
|
679
|
-
```javascript
|
|
680
|
-
// Using wrap() to create a cached version of a function
|
|
681
|
-
const fetchBusinessPartnerData = async (businessPartnerId, includeAddresses) => {
|
|
682
|
-
// ... some expensive computation to fetch BP data
|
|
683
|
-
return businessPartnerData
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Creates a cached version of the function.
|
|
687
|
-
const cachedBpOperation = cache.rt.wrap("bp-data", fetchBusinessPartnerData, {
|
|
688
|
-
ttl: 3600,
|
|
689
|
-
tags: ['business-partner']
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
// Each call checks cache first, only executes if cache miss
|
|
693
|
-
const result = await cachedBpOperation("1000001", true)
|
|
694
|
-
|
|
695
|
-
// Using exec() for immediate execution with caching
|
|
696
|
-
const result = await cache.rt.exec("product-data", async (productId) => {
|
|
697
|
-
// ... some expensive computation to fetch product data
|
|
698
|
-
return productData
|
|
699
|
-
}, ["1000001"], {
|
|
700
|
-
ttl: 3600,
|
|
701
|
-
tags: ['product']
|
|
702
|
-
})
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
The key differences between `rt.wrap()` and `rt.exec()`:
|
|
706
|
-
- `rt.wrap()` returns a new function that includes caching logic
|
|
707
|
-
- `rt.exec()` immediately executes the function and caches the result
|
|
708
|
-
- Use `rt.wrap()` when you need to reuse the cached function multiple times
|
|
709
|
-
- Use `rt.exec()` for one-off executions with caching
|
|
710
|
-
|
|
711
|
-
#### Dynamic Key Generation
|
|
712
|
-
|
|
713
|
-
All `rt.xxx` methods automatically generate dynamic cache keys based on function arguments (`wrap`, `exec`) and request/query parameters. This ensures that different function calls with different arguments are cached separately.
|
|
714
|
-
|
|
715
|
-
```javascript
|
|
716
|
-
// Different arguments = different cache keys
|
|
717
|
-
const result1 = await cachedBpOperation("1000001", true) // Cache key: "bp-data:1000001:true"
|
|
718
|
-
const result2 = await cachedBpOperation("1000002", false) // Cache key: "bp-data:1000002:false"
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
You can override this behavior by providing a custom key template:
|
|
722
|
-
|
|
723
|
-
```javascript
|
|
724
|
-
const cachedOperation = cache.rt.wrap("bp-profile", fetchBusinessPartnerData, {
|
|
725
|
-
key: "profile:{args[0]}:{args[1]}"
|
|
726
|
-
})
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
For detailed information on how read-through keys are generated and configured, see [Key Management](docs/key-management.md).
|
|
730
|
-
|
|
731
|
-
### Cache Invalidation Strategies
|
|
732
|
-
|
|
733
|
-
The caching service provides different strategies to invalidate cached values.
|
|
734
|
-
|
|
735
|
-
**IMPORTANT: You should not use cds-caching without a proper invalidation strategy.**
|
|
736
|
-
|
|
737
|
-
#### 1. Time-Based (TTL)
|
|
738
|
-
|
|
739
|
-
The most basic strategy is to use a time-to-live (TTL) for the cache. The caching service will automatically delete the value from the cache after the specified TTL has expired.
|
|
740
|
-
The TTL can be specified for individually through all cache methods (e.g. `set`, `run`, `send`, `wrap`, `exec`).
|
|
741
|
-
|
|
742
|
-
```javascript
|
|
743
|
-
// Store with 60 seconds TTL
|
|
744
|
-
await cache.set("key", "value", { ttl: 60000 })
|
|
745
|
-
|
|
746
|
-
// Run with 30 seconds TTL
|
|
747
|
-
const { result } = await cache.rt.run(query, db, { ttl: 30000 })
|
|
748
|
-
|
|
749
|
-
// Send with 10 seconds TTL
|
|
750
|
-
const { result } = await cache.rt.send(request, service, { ttl: 10000 })
|
|
751
|
-
|
|
752
|
-
// Wrap with 10 seconds TTL
|
|
753
|
-
const cachedOperation = cache.rt.wrap("key", expensiveOperation, { ttl: 10000 })
|
|
754
|
-
|
|
755
|
-
// Exec with 10 seconds TTL
|
|
756
|
-
const { result } = await cache.rt.exec("key", async () => {
|
|
757
|
-
// ... some expensive computation
|
|
263
|
+
const { result } = await cache.rt.run(req, bupa, { ttl: 30000 })
|
|
758
264
|
return result
|
|
759
|
-
}, [] {
|
|
760
|
-
ttl: 10000
|
|
761
265
|
})
|
|
762
266
|
```
|
|
763
267
|
|
|
764
|
-
|
|
268
|
+
### ApplicationService Caching with `prepend`
|
|
765
269
|
|
|
766
|
-
Key-based invalidation is a way to invalidate cache entries based on a specific key.
|
|
767
|
-
|
|
768
|
-
```javascript
|
|
769
|
-
await cache.delete("key")
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
Keys are critical for cache invalidation. To allow custom key management, you can override the auto-generated key. This option is available for all essential methods (e.g cache.set, cache.rt.run, cache.rt.send, cache.createKey) and for the annotations.
|
|
773
|
-
|
|
774
|
-
**Read-Through (RT) Methods**: All `rt.xxx` methods automatically generate dynamic cache keys and return them in the response. The generated keys include configurable context (user, tenant, locale) and a content hash. For detailed information on RT key generation, see [Key Management](docs/key-management.md).
|
|
775
|
-
|
|
776
270
|
```javascript
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
// Automatically build the key for retrieval/deletion
|
|
784
|
-
cache.createKey(SELECT.from(BusinessPartners)) // key: bd3f3690d3e96a569bd89d9e207a89af
|
|
785
|
-
|
|
786
|
-
// Override and use your own key based on a fixed value
|
|
787
|
-
await cache.set(SELECT.from(BusinessPartners, 1000001), { key: "bp:1000001" })
|
|
788
|
-
|
|
789
|
-
// RT methods return the generated cache key
|
|
790
|
-
const { result, cacheKey } = await cache.rt.run(query, db)
|
|
791
|
-
console.log('Generated key:', cacheKey) // e.g., "tenant-acme:user-john:locale-en:hash-abc123"
|
|
792
|
-
|
|
793
|
-
// Override RT key template for requests
|
|
794
|
-
await cache.rt.run(req, remoteService, { key: "mykey:{tenant}:{user}:{locale}:{hash}" })
|
|
795
|
-
|
|
796
|
-
// This requests will be cached for all users and for each locale
|
|
797
|
-
await cache.rt.run(req, remoteService, { key: "mykey:{user}:{locale}:{hash}" })
|
|
798
|
-
|
|
799
|
-
// Function wrapping with custom key template
|
|
800
|
-
const cachedFunction = cache.rt.wrap("user-data", expensiveOperation, {
|
|
801
|
-
key: "user:{user}:{args[0]}"
|
|
271
|
+
this.prepend(() => {
|
|
272
|
+
this.on('READ', MyEntity, async (req, next) => {
|
|
273
|
+
const cache = await cds.connect.to("caching")
|
|
274
|
+
const { result } = await cache.rt.run(req, next)
|
|
275
|
+
return result
|
|
276
|
+
})
|
|
802
277
|
})
|
|
803
278
|
```
|
|
804
279
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
Tags are a way to invalidate cache entries based on a specific tag. Tags need to be provided explicitly when storing a value in the cache and are supported for all cache methods (e.g. `set`, `rt.run`, `rt.send`, `rt.wrap`, `rt.exec`).
|
|
808
|
-
Tags can be provided as an array of strings or as an array of objects with the following properties:
|
|
809
|
-
- `value`: The value to use for the tag.
|
|
810
|
-
- `data`: A field from the value to use for the tag. This is working for objects and arrays of objects.
|
|
811
|
-
- `prefix`: A prefix that will be added to the tag.
|
|
812
|
-
- `suffix`: A suffix that will be added to the tag.
|
|
813
|
-
- `template`: A template string that will be used to generate the tag (e.g. `{tenant}-{locale}-{user}-{hash}`). This is useful for dynamic tags based on cds.Requests.
|
|
814
|
-
Templates support the following properties:
|
|
815
|
-
- `{user}`: The current user
|
|
816
|
-
- `{tenant}`: The current tenant
|
|
817
|
-
- `{locale}`: The current locale
|
|
818
|
-
- `{hash}`: The hash of the current query
|
|
819
|
-
|
|
820
|
-
|
|
280
|
+
### Function Caching
|
|
821
281
|
|
|
822
282
|
```javascript
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
// Store with template tag (will generate a tag like "tenant-global-user-anonymous")
|
|
829
|
-
await cache.set("bp:1000001", businessPartnerData, {
|
|
830
|
-
tags: [{ template: "tenant-{tenant}-user-{user}" }]
|
|
831
|
-
})
|
|
283
|
+
// Wrap: create a cached version of a function
|
|
284
|
+
const cachedFn = cache.rt.wrap("bp-data", fetchBPData, { ttl: 3600, tags: ['bp'] })
|
|
285
|
+
const { result } = await cachedFn("1000001", true)
|
|
832
286
|
|
|
833
|
-
//
|
|
834
|
-
await cache.
|
|
835
|
-
tags: [{ data: "productId", prefix: "product-" }]
|
|
836
|
-
})
|
|
837
|
-
|
|
838
|
-
// Invalidate by tag
|
|
839
|
-
await cache.deleteByTag('bp-1000001')
|
|
287
|
+
// Exec: immediate one-off execution with caching
|
|
288
|
+
const { result } = await cache.rt.exec("product", fetchProduct, ["1000001"], { ttl: 3600 })
|
|
840
289
|
```
|
|
841
|
-
This is really useful for invalidating cache entries based on a specific attribute or context.
|
|
842
|
-
|
|
843
|
-
#### 3. Dynamic Tags
|
|
844
290
|
|
|
845
|
-
|
|
291
|
+
### Cache Invalidation
|
|
846
292
|
|
|
847
293
|
```javascript
|
|
294
|
+
// Time-based (TTL)
|
|
295
|
+
await cache.set("key", value, { ttl: 60000 })
|
|
848
296
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
businessPartner: 1000001,
|
|
852
|
-
name: 'Acme Corporation'
|
|
853
|
-
},
|
|
854
|
-
{
|
|
855
|
-
businessPartner: 1000002,
|
|
856
|
-
name: 'Tech Solutions Ltd'
|
|
857
|
-
}
|
|
858
|
-
]
|
|
859
|
-
|
|
860
|
-
// Store with dynamic tags
|
|
861
|
-
await cache.set("bp-list", businessPartners, {
|
|
862
|
-
tags: [
|
|
863
|
-
{ data: 'businessPartner', prefix: 'bp-' },
|
|
864
|
-
{ value: "businessPartner" }
|
|
865
|
-
]
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
// Introspect the tags
|
|
869
|
-
const tags = await cache.tags("bp-list") // => ["bp-1000001", "bp-1000002", "businessPartner"]
|
|
870
|
-
|
|
871
|
-
// Invalidate by tag
|
|
872
|
-
await cache.deleteByTag('bp-1000001')
|
|
873
|
-
await cache.deleteByTag('bp-1000002')
|
|
874
|
-
```
|
|
297
|
+
// Key-based
|
|
298
|
+
await cache.delete("bp:1000001")
|
|
875
299
|
|
|
876
|
-
|
|
300
|
+
// Tag-based
|
|
301
|
+
await cache.set("bp:1000001", data, { tags: [{ value: "bp-list" }] })
|
|
302
|
+
await cache.set("bp:1000002", data, { tags: [{ value: "bp-list" }] })
|
|
303
|
+
await cache.deleteByTag("bp-list")
|
|
877
304
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
tags: [{ data:
|
|
305
|
+
// Dynamic tags from data
|
|
306
|
+
await cache.set("bp-list", bpArray, {
|
|
307
|
+
tags: [{ data: "businessPartner", prefix: "bp-" }]
|
|
881
308
|
})
|
|
882
309
|
```
|
|
883
310
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
### Cache Iteration
|
|
887
|
-
|
|
888
|
-
The caching service provides an iterator interface to traverse all cache entries:
|
|
889
|
-
|
|
890
|
-
```javascript
|
|
891
|
-
const iterator = await cache.iterator()
|
|
892
|
-
|
|
893
|
-
for await (const entry of iterator) {
|
|
894
|
-
console.log(entry)
|
|
895
|
-
}
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
This will return an iterator over all cache entries. You can use this to traverse all cache entries and invalidate them based on a specific condition. You should only use this for small caches (e.g. by using multiple caching services with different namespaces).
|
|
899
|
-
|
|
900
|
-
### TypeScript Support
|
|
901
|
-
|
|
902
|
-
cds-caching includes comprehensive TypeScript definitions. The library is written in JavaScript but provides full TypeScript support for better development experience.
|
|
903
|
-
|
|
904
|
-
#### Basic Usage with TypeScript
|
|
905
|
-
|
|
906
|
-
```typescript
|
|
907
|
-
import { CachingService, CacheOptions, ReadThroughResult } from 'cds-caching';
|
|
908
|
-
|
|
909
|
-
const cache = await cds.connect.to('caching') as CachingService;
|
|
910
|
-
|
|
911
|
-
// Basic cache operations
|
|
912
|
-
await cache.set('my-key', { data: 'value' }, { ttl: 3600 });
|
|
913
|
-
const value = await cache.get('my-key');
|
|
914
|
-
|
|
915
|
-
// Read-through operations with full type safety
|
|
916
|
-
const { result, cacheKey, metadata } = await cache.rt.send(request, service, {
|
|
917
|
-
ttl: 1800,
|
|
918
|
-
tags: ['user-data']
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
// Function wrapping with type inference
|
|
922
|
-
const cachedFunction = cache.rt.wrap('expensive-operation', async (id: string) => {
|
|
923
|
-
return await this.performExpensiveOperation(id);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
const { result: operationResult } = await cachedFunction('user-123');
|
|
927
|
-
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
### OData Service Caching Considerations
|
|
931
|
-
|
|
932
|
-
While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
|
|
933
|
-
|
|
934
|
-
1. **Data Consistency**: OData services expose live business data that frequently changes. Caching responses without an appropriate invalidation strategy can lead to outdated or incorrect data being served.
|
|
935
|
-
|
|
936
|
-
2. **Query Complexity**: OData allows dynamic query parameters ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
|
|
937
|
-
|
|
938
|
-
3. **Payload Size**: Full OData responses can be significantly large, consuming cache memory inefficiently compared to caching targeted CQN queries or specific request results.
|
|
939
|
-
|
|
940
|
-
Instead of caching entire OData service responses, focus on:
|
|
941
|
-
- Specific queries or request results
|
|
942
|
-
- Static master data
|
|
943
|
-
- Computed results
|
|
944
|
-
- Remote service calls with stable data
|
|
945
|
-
|
|
946
|
-
### Best Practices
|
|
947
|
-
|
|
948
|
-
1. **Cache Selectively**: Not all data benefits from caching. Focus on:
|
|
949
|
-
- Frequently accessed, rarely changed data
|
|
950
|
-
- Computationally expensive operations
|
|
951
|
-
- Remote service calls with stable data
|
|
952
|
-
|
|
953
|
-
2. **Use Appropriate TTLs**: Set TTLs based on data volatility:
|
|
954
|
-
- Short TTLs (seconds/minutes) for frequently changing data
|
|
955
|
-
- Longer TTLs (hours/days) for stable reference data
|
|
956
|
-
|
|
957
|
-
3. **Implement Cache Tags**: Use tags for granular cache invalidation:
|
|
958
|
-
- Group related cache entries
|
|
959
|
-
- Enable targeted invalidation
|
|
960
|
-
- Use dynamic tags for user/tenant-specific caching
|
|
961
|
-
|
|
962
|
-
4. **Monitor Cache Performance**: Regularly check cache statistics:
|
|
963
|
-
- Hit rates
|
|
964
|
-
- Memory usage
|
|
965
|
-
- Response times
|
|
966
|
-
- Error rates
|
|
967
|
-
|
|
968
|
-
### Limitations and Considerations
|
|
969
|
-
|
|
970
|
-
1. **Memory Usage**: Monitor cache size, especially with in-memory storage
|
|
971
|
-
2. **Consistency**: Consider data freshness requirements when setting TTLs
|
|
972
|
-
3. **Multi-Tenant**: Use appropriate namespacing and key strategies
|
|
973
|
-
4. **Redis Setup**: Ensure proper configuration for production use
|
|
974
|
-
|
|
975
|
-
## Enhanced Statistics & Monitoring
|
|
976
|
-
|
|
977
|
-
The plugin now includes comprehensive statistics and monitoring capabilities that provide deep insights into cache performance and help optimize cache usage.
|
|
978
|
-
|
|
979
|
-
[See the full Metrics Guide →](docs/metrics-guide.md)
|
|
980
|
-
|
|
981
|
-
### Key Features
|
|
982
|
-
|
|
983
|
-
- **Real-time Metrics**: Monitor cache performance with detailed hit rates, latencies, and throughput
|
|
984
|
-
- **Key-level Tracking**: Track performance metrics for individual cache keys
|
|
985
|
-
- **Historical Data**: Store and analyze metrics over time (hourly/daily periods)
|
|
986
|
-
- **Performance Analytics**: Calculate cache efficiency, error rates, and response times
|
|
987
|
-
- **Runtime Configuration**: Enable/disable metrics at runtime without restart
|
|
988
|
-
- **API Access**: Access metrics programmatically or via OData service
|
|
989
|
-
|
|
990
|
-
### Metrics Overview
|
|
991
|
-
|
|
992
|
-
cds-caching provides two types of metrics:
|
|
993
|
-
|
|
994
|
-
#### 1. General Cache Metrics
|
|
995
|
-
Track overall cache performance including:
|
|
996
|
-
- **Hit/Miss Statistics**: Total hits, misses, and hit ratios
|
|
997
|
-
- **Latency Metrics**: Average, min, max, and percentile latencies for hits and misses
|
|
998
|
-
- **Performance Metrics**: Throughput (requests/second), error rates, cache efficiency
|
|
999
|
-
- **Memory Usage**: Current memory consumption and item count
|
|
1000
|
-
- **Native Operations**: Counts of direct cache operations (set, get, delete, etc.)
|
|
1001
|
-
|
|
1002
|
-
#### 2. Key-level Metrics
|
|
1003
|
-
Track performance for individual cache keys including:
|
|
1004
|
-
- **Key-specific Statistics**: Hits, misses, and hit ratios per key
|
|
1005
|
-
- **Context Information**: Data type, service name, entity name, operation type
|
|
1006
|
-
- **Enhanced Metadata**: Query text, request info, function names, user/tenant context
|
|
1007
|
-
- **Performance Tracking**: Latency and throughput metrics per key
|
|
1008
|
-
|
|
1009
|
-
### Enabling Metrics
|
|
1010
|
-
|
|
1011
|
-
Metrics are disabled by default to minimize performance impact. They can only be enabled/disabled via the programmatic API or OData API at runtime, not through package.json configuration.
|
|
1012
|
-
|
|
1013
|
-
To enable metrics programmatically:
|
|
1014
|
-
|
|
1015
|
-
```javascript
|
|
1016
|
-
// Connect to the caching service
|
|
1017
|
-
const cache = await cds.connect.to("caching")
|
|
1018
|
-
|
|
1019
|
-
// Enable metrics at runtime
|
|
1020
|
-
await cache.setMetricsEnabled(true)
|
|
1021
|
-
await cache.setKeyMetricsEnabled(true)
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
Or via OData API:
|
|
1025
|
-
|
|
1026
|
-
```http
|
|
1027
|
-
### Enable general metrics
|
|
1028
|
-
POST http://localhost:4004/odata/v4/caching-api/Caches('caching')/setMetricsEnabled
|
|
1029
|
-
Content-Type: application/json
|
|
311
|
+
For more usage patterns, error handling details, and TypeScript support, see [Programmatic API](docs/programmatic-api.md).
|
|
1030
312
|
|
|
1031
|
-
|
|
1032
|
-
"enabled": true
|
|
1033
|
-
}
|
|
313
|
+
## Statistics & Monitoring
|
|
1034
314
|
|
|
1035
|
-
|
|
1036
|
-
POST http://localhost:4004/odata/v4/caching-api/Caches('caching')/setKeyMetricsEnabled
|
|
1037
|
-
Content-Type: application/json
|
|
315
|
+
To persist metrics to the database, add a `statistics` block to your configuration. This automatically loads the required data model (`Caches`, `Metrics`, `KeyMetrics` tables):
|
|
1038
316
|
|
|
317
|
+
```json
|
|
1039
318
|
{
|
|
1040
|
-
"
|
|
319
|
+
"cds": {
|
|
320
|
+
"requires": {
|
|
321
|
+
"caching": {
|
|
322
|
+
"impl": "cds-caching",
|
|
323
|
+
"store": "redis",
|
|
324
|
+
"statistics": {
|
|
325
|
+
"enabled": true,
|
|
326
|
+
"persistenceInterval": 60000
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
1041
331
|
}
|
|
1042
332
|
```
|
|
1043
333
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
The caching service provides comprehensive metrics collection and persistence capabilities. Metrics are automatically collected during cache operations and can be accessed both in real-time and from historical data.
|
|
1047
|
-
|
|
1048
|
-
#### Metrics Persistence
|
|
1049
|
-
|
|
1050
|
-
**Transient Metrics**: Current statistics are kept in memory and provide real-time insights into cache performance:
|
|
1051
|
-
- Hit/miss ratios
|
|
1052
|
-
- Current latency statistics
|
|
1053
|
-
- Active cache entries
|
|
1054
|
-
- Key-level performance data
|
|
1055
|
-
|
|
1056
|
-
**Persisted Metrics**: Historical data is automatically stored in the database for long-term analysis:
|
|
1057
|
-
- Hourly aggregated statistics
|
|
1058
|
-
- Key-level metrics over time
|
|
1059
|
-
- Performance trends and patterns
|
|
1060
|
-
- Cache efficiency analysis
|
|
1061
|
-
|
|
1062
|
-
#### Current Statistics
|
|
334
|
+
You can also enable metrics at runtime:
|
|
1063
335
|
|
|
1064
336
|
```javascript
|
|
1065
|
-
// Connect to the caching service
|
|
1066
337
|
const cache = await cds.connect.to("caching")
|
|
1067
|
-
|
|
1068
|
-
// Get current statistics
|
|
1069
|
-
const stats = await cache.getCurrentStats()
|
|
1070
|
-
console.log('Hit ratio:', stats.hitRatio)
|
|
1071
|
-
console.log('Average hit latency:', stats.avgHitLatency)
|
|
1072
|
-
console.log('Throughput:', stats.throughput)
|
|
1073
|
-
|
|
1074
|
-
// Get current key metrics
|
|
1075
|
-
const keyMetrics = await cache.getCurrentKeyMetrics()
|
|
1076
|
-
for (const [key, metrics] of keyMetrics) {
|
|
1077
|
-
console.log(`Key ${key}:`, {
|
|
1078
|
-
hits: metrics.hits,
|
|
1079
|
-
misses: metrics.misses,
|
|
1080
|
-
hitRatio: metrics.hitRatio,
|
|
1081
|
-
avgHitLatency: metrics.avgHitLatency
|
|
1082
|
-
})
|
|
1083
|
-
}
|
|
1084
|
-
```
|
|
1085
|
-
|
|
1086
|
-
#### Historical Metrics
|
|
1087
|
-
|
|
1088
|
-
```javascript
|
|
1089
|
-
// Get metrics for a specific time period
|
|
1090
|
-
const from = new Date('2024-01-01')
|
|
1091
|
-
const to = new Date('2024-01-31')
|
|
1092
|
-
const historicalStats = await cache.getMetrics(from, to)
|
|
1093
|
-
|
|
1094
|
-
// Get key-specific metrics
|
|
1095
|
-
const keyStats = await cache.getKeyMetrics('my-cache-key', from, to)
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
#### Runtime Configuration
|
|
1099
|
-
|
|
1100
|
-
```javascript
|
|
1101
|
-
// Enable/disable metrics at runtime
|
|
1102
338
|
await cache.setMetricsEnabled(true)
|
|
1103
339
|
await cache.setKeyMetricsEnabled(true)
|
|
1104
340
|
|
|
1105
|
-
|
|
1106
|
-
const config = await cache.getRuntimeConfiguration()
|
|
1107
|
-
console.log('Metrics enabled:', config.metricsEnabled)
|
|
1108
|
-
console.log('Key metrics enabled:', config.keyMetricsEnabled)
|
|
1109
|
-
|
|
1110
|
-
// Clear metrics
|
|
1111
|
-
await cache.clearMetrics()
|
|
1112
|
-
await cache.clearKeyMetrics()
|
|
341
|
+
const stats = await cache.getCurrentMetrics()
|
|
1113
342
|
```
|
|
1114
343
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
#### General Cache Statistics (Metrics Entity)
|
|
344
|
+
To add the monitoring dashboard to your project, run:
|
|
1118
345
|
|
|
1119
|
-
```
|
|
1120
|
-
|
|
1121
|
-
// Entity identification
|
|
1122
|
-
ID: "daily:2024-01-15", // Unique identifier (period:date)
|
|
1123
|
-
cache: "caching", // Cache name
|
|
1124
|
-
timestamp: "2024-01-15T10:30:00Z", // When metrics were recorded
|
|
1125
|
-
period: "daily", // Aggregation period (hourly/daily/monthly)
|
|
1126
|
-
|
|
1127
|
-
// Read-through metrics
|
|
1128
|
-
hits: 1500, // Number of cache hits
|
|
1129
|
-
misses: 300, // Number of cache misses
|
|
1130
|
-
errors: 5, // Number of errors
|
|
1131
|
-
totalRequests: 1800, // Total read-through requests
|
|
1132
|
-
|
|
1133
|
-
// Read-through latency metrics (milliseconds)
|
|
1134
|
-
avgHitLatency: 2.5, // Average hit latency
|
|
1135
|
-
minHitLatency: 0.1, // Minimum hit latency
|
|
1136
|
-
maxHitLatency: 15.2, // Maximum hit latency
|
|
1137
|
-
avgMissLatency: 45.8, // Average miss latency
|
|
1138
|
-
minMissLatency: 12.3, // Minimum miss latency
|
|
1139
|
-
maxMissLatency: 120.5, // Maximum miss latency
|
|
1140
|
-
avgReadThroughLatency: 8.9, // Average read-through latency
|
|
1141
|
-
|
|
1142
|
-
// Read-through performance metrics
|
|
1143
|
-
hitRatio: 0.833, // Hit ratio as percentage (83.3%)
|
|
1144
|
-
throughput: 25.5, // Requests per second
|
|
1145
|
-
errorRate: 0.003, // Error rate as percentage (0.3%)
|
|
1146
|
-
cacheEfficiency: 18.3, // Miss latency / hit latency ratio
|
|
1147
|
-
|
|
1148
|
-
// Native operation metrics
|
|
1149
|
-
nativeSets: 200, // Number of direct set operations
|
|
1150
|
-
nativeGets: 800, // Number of direct get operations
|
|
1151
|
-
nativeDeletes: 50, // Number of direct delete operations
|
|
1152
|
-
nativeClears: 2, // Number of clear operations
|
|
1153
|
-
nativeDeleteByTags: 10, // Number of delete-by-tag operations
|
|
1154
|
-
nativeErrors: 1, // Number of native operation errors
|
|
1155
|
-
totalNativeOperations: 1063, // Total native operations
|
|
1156
|
-
nativeThroughput: 17.7, // Native operations per second
|
|
1157
|
-
nativeErrorRate: 0.001, // Native operation error rate (0.1%)
|
|
1158
|
-
|
|
1159
|
-
// System metrics
|
|
1160
|
-
memoryUsage: 52428800, // Memory usage in bytes
|
|
1161
|
-
itemCount: 150, // Number of items in cache
|
|
1162
|
-
uptimeMs: 7200000 // Cache uptime in milliseconds
|
|
1163
|
-
}
|
|
346
|
+
```bash
|
|
347
|
+
cds add caching-dashboard
|
|
1164
348
|
```
|
|
1165
349
|
|
|
1166
|
-
|
|
350
|
+
This copies a pre-built UI5 dashboard into your `app/` folder and exposes the `CachingApiService`. After running `cds watch`, the dashboard is available at `/caching-dashboard/index.html`.
|
|
1167
351
|
|
|
1168
|
-
|
|
1169
|
-
{
|
|
1170
|
-
// Entity identification
|
|
1171
|
-
ID: "key:user-preferences:123", // Unique identifier
|
|
1172
|
-
cache: "caching", // Cache name
|
|
1173
|
-
keyName: "user-preferences:123", // Cache key name
|
|
1174
|
-
lastAccess: "2024-01-15T10:30:00Z", // Last access time
|
|
1175
|
-
period: "current", // Period type (current/hourly/daily)
|
|
1176
|
-
operationType: "read_through", // Operation category (read_through/native/mixed)
|
|
1177
|
-
|
|
1178
|
-
// Read-through metrics
|
|
1179
|
-
hits: 45, // Number of hits for this key
|
|
1180
|
-
misses: 5, // Number of misses for this key
|
|
1181
|
-
errors: 0, // Number of errors for this key
|
|
1182
|
-
totalRequests: 50, // Total requests for this key
|
|
1183
|
-
hitRatio: 0.9, // Hit ratio for this key (90%)
|
|
1184
|
-
cacheEfficiency: 21.2, // Cache efficiency for this key
|
|
1185
|
-
|
|
1186
|
-
// Read-through latency metrics (milliseconds)
|
|
1187
|
-
avgHitLatency: 1.2, // Average hit latency for this key
|
|
1188
|
-
minHitLatency: 0.5, // Minimum hit latency for this key
|
|
1189
|
-
maxHitLatency: 3.1, // Maximum hit latency for this key
|
|
1190
|
-
avgMissLatency: 25.4, // Average miss latency for this key
|
|
1191
|
-
minMissLatency: 15.2, // Minimum miss latency for this key
|
|
1192
|
-
maxMissLatency: 45.8, // Maximum miss latency for this key
|
|
1193
|
-
avgReadThroughLatency: 3.8, // Average read-through latency for this key
|
|
1194
|
-
|
|
1195
|
-
// Read-through performance metrics
|
|
1196
|
-
throughput: 2.5, // Requests per second for this key
|
|
1197
|
-
errorRate: 0.0, // Error rate for this key (0%)
|
|
1198
|
-
|
|
1199
|
-
// Native operation metrics for this key
|
|
1200
|
-
nativeHits: 10, // Native hits for this key
|
|
1201
|
-
nativeMisses: 2, // Native misses for this key
|
|
1202
|
-
nativeSets: 5, // Native sets for this key
|
|
1203
|
-
nativeDeletes: 1, // Native deletes for this key
|
|
1204
|
-
nativeClears: 0, // Native clears for this key
|
|
1205
|
-
nativeDeleteByTags: 0, // Native delete-by-tags for this key
|
|
1206
|
-
nativeErrors: 0, // Native errors for this key
|
|
1207
|
-
totalNativeOperations: 18, // Total native operations for this key
|
|
1208
|
-
nativeThroughput: 0.5, // Native operations per second for this key
|
|
1209
|
-
nativeErrorRate: 0.0, // Native error rate for this key
|
|
1210
|
-
|
|
1211
|
-
// Context and metadata
|
|
1212
|
-
dataType: "request", // Type of data (query/request/function/custom)
|
|
1213
|
-
operation: "READ", // Cache operation type
|
|
1214
|
-
metadata: '{"ttl":3600}', // JSON string with additional metadata
|
|
1215
|
-
context: '{"user":"john.doe","tenant":"acme"}', // JSON string with context
|
|
1216
|
-
query: "SELECT * FROM UserPreferences WHERE userId = '123'", // CQL query text
|
|
1217
|
-
subject: '{"entity":"UserPreferences"}', // JSON string with subject info
|
|
1218
|
-
target: "UserService", // Target service name
|
|
1219
|
-
tenant: "acme", // Tenant information
|
|
1220
|
-
user: "john.doe", // User information
|
|
1221
|
-
locale: "en-US", // Locale information
|
|
1222
|
-
cacheOptions: '{"ttl":3600}', // JSON string with cache options
|
|
1223
|
-
timestamp: "2024-01-15T09:00:00Z" // When this key was first accessed
|
|
1224
|
-
}
|
|
1225
|
-
```
|
|
352
|
+

|
|
1226
353
|
|
|
1227
|
-
|
|
354
|
+
See the [Dashboard Guide](docs/dashboard.md) for details on features, security, and customization.
|
|
1228
355
|
|
|
1229
|
-
|
|
1230
|
-
2. **Monitor Memory Usage**: Key metrics can consume significant memory for large caches
|
|
1231
|
-
3. **Set Appropriate Intervals**: Balance persistence frequency with performance impact
|
|
1232
|
-
4. **Use Historical Data**: Analyze trends over time to optimize cache configuration
|
|
1233
|
-
5. **Monitor Error Rates**: High error rates may indicate configuration issues
|
|
1234
|
-
6. **Track Cache Efficiency**: Aim for high cache efficiency (miss latency >> hit latency)
|
|
356
|
+
[See the full Metrics Guide →](docs/metrics-guide.md)
|
|
1235
357
|
|
|
1236
358
|
## API Reference
|
|
1237
359
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
### Programmatic API
|
|
1244
|
-
|
|
1245
|
-
The programmatic API provides methods for direct cache operations within your CAP application:
|
|
1246
|
-
|
|
1247
|
-
```javascript
|
|
1248
|
-
// Connect to the caching service
|
|
1249
|
-
const cache = await cds.connect.to("caching")
|
|
1250
|
-
|
|
1251
|
-
// Basic operations
|
|
1252
|
-
await cache.set("key", "value")
|
|
1253
|
-
const value = await cache.get("key")
|
|
1254
|
-
await cache.delete("key")
|
|
1255
|
-
|
|
1256
|
-
// Read-through operations
|
|
1257
|
-
const result = await cache.rt.run(query, db)
|
|
1258
|
-
const result = await cache.rt.send(request, service)
|
|
1259
|
-
|
|
1260
|
-
// Metrics and statistics
|
|
1261
|
-
const stats = await cache.getCurrentStats()
|
|
1262
|
-
const keyMetrics = await cache.getCurrentKeyMetrics()
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
[See the full Programmatic API Reference →](docs/programmatic-api.md)
|
|
1266
|
-
|
|
1267
|
-
### OData API
|
|
1268
|
-
|
|
1269
|
-
The OData API provides REST endpoints for external applications, monitoring tools, and administrative interfaces:
|
|
1270
|
-
|
|
1271
|
-
```http
|
|
1272
|
-
### Get cache statistics
|
|
1273
|
-
GET /odata/v4/caching-api/Metrics?$filter=cache eq 'mycache'
|
|
1274
|
-
|
|
1275
|
-
### Get cache entries
|
|
1276
|
-
GET /odata/v4/caching-api/Caches('mycache')/getEntries()
|
|
1277
|
-
|
|
1278
|
-
### Clear cache
|
|
1279
|
-
POST /odata/v4/caching-api/Caches('mycache')/clear()
|
|
1280
|
-
```
|
|
1281
|
-
|
|
1282
|
-
[See the full OData API Reference →](docs/odata-api.md)
|
|
360
|
+
| API | Description |
|
|
361
|
+
|-----|-------------|
|
|
362
|
+
| [Programmatic API](docs/programmatic-api.md) | JavaScript methods for cache operations |
|
|
363
|
+
| [OData API](docs/odata-api.md) | REST endpoints for monitoring and management |
|
|
1283
364
|
|
|
1284
|
-
|
|
365
|
+
## Contributing
|
|
1285
366
|
|
|
1286
|
-
Contributions are welcome! Please
|
|
367
|
+
Contributions are welcome! Please submit pull requests to the [repository](https://github.com/mikezaschka/cds-caching).
|
|
1287
368
|
|
|
1288
|
-
|
|
369
|
+
## License
|
|
1289
370
|
|
|
1290
371
|
This project is licensed under the MIT License - see the LICENSE file for details.
|