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.max_namespaces",
118        tier: Tier::Critical,
119        default: || num(crate::storage::cache::DEFAULT_BLOB_MAX_NAMESPACES as f64),
120    },
121    // storage.binary_document_body — DOCUMENT native binary body container
122    // (PRD-1398, ADR-0063). Opt-in: when true, document writes store the body
123    // as the native binary container; reads decode it back to JSON
124    // transparently. Default false so existing deployments keep the plain-JSON
125    // body until they choose to flip. (Keyed off `storage.*`, not `document.*`,
126    // because `document` is a reserved RQL keyword and would break SET CONFIG.)
127    ConfigDefault {
128        key: "storage.binary_document_body",
129        tier: Tier::Optional,
130        default: || JsonValue::Bool(false),
131    },
132    // durability.*
133    ConfigDefault {
134        key: "durability.mode",
135        tier: Tier::Critical,
136        default: || text("sync"),
137    },
138    // runtime.result_cache.*
139    ConfigDefault {
140        key: "runtime.result_cache.backend",
141        tier: Tier::Critical,
142        default: || text("legacy"),
143    },
144    // Kill-switch (issue #802). Critical so it self-heals to `true` on
145    // boot and is always visible in SHOW CONFIG — operators flip it to
146    // `false` to disable result caching wholesale for debugging.
147    ConfigDefault {
148        key: "runtime.result_cache.enabled",
149        tier: Tier::Critical,
150        default: || JsonValue::Bool(true),
151    },
152    // Per-entry freshness window in seconds (issue #802). Mirrors the
153    // former `RESULT_CACHE_TTL_SECS` constant.
154    ConfigDefault {
155        key: "runtime.result_cache.ttl_seconds",
156        tier: Tier::Optional,
157        default: || num(30.0),
158    },
159    // LRU capacity in entries (issue #802). Mirrors the former
160    // `RESULT_CACHE_MAX_ENTRIES` constant.
161    ConfigDefault {
162        key: "runtime.result_cache.capacity_entries",
163        tier: Tier::Optional,
164        default: || num(1000.0),
165    },
166    // concurrency.*
167    ConfigDefault {
168        key: "concurrency.locking.enabled",
169        tier: Tier::Critical,
170        default: || JsonValue::Bool(true),
171    },
172    ConfigDefault {
173        key: "concurrency.locking.deadlock_timeout_ms",
174        tier: Tier::Optional,
175        default: || num(5000.0),
176    },
177    // storage.wal.*
178    ConfigDefault {
179        key: "storage.wal.max_interval_ms",
180        tier: Tier::Critical,
181        default: || num(10.0),
182    },
183    ConfigDefault {
184        key: "storage.wal.min_batch_size",
185        tier: Tier::Optional,
186        default: || num(4.0),
187    },
188    // storage.deploy.* — official deploy/storage profile selection.
189    ConfigDefault {
190        key: "storage.deploy.profile",
191        tier: Tier::Critical,
192        default: || text("embedded"),
193    },
194    ConfigDefault {
195        key: "storage.deploy.packaging",
196        tier: Tier::Critical,
197        default: || text("single-file"),
198    },
199    ConfigDefault {
200        key: "storage.deploy.preset",
201        tier: Tier::Critical,
202        default: || text("embedded"),
203    },
204    ConfigDefault {
205        key: "storage.deploy.replica_count",
206        tier: Tier::Critical,
207        default: || num(0.0),
208    },
209    ConfigDefault {
210        key: "storage.deploy.managed_backup",
211        tier: Tier::Critical,
212        default: || JsonValue::Bool(false),
213    },
214    ConfigDefault {
215        key: "storage.deploy.wal_retention",
216        tier: Tier::Critical,
217        default: || JsonValue::Bool(false),
218    },
219    // storage.bgwriter.*
220    ConfigDefault {
221        key: "storage.bgwriter.delay_ms",
222        tier: Tier::Critical,
223        default: || num(200.0),
224    },
225    ConfigDefault {
226        key: "storage.bgwriter.max_pages_per_round",
227        tier: Tier::Optional,
228        default: || num(100.0),
229    },
230    ConfigDefault {
231        key: "storage.bgwriter.lru_multiplier",
232        tier: Tier::Optional,
233        default: || num(2.0),
234    },
235    // storage.bulk_insert.*
236    ConfigDefault {
237        key: "storage.bulk_insert.max_buffered_rows",
238        tier: Tier::Optional,
239        default: || num(1000.0),
240    },
241    ConfigDefault {
242        key: "storage.bulk_insert.max_buffered_bytes",
243        tier: Tier::Optional,
244        default: || num(65536.0),
245    },
246    // storage.hot_update.*
247    ConfigDefault {
248        key: "storage.hot_update.max_chain_hops",
249        tier: Tier::Optional,
250        default: || num(32.0),
251    },
252    // storage.btree.*
253    ConfigDefault {
254        key: "storage.btree.lehman_yao",
255        tier: Tier::Critical,
256        default: || JsonValue::Bool(true),
257    },
258    // ai.ner.* — opt-in LLM backend for AskPipeline Stage 1 (issue #189).
259    // Default backend stays heuristic so existing deployments keep
260    // their current behaviour without operator action.
261    ConfigDefault {
262        key: "ai.ner.backend",
263        tier: Tier::Optional,
264        default: || text("heuristic"),
265    },
266    ConfigDefault {
267        key: "ai.ner.endpoint",
268        tier: Tier::Optional,
269        default: || text(""),
270    },
271    ConfigDefault {
272        key: "ai.ner.model",
273        tier: Tier::Optional,
274        default: || text(""),
275    },
276    ConfigDefault {
277        key: "ai.ner.timeout_ms",
278        tier: Tier::Optional,
279        default: || num(5000.0),
280    },
281    ConfigDefault {
282        key: "ai.ner.fallback",
283        tier: Tier::Optional,
284        default: || text("use_heuristic"),
285    },
286    // runtime.ai.transport.* — shared outbound AI HTTP client foundation
287    // (issue #274). Provider rewiring can opt into these defaults
288    // incrementally.
289    ConfigDefault {
290        key: "runtime.ai.transport_pool_size",
291        tier: Tier::Optional,
292        default: || num(16.0),
293    },
294    ConfigDefault {
295        key: "runtime.ai.transport_timeout_ms",
296        tier: Tier::Optional,
297        default: || num(30000.0),
298    },
299    ConfigDefault {
300        key: "runtime.ai.transport_retry_max_attempts",
301        tier: Tier::Optional,
302        default: || num(3.0),
303    },
304    ConfigDefault {
305        key: "runtime.ai.transport_retry_base_ms",
306        tier: Tier::Optional,
307        default: || num(500.0),
308    },
309    // cache.blob.policy.* — extended TTL hot-path opt-in (issue #189).
310    ConfigDefault {
311        key: "cache.blob.policy.extended",
312        tier: Tier::Optional,
313        default: || text("off"),
314    },
315    // cache.blob.async_promotion — async L2->L1 promotion pool opt-in
316    // (issue #193). When "on", L2 hits return bytes to the caller
317    // immediately and the L1 install runs on a background worker.
318    // Default "off" for safe rollout — legacy synchronous promotion path.
319    ConfigDefault {
320        key: "cache.blob.async_promotion",
321        tier: Tier::Optional,
322        default: || text("off"),
323    },
324];
325
326/// Fetch the JSON default for a matrix key. Returns `None` when the
327/// key is not in the matrix (callers should treat that as a
328/// programming error — unknown key, unknown tier, unknown semantics).
329pub fn default_for(key: &str) -> Option<JsonValue> {
330    MATRIX
331        .iter()
332        .find(|entry| entry.key == key)
333        .map(|entry| (entry.default)())
334}
335
336/// Tier lookup — useful for tests and for introspection commands
337/// that want to report whether a key is expected to self-heal.
338pub fn tier_for(key: &str) -> Option<Tier> {
339    MATRIX
340        .iter()
341        .find(|entry| entry.key == key)
342        .map(|entry| entry.tier)
343}
344
345/// Boot-time self-healing pass: for every `Tier::Critical` key, if
346/// `red_config` does not already contain the key, write the default
347/// in. Idempotent — re-running produces no writes.
348///
349/// `Tier::Optional` keys are never touched here; they stay
350/// transparent-default until a user `SET CONFIG` elevates them.
351pub fn heal_critical_keys(store: &UnifiedStore) {
352    // `set_config_tree` dot-splits the key and stores one row per
353    // leaf, so we handle each matrix entry individually.
354    for entry in MATRIX {
355        if entry.tier != Tier::Critical {
356            continue;
357        }
358        if is_key_present(store, entry.key) {
359            continue;
360        }
361        store.set_config_tree(entry.key, &(entry.default)());
362    }
363}
364
365/// Lightweight presence probe. Avoids loading the whole red_config
366/// collection; scans until the first hit.
367fn is_key_present(store: &UnifiedStore, key: &str) -> bool {
368    let Some(manager) = store.get_collection("red_config") else {
369        return false;
370    };
371    let mut found = false;
372    manager.for_each_entity(|entity| {
373        if let Some(row) = entity.data.as_row() {
374            let entry_key = row.get_field("key").and_then(|v| match v {
375                crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
376                _ => None,
377            });
378            if entry_key == Some(key) {
379                found = true;
380                return false; // short-circuit
381            }
382        }
383        true
384    });
385    found
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn every_matrix_entry_has_a_default_that_resolves() {
394        for entry in MATRIX {
395            let value = (entry.default)();
396            assert!(
397                !matches!(value, JsonValue::Null),
398                "matrix key {} has a null default, defeats self-heal",
399                entry.key
400            );
401        }
402    }
403
404    #[test]
405    fn critical_keys_cover_the_core_guarantees() {
406        // This list is a tripwire — if someone drops one of these
407        // from Tier A without updating callers, the test catches it.
408        let required_critical = [
409            "cache.blob.l1_bytes_max",
410            "cache.blob.l2_bytes_max",
411            "cache.blob.max_namespaces",
412            "durability.mode",
413            "runtime.result_cache.backend",
414            "concurrency.locking.enabled",
415            "storage.wal.max_interval_ms",
416            "storage.deploy.profile",
417            "storage.deploy.packaging",
418            "storage.deploy.preset",
419            "storage.deploy.replica_count",
420            "storage.deploy.managed_backup",
421            "storage.deploy.wal_retention",
422            "storage.bgwriter.delay_ms",
423            "storage.btree.lehman_yao",
424        ];
425        for key in required_critical {
426            assert_eq!(
427                tier_for(key),
428                Some(Tier::Critical),
429                "{key} must be a Tier A (Critical) key",
430            );
431        }
432    }
433
434    #[test]
435    fn optional_keys_are_not_self_healed() {
436        let must_be_optional = [
437            "concurrency.locking.deadlock_timeout_ms",
438            "storage.wal.min_batch_size",
439            "storage.bgwriter.max_pages_per_round",
440            "storage.bgwriter.lru_multiplier",
441            "storage.bulk_insert.max_buffered_rows",
442            "storage.bulk_insert.max_buffered_bytes",
443            "storage.hot_update.max_chain_hops",
444        ];
445        for key in must_be_optional {
446            assert_eq!(tier_for(key), Some(Tier::Optional), "{key} tier mismatch");
447        }
448    }
449
450    #[test]
451    fn unknown_key_returns_none() {
452        assert!(default_for("nonexistent.key").is_none());
453        assert!(tier_for("nonexistent.key").is_none());
454    }
455}