Skip to main content

reddb_server/runtime/
config_matrix.rs

1//! Performance / operational config matrix.
2//!
3//! Two tiers:
4//!
5//! - **Tier A (`Critical`)** — self-healing on boot. If the key is
6//!   missing from `red.config`, the loader writes the default in.
7//!   Operators always see these via `SHOW CONFIG` so they know what
8//!   guarantees and tuning they have.
9//! - **Tier B (`Optional`)** — in-memory default. Never self-populated.
10//!   Appears in `SHOW CONFIG` only after an explicit `SET CONFIG`.
11//!
12//! The matrix is the single source of truth for perf / durability /
13//! concurrency / storage keys introduced by the perf-parity push.
14//! It intentionally does **not** cover the pre-existing `red.*`
15//! trees (ai, server, storage, search, etc.) — those have their own
16//! lifecycle in `impl_core`. Keys here live under the new
17//! `cache.*`, `durability.*`, `concurrency.*`, `storage.*` namespaces.
18
19use crate::serde_json::Value as JsonValue;
20use crate::storage::UnifiedStore;
21
22#[inline]
23fn num(v: f64) -> JsonValue {
24    JsonValue::Number(v)
25}
26
27#[inline]
28fn text(s: &str) -> JsonValue {
29    JsonValue::String(s.to_string())
30}
31
32/// Default value encoded as JSON so the loader can delegate to
33/// `set_config_tree` which already handles every `Value` variant.
34#[derive(Debug, Clone)]
35pub struct ConfigDefault {
36    pub key: &'static str,
37    pub tier: Tier,
38    /// Lazily produced JSON default. A closure because `bgwriter.delay_ms`
39    /// etc. are unsigned and `serde_json::Value::from(u64)` is fine, but
40    /// we want the option of composing richer defaults later.
41    pub default: fn() -> JsonValue,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Tier {
46    /// Self-healing on boot. Always visible in `SHOW CONFIG`.
47    Critical,
48    /// In-memory default. Only visible in `SHOW CONFIG` after user writes.
49    Optional,
50}
51
52/// The full matrix. Keep sorted by namespace for readability.
53pub const MATRIX: &[ConfigDefault] = &[
54    // ask.*
55    ConfigDefault {
56        key: "ask.max_prompt_tokens",
57        tier: Tier::Optional,
58        default: || num(8192.0),
59    },
60    ConfigDefault {
61        key: "ask.max_completion_tokens",
62        tier: Tier::Optional,
63        default: || num(1024.0),
64    },
65    ConfigDefault {
66        key: "ask.max_sources_bytes",
67        tier: Tier::Optional,
68        default: || num(262_144.0),
69    },
70    ConfigDefault {
71        key: "ask.timeout_ms",
72        tier: Tier::Optional,
73        default: || num(30_000.0),
74    },
75    ConfigDefault {
76        key: "ask.daily_cost_cap_usd",
77        tier: Tier::Optional,
78        default: || text(""),
79    },
80    ConfigDefault {
81        key: "ask.audit.include_answer",
82        tier: Tier::Optional,
83        default: || JsonValue::Bool(false),
84    },
85    ConfigDefault {
86        key: "ask.audit.retention_days",
87        tier: Tier::Optional,
88        default: || num(90.0),
89    },
90    ConfigDefault {
91        key: "ask.cache.enabled",
92        tier: Tier::Optional,
93        default: || JsonValue::Bool(false),
94    },
95    ConfigDefault {
96        key: "ask.cache.default_ttl",
97        tier: Tier::Optional,
98        default: || text(""),
99    },
100    ConfigDefault {
101        key: "ask.cache.max_entries",
102        tier: Tier::Optional,
103        default: || num(1024.0),
104    },
105    // cache.blob.*
106    ConfigDefault {
107        key: "cache.blob.l1_bytes_max",
108        tier: Tier::Critical,
109        default: || num(crate::storage::cache::DEFAULT_BLOB_L1_BYTES_MAX as f64),
110    },
111    ConfigDefault {
112        key: "cache.blob.l2_bytes_max",
113        tier: Tier::Critical,
114        default: || num(crate::storage::cache::DEFAULT_BLOB_L2_BYTES_MAX as f64),
115    },
116    ConfigDefault {
117        key: "cache.blob.namespace_bytes_max",
118        tier: Tier::Critical,
119        default: || num(crate::storage::cache::DEFAULT_BLOB_NAMESPACE_BYTES_MAX as f64),
120    },
121    ConfigDefault {
122        key: "cache.blob.max_namespaces",
123        tier: Tier::Critical,
124        default: || num(crate::storage::cache::DEFAULT_BLOB_MAX_NAMESPACES as f64),
125    },
126    // Protocol-specific public addresses. Environment overlays keep each
127    // node's listeners separate from replicated application configuration.
128    ConfigDefault {
129        key: "routing.red.advertise_addr",
130        tier: Tier::Optional,
131        default: || text(""),
132    },
133    ConfigDefault {
134        key: "routing.reds.advertise_addr",
135        tier: Tier::Optional,
136        default: || text(""),
137    },
138    ConfigDefault {
139        key: "routing.grpc.advertise_addr",
140        tier: Tier::Optional,
141        default: || text(""),
142    },
143    ConfigDefault {
144        key: "routing.grpcs.advertise_addr",
145        tier: Tier::Optional,
146        default: || text(""),
147    },
148    ConfigDefault {
149        key: "routing.http.advertise_addr",
150        tier: Tier::Optional,
151        default: || text(""),
152    },
153    ConfigDefault {
154        key: "routing.https.advertise_addr",
155        tier: Tier::Optional,
156        default: || text(""),
157    },
158    ConfigDefault {
159        key: "routing.ws.advertise_addr",
160        tier: Tier::Optional,
161        default: || text(""),
162    },
163    ConfigDefault {
164        key: "routing.wss.advertise_addr",
165        tier: Tier::Optional,
166        default: || text(""),
167    },
168    // storage.binary_document_body — DOCUMENT native binary body container
169    // (PRD-1398, ADR-0063). Production cutover default: document writes store
170    // the body as the native binary container; reads decode it back to JSON
171    // transparently. (Keyed off `storage.*`, not `document.*`, because
172    // `document` is a reserved RQL keyword and would break SET CONFIG.)
173    ConfigDefault {
174        key: "storage.binary_document_body",
175        tier: Tier::Optional,
176        default: || JsonValue::Bool(true),
177    },
178    // durability.*
179    ConfigDefault {
180        key: "durability.mode",
181        tier: Tier::Critical,
182        default: || text("sync"),
183    },
184    // runtime.result_cache.*
185    // Kill-switch (issue #802). Critical so it self-heals to `true` on
186    // boot and is always visible in SHOW CONFIG — operators flip it to
187    // `false` to disable result caching wholesale for debugging.
188    ConfigDefault {
189        key: "runtime.result_cache.enabled",
190        tier: Tier::Critical,
191        default: || JsonValue::Bool(true),
192    },
193    // Per-entry freshness window in seconds (issue #802). Mirrors the
194    // former `RESULT_CACHE_TTL_SECS` constant.
195    ConfigDefault {
196        key: "runtime.result_cache.ttl_seconds",
197        tier: Tier::Optional,
198        default: || num(30.0),
199    },
200    // LRU capacity in entries (issue #802). Mirrors the former
201    // `RESULT_CACHE_MAX_ENTRIES` constant.
202    ConfigDefault {
203        key: "runtime.result_cache.capacity_entries",
204        tier: Tier::Optional,
205        default: || num(1000.0),
206    },
207    // concurrency.*
208    ConfigDefault {
209        key: "concurrency.locking.enabled",
210        tier: Tier::Critical,
211        default: || JsonValue::Bool(true),
212    },
213    ConfigDefault {
214        key: "concurrency.locking.deadlock_timeout_ms",
215        tier: Tier::Optional,
216        default: || num(5000.0),
217    },
218    // storage.wal.*
219    ConfigDefault {
220        key: "storage.wal.max_interval_ms",
221        tier: Tier::Critical,
222        default: || num(10.0),
223    },
224    ConfigDefault {
225        key: "storage.wal.min_batch_size",
226        tier: Tier::Optional,
227        default: || num(4.0),
228    },
229    // storage.deploy.* — official deploy/storage profile selection.
230    ConfigDefault {
231        key: "storage.deploy.profile",
232        tier: Tier::Critical,
233        default: || text("embedded"),
234    },
235    ConfigDefault {
236        key: "storage.deploy.packaging",
237        tier: Tier::Critical,
238        default: || text("single-file"),
239    },
240    ConfigDefault {
241        key: "storage.deploy.preset",
242        tier: Tier::Critical,
243        default: || text("embedded"),
244    },
245    ConfigDefault {
246        key: "storage.deploy.replica_count",
247        tier: Tier::Critical,
248        default: || num(0.0),
249    },
250    ConfigDefault {
251        key: "storage.deploy.managed_backup",
252        tier: Tier::Critical,
253        default: || JsonValue::Bool(false),
254    },
255    ConfigDefault {
256        key: "storage.deploy.wal_retention",
257        tier: Tier::Critical,
258        default: || JsonValue::Bool(false),
259    },
260    // storage.bgwriter.*
261    ConfigDefault {
262        key: "storage.bgwriter.delay_ms",
263        tier: Tier::Critical,
264        default: || num(200.0),
265    },
266    ConfigDefault {
267        key: "storage.bgwriter.max_pages_per_round",
268        tier: Tier::Optional,
269        default: || num(100.0),
270    },
271    ConfigDefault {
272        key: "storage.bgwriter.lru_multiplier",
273        tier: Tier::Optional,
274        default: || num(2.0),
275    },
276    // storage.bulk_insert.*
277    ConfigDefault {
278        key: "storage.bulk_insert.max_buffered_rows",
279        tier: Tier::Optional,
280        default: || num(1000.0),
281    },
282    ConfigDefault {
283        key: "storage.bulk_insert.max_buffered_bytes",
284        tier: Tier::Optional,
285        default: || num(65536.0),
286    },
287    // storage.hot_update.*
288    ConfigDefault {
289        key: "storage.hot_update.max_chain_hops",
290        tier: Tier::Optional,
291        default: || num(32.0),
292    },
293    // storage.time_series.*
294    ConfigDefault {
295        key: "storage.time_series.max_series_per_collection",
296        tier: Tier::Optional,
297        default: || num(1_000_000.0),
298    },
299    // storage.btree.*
300    ConfigDefault {
301        key: "storage.btree.lehman_yao",
302        tier: Tier::Critical,
303        default: || JsonValue::Bool(true),
304    },
305    // ai.ner.* — opt-in LLM backend for AskPipeline Stage 1 (issue #189).
306    ConfigDefault {
307        key: "ai.collection_descriptions.auto_enrich",
308        tier: Tier::Optional,
309        default: || JsonValue::Bool(false),
310    },
311    // Default backend stays heuristic so existing deployments keep
312    // their current behaviour without operator action.
313    ConfigDefault {
314        key: "ai.ner.backend",
315        tier: Tier::Optional,
316        default: || text("heuristic"),
317    },
318    ConfigDefault {
319        key: "ai.ner.endpoint",
320        tier: Tier::Optional,
321        default: || text(""),
322    },
323    ConfigDefault {
324        key: "ai.ner.model",
325        tier: Tier::Optional,
326        default: || text(""),
327    },
328    ConfigDefault {
329        key: "ai.ner.timeout_ms",
330        tier: Tier::Optional,
331        default: || num(5000.0),
332    },
333    ConfigDefault {
334        key: "ai.ner.fallback",
335        tier: Tier::Optional,
336        default: || text("use_heuristic"),
337    },
338    // runtime.ai.transport.* — shared outbound AI HTTP client foundation
339    // (issue #274). Provider rewiring can opt into these defaults
340    // incrementally.
341    ConfigDefault {
342        key: "runtime.ai.transport_pool_size",
343        tier: Tier::Optional,
344        default: || num(16.0),
345    },
346    ConfigDefault {
347        key: "runtime.ai.transport_timeout_ms",
348        tier: Tier::Optional,
349        default: || num(30000.0),
350    },
351    ConfigDefault {
352        key: "runtime.ai.transport_retry_max_attempts",
353        tier: Tier::Optional,
354        default: || num(3.0),
355    },
356    ConfigDefault {
357        key: "runtime.ai.transport_retry_base_ms",
358        tier: Tier::Optional,
359        default: || num(500.0),
360    },
361    // cache.blob.policy.* — extended TTL hot-path opt-in (issue #189).
362    ConfigDefault {
363        key: "cache.blob.policy.extended",
364        tier: Tier::Optional,
365        default: || text("off"),
366    },
367    // cache.blob.async_promotion — async L2->L1 promotion pool opt-in
368    // (issue #193). When "on", L2 hits return bytes to the caller
369    // immediately and the L1 install runs on a background worker.
370    // Default "off" for safe rollout — legacy synchronous promotion path.
371    ConfigDefault {
372        key: "cache.blob.async_promotion",
373        tier: Tier::Optional,
374        default: || text("off"),
375    },
376];
377
378/// Fetch the JSON default for a matrix key. Returns `None` when the
379/// key is not in the matrix (callers should treat that as a
380/// programming error — unknown key, unknown tier, unknown semantics).
381pub fn default_for(key: &str) -> Option<JsonValue> {
382    MATRIX
383        .iter()
384        .find(|entry| entry.key == key)
385        .map(|entry| (entry.default)())
386}
387
388/// Tier lookup — useful for tests and for introspection commands
389/// that want to report whether a key is expected to self-heal.
390pub fn tier_for(key: &str) -> Option<Tier> {
391    MATRIX
392        .iter()
393        .find(|entry| entry.key == key)
394        .map(|entry| entry.tier)
395}
396
397/// Boot-time self-healing pass: for every `Tier::Critical` key, if
398/// `red.config` does not already contain the key, write the default
399/// in. Idempotent — re-running produces no writes.
400///
401/// `Tier::Optional` keys are never touched here; they stay
402/// transparent-default until a user `SET CONFIG` elevates them.
403pub fn heal_critical_keys(store: &UnifiedStore) {
404    // `set_config_tree` dot-splits the key and stores one row per
405    // leaf, so we handle each matrix entry individually.
406    for entry in MATRIX {
407        if entry.tier != Tier::Critical {
408            continue;
409        }
410        if is_key_present(store, entry.key) {
411            continue;
412        }
413        store.set_config_tree(entry.key, &(entry.default)());
414    }
415}
416
417/// Lightweight presence probe. Avoids loading the whole red.config
418/// collection; scans until the first hit.
419fn is_key_present(store: &UnifiedStore, key: &str) -> bool {
420    match store.try_get_config(key) {
421        Ok(value) => value.is_some(),
422        Err(error) => {
423            // A failed physical read is not a missing operator setting. Never
424            // overwrite it with a default or a mounted-file value.
425            tracing::warn!(key, %error, "cannot read persisted configuration; skipping seed");
426            true
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn every_matrix_entry_has_a_default_that_resolves() {
437        for entry in MATRIX {
438            let value = (entry.default)();
439            assert!(
440                !matches!(value, JsonValue::Null),
441                "matrix key {} has a null default, defeats self-heal",
442                entry.key
443            );
444        }
445    }
446
447    #[test]
448    fn critical_keys_cover_the_core_guarantees() {
449        // This list is a tripwire — if someone drops one of these
450        // from Tier A without updating callers, the test catches it.
451        let required_critical = [
452            "cache.blob.l1_bytes_max",
453            "cache.blob.l2_bytes_max",
454            "cache.blob.namespace_bytes_max",
455            "cache.blob.max_namespaces",
456            "durability.mode",
457            "concurrency.locking.enabled",
458            "storage.wal.max_interval_ms",
459            "storage.deploy.profile",
460            "storage.deploy.packaging",
461            "storage.deploy.preset",
462            "storage.deploy.replica_count",
463            "storage.deploy.managed_backup",
464            "storage.deploy.wal_retention",
465            "storage.bgwriter.delay_ms",
466            "storage.btree.lehman_yao",
467        ];
468        for key in required_critical {
469            assert_eq!(
470                tier_for(key),
471                Some(Tier::Critical),
472                "{key} must be a Tier A (Critical) key",
473            );
474        }
475    }
476
477    #[test]
478    fn optional_keys_are_not_self_healed() {
479        let must_be_optional = [
480            "concurrency.locking.deadlock_timeout_ms",
481            "storage.wal.min_batch_size",
482            "storage.bgwriter.max_pages_per_round",
483            "storage.bgwriter.lru_multiplier",
484            "storage.bulk_insert.max_buffered_rows",
485            "storage.bulk_insert.max_buffered_bytes",
486            "storage.hot_update.max_chain_hops",
487            "storage.time_series.max_series_per_collection",
488        ];
489        for key in must_be_optional {
490            assert_eq!(tier_for(key), Some(Tier::Optional), "{key} tier mismatch");
491        }
492    }
493
494    #[test]
495    fn unknown_key_returns_none() {
496        assert!(default_for("nonexistent.key").is_none());
497        assert!(tier_for("nonexistent.key").is_none());
498    }
499}