Skip to main content

reddb_server/runtime/
impl_lifecycle.rs

1//! Runtime bootstrap / constructor / handle accessors.
2//!
3//! Extracted verbatim from `impl_core.rs` (impl_core slice 8/10, issue #1629).
4//! Houses the in-memory / options constructors, the ~800-line `with_pool`
5//! constructor (moved verbatim — restructuring it is future work), snapshot
6//! rehydration and materialized-view backing lifecycle, system keyed-collection
7//! bootstrap, and the db / index-store / schema-vocabulary / auth-store /
8//! oauth / browser-token / registry handle accessors.
9//!
10//! - **Free helpers** — `view_records_to_entities`,
11//!   `system_keyed_collection_contract`, `table_row_index_fields`.
12use super::impl_config_secret::seed_storage_deploy_config;
13use super::*;
14
15/// Convert the rows produced by a materialized-view body into
16/// `UnifiedEntity` table rows targeting the backing collection.
17/// Issue #595 slice 9c — feeds `UnifiedStore::refresh_collection`.
18///
19/// Graph fragments and vector hits are ignored: a materialized view
20/// is a relational result set (SELECT-shaped); slices 11+ may extend
21/// this once we have a richer view body shape. Each row materialises
22/// the union of its schema-bound columns + overflow.
23pub(crate) fn view_records_to_entities(
24    table: &str,
25    records: &[crate::storage::query::unified::UnifiedRecord],
26) -> Vec<crate::storage::UnifiedEntity> {
27    use std::collections::HashMap;
28    let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
29    let mut out = Vec::with_capacity(records.len());
30    for record in records {
31        let mut named: HashMap<String, crate::storage::schema::Value> = HashMap::new();
32        for (name, value) in record.iter_fields() {
33            named.insert(name.to_string(), value.clone());
34        }
35        let entity = crate::storage::UnifiedEntity::new(
36            crate::storage::EntityId::new(0),
37            crate::storage::EntityKind::TableRow {
38                table: std::sync::Arc::clone(&table_arc),
39                row_id: 0,
40            },
41            crate::storage::EntityData::Row(crate::storage::RowData {
42                columns: Vec::new(),
43                named: Some(named),
44                schema: None,
45            }),
46        );
47        out.push(entity);
48    }
49    out
50}
51
52fn system_keyed_collection_contract(
53    name: &str,
54    model: crate::catalog::CollectionModel,
55) -> crate::physical::CollectionContract {
56    let now = crate::utils::now_unix_millis() as u128;
57    crate::physical::CollectionContract {
58        name: name.to_string(),
59        declared_model: model,
60        schema_mode: crate::catalog::SchemaMode::Dynamic,
61        origin: crate::physical::ContractOrigin::Implicit,
62        version: 1,
63        created_at_unix_ms: now,
64        updated_at_unix_ms: now,
65        default_ttl_ms: None,
66        vector_dimension: None,
67        vector_metric: None,
68        context_index_fields: Vec::new(),
69        declared_columns: Vec::new(),
70        table_def: None,
71        timestamps_enabled: false,
72        context_index_enabled: false,
73        metrics_raw_retention_ms: None,
74        metrics_rollup_policies: Vec::new(),
75        metrics_tenant_identity: None,
76        metrics_namespace: None,
77        append_only: false,
78        subscriptions: Vec::new(),
79        analytics_config: Vec::new(),
80        session_key: None,
81        session_gap_ms: None,
82        retention_duration_ms: None,
83        analytical_storage: None,
84
85        ai_policy: None,
86    }
87}
88
89pub(crate) fn table_row_index_fields(
90    entity: &crate::storage::unified::entity::UnifiedEntity,
91) -> Vec<(String, crate::storage::schema::Value)> {
92    let crate::storage::EntityData::Row(row) = &entity.data else {
93        return Vec::new();
94    };
95    if let Some(named) = &row.named {
96        return named
97            .iter()
98            .map(|(name, value)| (name.clone(), value.clone()))
99            .collect();
100    }
101    if let Some(schema) = &row.schema {
102        return schema
103            .iter()
104            .zip(row.columns.iter())
105            .map(|(name, value)| (name.clone(), value.clone()))
106            .collect();
107    }
108    Vec::new()
109}
110
111impl RedDBRuntime {
112    pub fn in_memory() -> RedDBResult<Self> {
113        Self::with_options(RedDBOptions::in_memory())
114    }
115
116    pub fn flush(&self) -> RedDBResult<()> {
117        self.inner
118            .db
119            .flush()
120            .map_err(|err| RedDBError::Internal(err.to_string()))
121    }
122
123    /// Handle to the intent-lock manager for tests + introspection.
124    /// Production code acquires via `LockerGuard::new(rt.lock_manager())`
125    /// rather than touching the manager directly.
126    pub fn lock_manager(&self) -> std::sync::Arc<crate::runtime::lock_manager::LockManager> {
127        self.inner.lock_manager.clone()
128    }
129
130    /// Process-local governance registry for managed policy/config guardrails.
131    pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
132        self.inner.config_registry.clone()
133    }
134
135    pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
136        self.inner.query_audit.clone()
137    }
138
139    pub fn control_events_require_persistence(&self) -> bool {
140        self.inner.control_event_config.require_persistence()
141    }
142
143    pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
144        self.inner.control_event_config
145    }
146
147    pub fn control_event_ledger(
148        &self,
149    ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
150        self.inner.control_event_ledger.read().clone()
151    }
152
153    #[doc(hidden)]
154    pub fn replace_control_event_ledger_for_tests(
155        &self,
156        ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
157    ) {
158        *self.inner.control_event_ledger.write() = ledger;
159    }
160
161    #[inline(never)]
162    pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
163        Self::with_pool(options, ConnectionPoolConfig::default())
164    }
165
166    pub fn with_pool(
167        options: RedDBOptions,
168        pool_config: ConnectionPoolConfig,
169    ) -> RedDBResult<Self> {
170        // PLAN.md Phase 9.1 — capture wall-clock before storage
171        // open so the cold-start phase markers can be backfilled
172        // once Lifecycle is constructed below. Storage open
173        // encapsulates auto-restore + WAL replay; we treat the
174        // whole window as one combined "restore" + "wal_replay"
175        // phase split at the same boundary because the storage
176        // layer doesn't yet emit a finer signal.
177        let boot_open_start_ms = std::time::SystemTime::now()
178            .duration_since(std::time::UNIX_EPOCH)
179            .map(|d| d.as_millis() as u64)
180            .unwrap_or(0);
181        let embedded_single_file = options.storage_profile.deploy_profile
182            == crate::storage::DeployProfile::Embedded
183            && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
184        let db = Arc::new(
185            RedDB::open_with_options(&options)
186                .map_err(|err| RedDBError::Internal(err.to_string()))?,
187        );
188        let result_blob_cache_config = if embedded_single_file {
189            crate::storage::cache::BlobCacheConfig::default()
190        } else {
191            crate::storage::cache::BlobCacheConfig::default().with_l2_path(
192                reddb_file::layout::result_cache_l2_path(
193                    &options.resolved_path(reddb_file::default_database_path()),
194                ),
195            )
196        };
197        let result_blob_cache =
198            crate::storage::cache::BlobCache::open_with_l2(result_blob_cache_config).map_err(
199                |err| RedDBError::Internal(format!("open result Blob Cache L2 failed: {err:?}")),
200            )?;
201        let storage_ready_ms = std::time::SystemTime::now()
202            .duration_since(std::time::UNIX_EPOCH)
203            .map(|d| d.as_millis() as u64)
204            .unwrap_or(0);
205
206        let runtime = Self {
207            inner: Arc::new(RuntimeInner {
208                db: db.clone(),
209                layout: PhysicalLayout::from_options(&options),
210                embedded_single_file,
211                indices: IndexCatalog::register_default_vector_graph(
212                    options.has_capability(crate::api::Capability::Table),
213                    options.has_capability(crate::api::Capability::Graph),
214                ),
215                pool_config,
216                pool: Mutex::new(PoolState::default()),
217                started_at_unix_ms: SystemTime::now()
218                    .duration_since(UNIX_EPOCH)
219                    .unwrap_or_default()
220                    .as_millis(),
221                probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
222                index_store: super::index_store::IndexStore::new(),
223                cdc: crate::replication::cdc::CdcBuffer::new(100_000),
224                backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
225                query_cache: parking_lot::RwLock::new(
226                    crate::storage::query::planner::cache::PlanCache::new(1000),
227                ),
228                result_cache: parking_lot::RwLock::new((
229                    HashMap::new(),
230                    std::collections::VecDeque::new(),
231                )),
232                result_blob_cache,
233                result_blob_entries: parking_lot::RwLock::new((
234                    HashMap::new(),
235                    std::collections::VecDeque::new(),
236                )),
237                ask_answer_cache_entries: parking_lot::RwLock::new((
238                    HashSet::new(),
239                    std::collections::VecDeque::new(),
240                )),
241                result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
242                result_cache_hits: std::sync::atomic::AtomicU64::new(0),
243                result_cache_misses: std::sync::atomic::AtomicU64::new(0),
244                result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
245                ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
246                queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
247                rmw_locks: RmwLockTable::new(),
248                planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
249                ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
250                config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
251                ec_worker: crate::ec::worker::EcWorker::new(),
252                auth_store: parking_lot::RwLock::new(None),
253                oauth_validator: parking_lot::RwLock::new(None),
254                browser_token_authority: parking_lot::RwLock::new(None),
255                views: parking_lot::RwLock::new(HashMap::new()),
256                materialized_views: parking_lot::RwLock::new(
257                    crate::storage::cache::result::MaterializedViewCache::new(),
258                ),
259                retention_sweeper: parking_lot::RwLock::new(
260                    crate::runtime::retention_sweeper::RetentionSweeperState::new(),
261                ),
262                snapshot_manager: Arc::new(
263                    crate::storage::transaction::snapshot::SnapshotManager::new(),
264                ),
265                tx_contexts: parking_lot::RwLock::new(HashMap::new()),
266                tx_local_tenants: parking_lot::RwLock::new(HashMap::new()),
267                env_config_overrides: crate::runtime::config_overlay::collect_env_overrides(),
268                lock_manager: Arc::new({
269                    // Sourced from the matrix: Tier B key
270                    // `concurrency.locking.deadlock_timeout_ms`
271                    // (default 5000). Env var wins at boot so
272                    // operators can tune without touching red_config.
273                    let env = crate::runtime::config_overlay::collect_env_overrides();
274                    let timeout_ms = env
275                        .get("concurrency.locking.deadlock_timeout_ms")
276                        .and_then(|raw| raw.parse::<u64>().ok())
277                        .unwrap_or_else(|| {
278                            match crate::runtime::config_matrix::default_for(
279                                "concurrency.locking.deadlock_timeout_ms",
280                            ) {
281                                Some(crate::serde_json::Value::Number(n)) => n as u64,
282                                _ => 5000,
283                            }
284                        });
285                    let cfg = crate::runtime::lock_manager::LockConfig {
286                        default_timeout: std::time::Duration::from_millis(timeout_ms),
287                        ..Default::default()
288                    };
289                    crate::runtime::lock_manager::LockManager::new(cfg)
290                }),
291                rls_policies: parking_lot::RwLock::new(HashMap::new()),
292                rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
293                foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
294                pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
295                pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
296                pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
297                pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
298                pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
299                queue_wait_registry: std::sync::Arc::new(
300                    crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
301                ),
302                pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
303                tenant_tables: parking_lot::RwLock::new(HashMap::new()),
304                ddl_epoch: std::sync::atomic::AtomicU64::new(0),
305                write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
306                    &options,
307                )),
308                lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
309                resource_limits: crate::runtime::resource_limits::ResourceLimits::from_env(),
310                audit_log: {
311                    // Default audit-log path for the in-memory case
312                    // sits in the system temp dir; persistent runs
313                    // place it next to the resolved data file.
314                    //
315                    // gh-471 iter 2: route through the resolved
316                    // `LogDestination`. Performance/Max tiers emit a
317                    // file-backed log destination under the file-owned
318                    // support-directory logs tier;
319                    // lower tiers / ephemeral runs report `Stderr`
320                    // and we keep the legacy file-next-to-data sink.
321                    // #1375 — single-file embedded mode keeps the data
322                    // directory to exactly the `.rdb` artifact, so the audit
323                    // log must NOT land as a sibling. Route it to a
324                    // process-unique temp location even when a data path is
325                    // set; only the non-embedded case uses the data dir.
326                    let data_path = if embedded_single_file {
327                        std::env::temp_dir()
328                            .join("reddb-embedded-runtime")
329                            .join(format!("audit-{}", std::process::id()))
330                    } else {
331                        options
332                            .data_path
333                            .clone()
334                            .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
335                    };
336                    let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
337                    if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
338                        && (embedded_single_file
339                            || options
340                                .metadata
341                                .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
342                    {
343                        // The Stderr/Syslog lower-tier sink resolves to a
344                        // `for_data_path` sibling that collides across concurrent
345                        // temp-dir runtimes — nextest's process-per-test model
346                        // truncates one shared file, flaking audit assertions.
347                        // Pin a unique sibling for these short-lived ephemeral /
348                        // single-file embedded runtimes. The file-owned support-
349                        // dir tier (`File`) is already per-data unique, so leave
350                        // it to `for_destination` (#1375: the embedded audit then
351                        // still never lands a sibling next to the `.rdb`).
352                        let audit_path = reddb_file::layout::sibling_path(
353                            &data_path,
354                            &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
355                        );
356                        Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
357                            audit_path,
358                        ))
359                    } else {
360                        Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
361                            &audit_dest,
362                            &data_path,
363                        ))
364                    }
365                },
366                control_event_ledger: parking_lot::RwLock::new(Arc::new(
367                    crate::runtime::control_events::RuntimeLedger::new(db.store()),
368                )),
369                control_event_config: options.control_events,
370                query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
371                    db.store(),
372                    options.query_audit.clone(),
373                )),
374                lease_lifecycle: std::sync::OnceLock::new(),
375                replica_apply_metrics: std::sync::Arc::new(
376                    crate::replication::logical::ReplicaApplyMetrics::default(),
377                ),
378                replica_link_metrics: std::sync::Arc::new(
379                    crate::replication::reconnect::ReplicaLinkMetrics::default(),
380                ),
381                quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
382                schema_vocabulary: parking_lot::RwLock::new(
383                    crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
384                ),
385                slow_query_logger: {
386                    // Issue #205 — slow-query sink lives in the same
387                    // directory the audit log uses, so backup/restore
388                    // ships them together. Threshold + sample-pct
389                    // default conservatively (1 s, 100% sampling) so
390                    // emitted lines are rare and complete. Operators
391                    // tune via env / config matrix in a follow-up.
392                    //
393                    // gh-471 iter 2: same routing as the audit log —
394                    // `LogDestination::File(...)` for Performance/Max
395                    // lands under the file-owned support-directory logs tier;
396                    // lower tiers fall back to `red-slow.log` in the
397                    // data directory.
398                    // #1375 — see the audit-log note above: single-file mode
399                    // never writes the slow-query log as a sibling of the
400                    // `.rdb`. Route to a process-unique temp dir when embedded,
401                    // regardless of the data path.
402                    let fallback_dir = if embedded_single_file {
403                        std::env::temp_dir()
404                            .join("reddb-embedded-runtime")
405                            .join(format!("slow-{}", std::process::id()))
406                    } else {
407                        options
408                            .data_path
409                            .as_ref()
410                            .and_then(|p| p.parent().map(std::path::PathBuf::from))
411                            .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
412                    };
413                    let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
414                        .ok()
415                        .and_then(|s| s.parse::<u64>().ok())
416                        .unwrap_or(1000);
417                    let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
418                        .ok()
419                        .and_then(|s| s.parse::<u8>().ok())
420                        .unwrap_or(100);
421                    let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
422                    crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
423                        &slow_dest,
424                        &fallback_dir,
425                        threshold_ms,
426                        sample_pct,
427                    )
428                },
429                slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
430                    crate::telemetry::slow_query_store::DEFAULT_CAP,
431                ),
432                kv_stats: crate::runtime::KvStatsCounters::default(),
433                metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
434                metrics_tenant_activity_stats:
435                    crate::runtime::MetricsTenantActivityCounters::default(),
436                claim_telemetry: Arc::new(
437                    crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
438                ),
439                queue_telemetry: Arc::new(
440                    crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
441                ),
442                query_latency_telemetry: Arc::new(
443                    crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
444                ),
445                occupancy_sampler: Arc::new(
446                    crate::runtime::occupancy_sampler::OccupancySampler::new(),
447                ),
448                node_load_telemetry: Arc::new(
449                    crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
450                ),
451                queue_presence: Arc::new(
452                    crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
453                ),
454                vector_introspection: Arc::new(
455                    crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
456                ),
457                kv_tag_index: crate::runtime::KvTagIndex::default(),
458                chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
459                chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
460                integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
461                integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
462            }),
463        };
464
465        // Issue #205 — install the process-wide OperatorEvent sink so
466        // emit sites buried in storage / replication / signal handlers
467        // can record without threading an `&AuditLogger` through every
468        // call stack. First registration wins; subsequent in-memory
469        // runtimes (test harnesses) fall through to tracing+eprintln.
470        crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
471            &runtime.inner.audit_log,
472        ));
473
474        // Issue #1238 — wire the slow-query telemetry substrate (ADR 0060).
475        // The logger dual-writes: file sink (existing) + ring store (new).
476        runtime
477            .inner
478            .slow_query_logger
479            .attach_store(Arc::clone(&runtime.inner.slow_query_store));
480
481        // PLAN.md Phase 9.1 — backfill cold-start phase markers
482        // from the wall-clock captured before storage open. The
483        // entire `RedDB::open_with_options` call covers both
484        // auto-restore (when configured) and WAL replay. We
485        // record both phases against the same boundary today;
486        // a follow-up will split them once the storage layer
487        // surfaces a finer-grained event.
488        runtime
489            .inner
490            .lifecycle
491            .set_restore_started_at_ms(boot_open_start_ms);
492        runtime
493            .inner
494            .lifecycle
495            .set_restore_ready_at_ms(storage_ready_ms);
496        runtime
497            .inner
498            .lifecycle
499            .set_wal_replay_started_at_ms(boot_open_start_ms);
500        runtime
501            .inner
502            .lifecycle
503            .set_wal_replay_ready_at_ms(storage_ready_ms);
504
505        let restored_cdc_lsn = runtime
506            .inner
507            .db
508            .replication
509            .as_ref()
510            .map(|repl| {
511                repl.logical_wal_spool
512                    .as_ref()
513                    .map(|spool| spool.current_lsn())
514                    .unwrap_or(0)
515            })
516            .unwrap_or(0)
517            .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
518        runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
519        runtime.rehydrate_snapshot_xid_floor();
520        runtime
521            .bootstrap_system_keyed_collections()
522            .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
523        runtime.rehydrate_declared_column_schemas();
524        runtime.rehydrate_runtime_index_registry()?;
525        runtime
526            .load_probabilistic_state()
527            .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
528
529        // Phase 2.5.4: replay `tenant_tables.{table}.column` markers so
530        // tables declared via `TENANT BY (col)` survive restart. Each
531        // entry re-registers the auto-policy and flips RLS on again.
532        runtime.rehydrate_tenant_tables();
533        // Issue #593 slice 9a — replay persisted materialized-view
534        // descriptors so `CREATE MATERIALIZED VIEW v AS …` survives a
535        // restart. Runs after the system-keyed collections bootstrap
536        // and before the API opens.
537        runtime.rehydrate_materialized_view_descriptors();
538        if let Some(repl) = &runtime.inner.db.replication {
539            repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
540        }
541
542        // Save system info to red_config on boot
543        {
544            let sys = SystemInfo::collect();
545            runtime.inner.db.store().set_config_tree(
546                "red.system",
547                &crate::serde_json::json!({
548                    "pid": sys.pid,
549                    "cpu_cores": sys.cpu_cores,
550                    "total_memory_bytes": sys.total_memory_bytes,
551                    "available_memory_bytes": sys.available_memory_bytes,
552                    "os": sys.os,
553                    "arch": sys.arch,
554                    "hostname": sys.hostname,
555                    "started_at": SystemTime::now()
556                        .duration_since(UNIX_EPOCH)
557                        .unwrap_or_default()
558                        .as_millis() as u64
559                }),
560            );
561
562            // Seed defaults on first boot (only if red_config is empty or missing defaults)
563            let store = runtime.inner.db.store();
564            if store
565                .get_collection("red_config")
566                .map(|m| m.query_all(|_| true).len())
567                .unwrap_or(0)
568                <= 10
569            {
570                store.set_config_tree("red.ai", &crate::json!({
571                    "default": crate::json!({
572                        "provider": "openai",
573                        "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
574                    }),
575                    "max_embedding_inputs": 256,
576                    "max_prompt_batch": 256,
577                    "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
578                }));
579                store.set_config_tree(
580                    "red.server",
581                    &crate::json!({
582                        "max_scan_limit": 1000,
583                        "max_body_size": 1048576,
584                        "read_timeout_ms": 5000,
585                        "write_timeout_ms": 5000
586                    }),
587                );
588                store.set_config_tree(
589                    "red.storage",
590                    &crate::json!({
591                        "page_size": 4096,
592                        "page_cache_capacity": 100000,
593                        "auto_checkpoint_pages": 1000,
594                        "snapshot_retention": 16,
595                        "verify_checksums": true,
596                        "segment": crate::json!({
597                            "max_entities": 100000,
598                            "max_bytes": 268435456_u64,
599                            "compression_level": 6
600                        }),
601                        "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
602                        "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
603                        "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
604                    }),
605                );
606                store.set_config_tree(
607                    "red.search",
608                    &crate::json!({
609                        "rag": crate::json!({
610                            "max_chunks_per_source": 10,
611                            "max_total_chunks": 25,
612                            "similarity_threshold": 0.8,
613                            "graph_depth": 2,
614                            "min_relevance": 0.3
615                        }),
616                        "fusion": crate::json!({
617                            "vector_weight": 0.5,
618                            "graph_weight": 0.3,
619                            "table_weight": 0.2,
620                            "dedup_threshold": 0.85
621                        })
622                    }),
623                );
624                store.set_config_tree(
625                    "red.auth",
626                    &crate::json!({
627                        "enabled": false,
628                        "session_ttl_secs": 3600,
629                        "require_auth": false
630                    }),
631                );
632                store.set_config_tree(
633                    "red.query",
634                    &crate::json!({
635                        "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
636                        "max_recursion_depth": 1000
637                    }),
638                );
639                store.set_config_tree(
640                    "red.indexes",
641                    &crate::json!({
642                        "auto_select": true,
643                        "bloom_filter": crate::json!({
644                            "enabled": true,
645                            "false_positive_rate": 0.01,
646                            "prune_on_scan": true
647                        }),
648                        "hash": crate::json!({ "enabled": true }),
649                        "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
650                        "spatial": crate::json!({ "enabled": true })
651                    }),
652                );
653                store.set_config_tree(
654                    "red.memtable",
655                    &crate::json!({
656                        "enabled": true,
657                        "max_bytes": 67108864_u64,
658                        "flush_threshold": 0.75
659                    }),
660                );
661                store.set_config_tree(
662                    "red.probabilistic",
663                    &crate::json!({
664                        "hll_registers": 16384,
665                        "sketch_default_width": 1000,
666                        "sketch_default_depth": 5,
667                        "filter_default_capacity": 100000
668                    }),
669                );
670                store.set_config_tree(
671                    "red.timeseries",
672                    &crate::json!({
673                        "default_chunk_size": 1024,
674                        "compression": crate::json!({
675                            "timestamps": "delta_of_delta",
676                            "values": "gorilla_xor"
677                        }),
678                        "default_retention_days": 0
679                    }),
680                );
681                store.set_config_tree(
682                    "red.queue",
683                    &crate::json!({
684                        "default_max_size": 0,
685                        "default_max_attempts": 3,
686                        "visibility_timeout_ms": 30000,
687                        "consumer_idle_timeout_ms": 60000
688                    }),
689                );
690                store.set_config_tree(
691                    "red.backup",
692                    &crate::json!({
693                        "enabled": false,
694                        "interval_secs": 3600,
695                        "retention_count": 24,
696                        "upload": false,
697                        "backend": "local"
698                    }),
699                );
700                store.set_config_tree(
701                    "red.wal",
702                    &crate::json!({
703                        "archive": crate::json!({
704                            "enabled": false,
705                            "retention_hours": 168,
706                            "prefix": reddb_file::backup_wal_prefix("")
707                        })
708                    }),
709                );
710                store.set_config_tree(
711                    "red.cdc",
712                    &crate::json!({
713                        "enabled": true,
714                        "buffer_size": 100000
715                    }),
716                );
717                store.set_config_tree(
718                    "red.config.secret",
719                    &crate::json!({
720                        "auto_encrypt": true,
721                        "auto_decrypt": true
722                    }),
723                );
724            }
725
726            // Perf-parity config matrix: heal the Tier A (critical)
727            // keys unconditionally on every boot. Idempotent — only
728            // writes the default when the key is missing. Keeps
729            // `SHOW CONFIG` showing every guarantee the operator has
730            // (durability.mode, concurrency.locking.enabled, …) even
731            // on long-running datadirs that predate the matrix.
732            crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
733            seed_storage_deploy_config(store.as_ref(), options.storage_profile);
734
735            // Phase 5 — Lehman-Yao runtime flag. Read the Tier A
736            // `storage.btree.lehman_yao` value from the matrix (env
737            // > file > red_config > default) and publish it to the
738            // storage layer's atomic so the B-tree read / split
739            // paths can branch without re-reading the config on
740            // every hot-path call.
741            let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
742            crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
743            if lehman_yao {
744                tracing::info!(
745                    "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
746                );
747            }
748
749            // Config file overlay — mounted `/etc/reddb/config.json`
750            // (override path via REDDB_CONFIG_FILE). Writes keys with
751            // write-if-absent semantics so a later user `SET CONFIG`
752            // always wins. Missing file = silent no-op.
753            let overlay_path = crate::runtime::config_overlay::config_file_path();
754            let _ =
755                crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
756        }
757
758        // VCS ("Git for Data") — create the `red_*` metadata
759        // collections on first boot. Idempotent: `get_or_create_collection`
760        // is a no-op if the collection already exists.
761        {
762            let store = runtime.inner.db.store();
763            for name in crate::application::vcs_collections::ALL {
764                let _ = store.get_or_create_collection(*name);
765            }
766            // Seed VCS config namespace with sensible defaults on first
767            // boot, matching the pattern used by red.ai / red.storage.
768            store.set_config_tree(
769                crate::application::vcs_collections::CONFIG_NAMESPACE,
770                &crate::json!({
771                    "default_branch": "main",
772                    "author": crate::json!({
773                        "name": "reddb",
774                        "email": "reddb@localhost"
775                    }),
776                    "protected_branches": crate::json!(["main"]),
777                    "closure": crate::json!({
778                        "enabled": true,
779                        "lazy": true
780                    }),
781                    "merge": crate::json!({
782                        "default_strategy": "auto",
783                        "fast_forward": true
784                    })
785                }),
786            );
787        }
788
789        // Migrations — create the `red_migrations` / `red_migration_deps`
790        // system collections on first boot. Idempotent.
791        {
792            let store = runtime.inner.db.store();
793            for name in crate::application::migration_collections::ALL {
794                let _ = store.get_or_create_collection(*name);
795            }
796        }
797
798        // Topology graph (#803) — ensure the built-in `red.topology.cluster`
799        // graph collection (declared WITH ANALYTICS) and its metadata sidecar
800        // exist. Idempotent and survives restarts via the WAL-backed contract.
801        let _ = crate::application::topology_collections::ensure(&runtime);
802
803        // #1369 — reserve a fixed internal-id floor so the first user-inserted
804        // entity always receives a stable, documented `rid` (FIRST_USER_ENTITY_ID),
805        // independent of how many internal collection-descriptor / config-default
806        // entities the boot sequence seeded above. `register_entity_id` only ever
807        // raises the allocator, so a database that already holds user data
808        // (counter past the floor) is untouched; a freshly-seeded database jumps
809        // straight to the floor.
810        runtime
811            .inner
812            .db
813            .store()
814            .register_entity_id(crate::storage::EntityId::new(
815                crate::storage::FIRST_USER_ENTITY_ID - 1,
816            ));
817
818        // Start background maintenance thread (context index refresh +
819        // session purge). Held by a WEAK reference to `RuntimeInner`
820        // so dropping the last `RedDBRuntime` handle actually releases
821        // the underlying Arc<Pager> (and its file lock). Polling at
822        // 200ms means shutdown latency is bounded; the real 60-second
823        // work cadence is tracked independently via a `last_work`
824        // timestamp.
825        //
826        // The previous version captured `rt = runtime.clone()` by
827        // strong reference and ran an unterminated `loop`, which held
828        // Arc<RuntimeInner> forever — reopening a persistent database
829        // in the same process failed with "Database is locked" because
830        // the pager could never drop. See the regression test
831        // `finding_1_select_after_bulk_insert_persistent_reopen`.
832        {
833            let weak = Arc::downgrade(&runtime.inner);
834            std::thread::Builder::new()
835                .name("reddb-maintenance".into())
836                .spawn(move || {
837                    let tick = std::time::Duration::from_millis(200);
838                    let work_interval = std::time::Duration::from_secs(60);
839                    let mut last_work = std::time::Instant::now();
840                    loop {
841                        std::thread::sleep(tick);
842                        let Some(inner) = weak.upgrade() else {
843                            // All strong references dropped — the
844                            // runtime is gone, exit cleanly.
845                            break;
846                        };
847                        if last_work.elapsed() >= work_interval {
848                            let _stats = inner.db.store().context_index().stats();
849                            last_work = std::time::Instant::now();
850                        }
851                    }
852                })
853                .ok();
854        }
855
856        // Start backup scheduler if enabled via red_config
857        {
858            let store = runtime.inner.db.store();
859            let mut backup_enabled = false;
860            let mut backup_interval = 3600u64;
861
862            if let Some(manager) = store.get_collection("red_config") {
863                manager.for_each_entity(|entity| {
864                    if let Some(row) = entity.data.as_row() {
865                        let key = row.get_field("key").and_then(|v| match v {
866                            crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
867                            _ => None,
868                        });
869                        let val = row.get_field("value");
870                        if key == Some("red.config.backup.enabled") {
871                            backup_enabled = match val {
872                                Some(crate::storage::schema::Value::Boolean(true)) => true,
873                                Some(crate::storage::schema::Value::Text(s)) => &**s == "true",
874                                _ => false,
875                            };
876                        } else if key == Some("red.config.backup.interval_secs") {
877                            if let Some(crate::storage::schema::Value::Integer(n)) = val {
878                                backup_interval = *n as u64;
879                            }
880                        }
881                    }
882                    true
883                });
884            }
885
886            if backup_enabled {
887                runtime.inner.backup_scheduler.set_interval(backup_interval);
888                let rt = runtime.clone();
889                runtime
890                    .inner
891                    .backup_scheduler
892                    .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
893            }
894        }
895
896        // Load EC registry from red_config and start worker
897        {
898            runtime
899                .inner
900                .ec_registry
901                .load_from_config_store(runtime.inner.db.store().as_ref());
902            if !runtime.inner.ec_registry.async_configs().is_empty() {
903                runtime.inner.ec_worker.start(
904                    Arc::clone(&runtime.inner.ec_registry),
905                    Arc::clone(&runtime.inner.db.store()),
906                );
907            }
908        }
909
910        if let crate::replication::ReplicationRole::Replica { primary_addr } =
911            runtime.inner.db.options().replication.role.clone()
912        {
913            let rt = runtime.clone();
914            std::thread::Builder::new()
915                .name("reddb-replica".into())
916                .spawn(move || rt.run_replica_loop(primary_addr))
917                .ok();
918        }
919
920        // PLAN.md Phase 1 — Lifecycle Contract. Mark Ready once every
921        // boot stage above has completed (WAL replay, restore-from-
922        // remote, replica-loop spawn). Health probes flip from 503 to
923        // 200 here; shutdown begins from this state.
924        runtime.inner.lifecycle.mark_ready();
925
926        // Issue #583 slice 10 — ContinuousMaterializedView scheduler.
927        // Low-priority background ticker that drains the cache's
928        // `claim_due_at` set every ~50ms. Holds only a Weak<RuntimeInner>
929        // so the thread exits cleanly when the runtime drops (≤50ms
930        // latency between drop and exit). Materialized views without
931        // a `REFRESH EVERY` clause stay on the manual-refresh path
932        // and are skipped by `claim_due_at`, so the loop is a no-op
933        // when no scheduled views exist.
934        {
935            let weak_inner = Arc::downgrade(&runtime.inner);
936            std::thread::Builder::new()
937                .name("reddb-mv-scheduler".into())
938                // Rust's default for spawned threads is 2 MiB, which is too
939                // small for the query-execution path that refresh_due_materialized_views
940                // runs (StatementExecutionFrame + guards per view). Match the
941                // 8 MiB that the OS gives the main thread.
942                .stack_size(8 * 1024 * 1024)
943                .spawn(move || loop {
944                    std::thread::sleep(std::time::Duration::from_millis(50));
945                    let Some(inner) = weak_inner.upgrade() else {
946                        break;
947                    };
948                    let rt = RedDBRuntime { inner };
949                    rt.refresh_due_materialized_views();
950                })
951                .ok();
952        }
953
954        // Issue #584 slice 12 — DeclarativeRetention background sweeper.
955        // Low-priority ticker that physically reclaims rows whose
956        // timestamp has fallen beyond the retention window. Holds a
957        // `Weak<RuntimeInner>` so the thread exits within one tick of
958        // the runtime drop (graceful shutdown leaves storage consistent
959        // because each tick goes through the standard DELETE path —
960        // there is no half-finished mutation state to clean up). The
961        // tick interval is intentionally longer than the MV scheduler
962        // (500ms) because retention is order-of-seconds at minimum.
963        if !runtime.write_gate().is_read_only() {
964            let weak_inner = Arc::downgrade(&runtime.inner);
965            std::thread::Builder::new()
966                .name("reddb-retention-sweeper".into())
967                .spawn(move || loop {
968                    std::thread::sleep(std::time::Duration::from_millis(500));
969                    let Some(inner) = weak_inner.upgrade() else {
970                        break;
971                    };
972                    let rt = RedDBRuntime { inner };
973                    rt.sweep_retention_tick(
974                        crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
975                    );
976                })
977                .ok();
978        }
979
980        Ok(runtime)
981    }
982
983    fn rehydrate_snapshot_xid_floor(&self) {
984        let store = self.inner.db.store();
985        for collection in store.list_collections() {
986            let Some(manager) = store.get_collection(&collection) else {
987                continue;
988            };
989            for entity in manager.query_all(|_| true) {
990                self.inner
991                    .snapshot_manager
992                    .observe_committed_xid(entity.xmin);
993                self.inner
994                    .snapshot_manager
995                    .observe_committed_xid(entity.xmax);
996            }
997        }
998    }
999
1000    /// Provision an empty Table-shaped collection that backs a
1001    /// `CREATE MATERIALIZED VIEW v` (issue #594 slice 9b of #575).
1002    /// `SELECT FROM v` reads this collection directly; the rewriter is
1003    /// configured to skip materialized views so the body is no longer
1004    /// substituted. REFRESH still writes to the cache slot — wiring it
1005    /// into this backing collection is the job of slice 9c.
1006    ///
1007    /// Idempotent: re-running for the same name leaves the existing
1008    /// collection in place (mirrors `CREATE TABLE IF NOT EXISTS`
1009    /// semantics). This keeps `CREATE OR REPLACE MATERIALIZED VIEW v`
1010    /// cheap — the body change does not invalidate already-buffered
1011    /// rows. Until 9c lands the backing is always empty anyway.
1012    pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1013        let store = self.inner.db.store();
1014        let mut changed = false;
1015        if store.get_collection(name).is_none() {
1016            store.get_or_create_collection(name);
1017            changed = true;
1018        }
1019        if self.inner.db.collection_contract(name).is_none() {
1020            self.inner
1021                .db
1022                .save_collection_contract(system_keyed_collection_contract(
1023                    name,
1024                    crate::catalog::CollectionModel::Table,
1025                ))
1026                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1027            changed = true;
1028        }
1029        if changed {
1030            self.inner
1031                .db
1032                .persist_metadata()
1033                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1034        }
1035        Ok(())
1036    }
1037
1038    /// Inverse of [`ensure_materialized_view_backing`] — drops the
1039    /// backing collection on `DROP MATERIALIZED VIEW v`. No-op when
1040    /// the collection was never created (e.g. a `DROP MATERIALIZED
1041    /// VIEW IF EXISTS v` against an unknown name).
1042    pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1043        let store = self.inner.db.store();
1044        if store.get_collection(name).is_none() {
1045            return Ok(());
1046        }
1047        store
1048            .drop_collection(name)
1049            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1050        // The contract may have been dropped already (DROP TABLE path)
1051        // — ignore "not found" errors by checking presence first.
1052        if self.inner.db.collection_contract(name).is_some() {
1053            self.inner
1054                .db
1055                .remove_collection_contract(name)
1056                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1057        }
1058        self.invalidate_result_cache();
1059        self.inner
1060            .db
1061            .persist_metadata()
1062            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1063        Ok(())
1064    }
1065
1066    fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
1067        let mut changed = false;
1068        for (name, model) in [
1069            ("red.config", crate::catalog::CollectionModel::Config),
1070            ("red.vault", crate::catalog::CollectionModel::Vault),
1071            // Issue #593 — materialized-view catalog. One row per
1072            // `CREATE MATERIALIZED VIEW`; rehydrated at boot before
1073            // the API opens.
1074            (
1075                crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
1076                crate::catalog::CollectionModel::Config,
1077            ),
1078        ] {
1079            if self.inner.db.store().get_collection(name).is_none() {
1080                self.inner.db.store().get_or_create_collection(name);
1081                changed = true;
1082            }
1083            if self.inner.db.collection_contract(name).is_none() {
1084                self.inner
1085                    .db
1086                    .save_collection_contract(system_keyed_collection_contract(name, model))
1087                    .map_err(|err| RedDBError::Internal(err.to_string()))?;
1088                changed = true;
1089            }
1090        }
1091        if changed {
1092            self.inner
1093                .db
1094                .persist_metadata()
1095                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1096        }
1097        Ok(())
1098    }
1099
1100    pub fn db(&self) -> Arc<RedDB> {
1101        Arc::clone(&self.inner.db)
1102    }
1103
1104    /// Direct access to the runtime's secondary-index store.
1105    /// Used by bulk-insert entry points (gRPC binary bulk, HTTP bulk,
1106    /// wire bulk) that need to push new rows through the per-index
1107    /// maintenance hook after `store.bulk_insert` returns.
1108    pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
1109        &self.inner.index_store
1110    }
1111
1112    /// Apply a DDL event to the schema-vocabulary reverse index
1113    /// (issue #120). Called by DDL execution paths after the catalog
1114    /// mutation has succeeded so the index never holds entries for
1115    /// half-applied DDL.
1116    pub(crate) fn schema_vocabulary_apply(
1117        &self,
1118        event: crate::runtime::schema_vocabulary::DdlEvent,
1119    ) {
1120        self.inner.schema_vocabulary.write().on_ddl(event);
1121    }
1122
1123    /// Lookup `token` in the schema-vocabulary reverse index. Returns
1124    /// an owned `Vec<VocabHit>` because the underlying read lock
1125    /// cannot be borrowed across the call boundary; the slice from
1126    /// `SchemaVocabulary::lookup` is cloned per hit.
1127    pub fn schema_vocabulary_lookup(
1128        &self,
1129        token: &str,
1130    ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
1131        self.inner.schema_vocabulary.read().lookup(token).to_vec()
1132    }
1133
1134    /// Inject an AuthStore into the runtime. Called by server boot
1135    /// after the vault has been bootstrapped, so that `Value::Secret`
1136    /// auto-encrypt/decrypt can reach the vault AES key.
1137    pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
1138        *self.inner.auth_store.write() = Some(store);
1139    }
1140
1141    /// Snapshot the current AuthStore (if any). Used by the wire listener
1142    /// to validate bearer tokens issued via HTTP `/auth/login`.
1143    pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
1144        self.inner.auth_store.read().clone()
1145    }
1146
1147    /// Inject an `OAuthValidator` into the runtime. When set, HTTP and
1148    /// wire transports try OAuth JWT validation before falling back to
1149    /// the local AuthStore lookup. Pass `None` to disable.
1150    pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
1151        *self.inner.oauth_validator.write() = validator;
1152    }
1153
1154    /// Returns a clone of the configured `OAuthValidator` Arc, if any.
1155    /// Hot path: called per HTTP request when an Authorization header
1156    /// is present, so we hand back a cheap Arc clone.
1157    pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
1158        self.inner.oauth_validator.read().clone()
1159    }
1160
1161    /// Inject the browser-token authority (issue #936). When set, the
1162    /// RedWire WS handshake accepts the short-lived access JWT it mints
1163    /// (alongside, and tried before, the federated OAuth validator), and
1164    /// the `/auth/browser/*` HTTP endpoints can issue/rotate the pair.
1165    /// `None` leaves the browser credential flow inert.
1166    pub fn set_browser_token_authority(
1167        &self,
1168        authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
1169    ) {
1170        *self.inner.browser_token_authority.write() = authority;
1171    }
1172
1173    /// Snapshot the browser-token authority, if wired. Read on the WS
1174    /// handshake path and by the `/auth/browser/*` handlers; a cheap Arc
1175    /// clone keeps the lock hold short.
1176    pub fn browser_token_authority(
1177        &self,
1178    ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
1179        self.inner.browser_token_authority.read().clone()
1180    }
1181}