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
15fn boot_cache_u64(
16    store: &crate::storage::UnifiedStore,
17    env: &HashMap<String, String>,
18    key: &str,
19    default: u64,
20) -> u64 {
21    if let Some(value) = env.get(key).and_then(|value| value.parse::<u64>().ok()) {
22        return value;
23    }
24    match store.get_config(key) {
25        Some(reddb_types::Value::UnsignedInteger(value)) => value,
26        Some(reddb_types::Value::Integer(value)) => u64::try_from(value).unwrap_or(default),
27        Some(reddb_types::Value::Text(value)) => value.parse::<u64>().unwrap_or(default),
28        _ => default,
29    }
30}
31
32/// Convert the rows produced by a materialized-view body into
33/// `UnifiedEntity` table rows targeting the backing collection.
34/// Issue #595 slice 9c — feeds `UnifiedStore::refresh_collection`.
35///
36/// Graph fragments and vector hits are ignored: a materialized view
37/// is a relational result set (SELECT-shaped); slices 11+ may extend
38/// this once we have a richer view body shape. Each row materialises
39/// the union of its schema-bound columns + overflow.
40pub(crate) fn view_records_to_entities(
41    table: &str,
42    records: &[crate::storage::query::unified::UnifiedRecord],
43) -> Vec<crate::storage::UnifiedEntity> {
44    use std::collections::HashMap;
45    let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
46    let mut out = Vec::with_capacity(records.len());
47    for record in records {
48        let mut named: HashMap<String, reddb_types::Value> = HashMap::new();
49        for (name, value) in record.iter_fields() {
50            named.insert(name.to_string(), value.clone());
51        }
52        let entity = crate::storage::UnifiedEntity::new(
53            crate::storage::EntityId::new(0),
54            crate::storage::EntityKind::TableRow {
55                table: std::sync::Arc::clone(&table_arc),
56                row_id: 0,
57            },
58            crate::storage::EntityData::Row(crate::storage::RowData {
59                columns: Vec::new(),
60                named: Some(named),
61                schema: None,
62            }),
63        );
64        out.push(entity);
65    }
66    out
67}
68
69fn system_keyed_collection_contract(
70    name: &str,
71    model: crate::catalog::CollectionModel,
72) -> crate::physical::CollectionContract {
73    let now = crate::utils::now_unix_millis() as u128;
74    crate::physical::CollectionContract {
75        name: name.to_string(),
76        description: None,
77        description_generated: false,
78        declared_model: model,
79        schema_mode: crate::catalog::SchemaMode::Dynamic,
80        origin: crate::physical::ContractOrigin::Implicit,
81        version: 1,
82        created_at_unix_ms: now,
83        updated_at_unix_ms: now,
84        default_ttl_ms: None,
85        vector_dimension: None,
86        vector_metric: None,
87        context_index_fields: Vec::new(),
88        declared_columns: Vec::new(),
89        table_def: None,
90        timestamps_enabled: false,
91        context_index_enabled: false,
92        metrics_raw_retention_ms: None,
93        metrics_rollup_policies: Vec::new(),
94        metrics_tenant_identity: None,
95        metrics_namespace: None,
96        append_only: false,
97        subscriptions: Vec::new(),
98        analytics_config: Vec::new(),
99        session_key: None,
100        session_gap_ms: None,
101        retention_duration_ms: None,
102        analytical_storage: None,
103
104        ai_policy: None,
105    }
106}
107
108pub(crate) fn table_row_index_fields(
109    entity: &crate::storage::unified::entity::UnifiedEntity,
110) -> Vec<(String, reddb_types::Value)> {
111    let crate::storage::EntityData::Row(row) = &entity.data else {
112        return Vec::new();
113    };
114    if let Some(named) = &row.named {
115        return named
116            .iter()
117            .map(|(name, value)| (name.clone(), value.clone()))
118            .collect();
119    }
120    if let Some(schema) = &row.schema {
121        return schema
122            .iter()
123            .zip(row.columns.iter())
124            .map(|(name, value)| (name.clone(), value.clone()))
125            .collect();
126    }
127    Vec::new()
128}
129
130impl RedDBRuntime {
131    pub fn in_memory() -> RedDBResult<Self> {
132        Self::with_options(RedDBOptions::in_memory())
133    }
134
135    pub fn flush(&self) -> RedDBResult<()> {
136        let store = self.inner.db.store();
137        let publication = store.vault_publication_lock.lock();
138        self.inner
139            .db
140            .flush_local_only()
141            .map_err(|err| RedDBError::Internal(err.to_string()))?;
142        self.index_store_ref().clear_paged_btree_overlays();
143        drop(publication);
144        if let Err(err) = self.assert_remote_write_allowed("flush") {
145            tracing::warn!(
146                target: "reddb::serverless::lease",
147                error = %err,
148                "flush: skipping remote upload — lease not held"
149            );
150            return Ok(());
151        }
152        self.publish_remote_snapshot()
153    }
154
155    /// Handle to the intent-lock manager for tests + introspection.
156    /// Production code acquires via `LockerGuard::new(rt.lock_manager())`
157    /// rather than touching the manager directly.
158    pub fn lock_manager(&self) -> std::sync::Arc<crate::runtime::lock_manager::LockManager> {
159        self.inner.lock_manager.clone()
160    }
161
162    /// Process-local governance registry for managed policy/config guardrails.
163    pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
164        self.inner.config_registry.clone()
165    }
166
167    pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
168        self.inner.query_audit.clone()
169    }
170
171    pub fn control_events_require_persistence(&self) -> bool {
172        self.inner.control_event_config.require_persistence()
173    }
174
175    pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
176        self.inner.control_event_config
177    }
178
179    pub fn control_event_ledger(
180        &self,
181    ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
182        self.inner.control_event_ledger.read().clone()
183    }
184
185    #[doc(hidden)]
186    pub fn replace_control_event_ledger_for_tests(
187        &self,
188        ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
189    ) {
190        *self.inner.control_event_ledger.write() = ledger;
191    }
192
193    #[inline(never)]
194    pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
195        Self::with_pool(options, ConnectionPoolConfig::default())
196    }
197
198    /// The memory budget resolved at boot (ADR 0073 §1). Immutable for the
199    /// process lifetime; echoed by the boot log and the `red.stats` budget
200    /// section.
201    pub fn memory_budget(&self) -> crate::storage::memory_budget::MemoryBudget {
202        self.inner.memory_budget
203    }
204
205    /// The one shared accounting pool (ADR 0073 §2). Per-pool shares are fixed
206    /// at boot; live usage is refreshed by
207    /// [`RedDBRuntime::refresh_memory_accounting`].
208    pub fn memory_accounting(&self) -> &crate::storage::memory_pools::MemoryAccounting {
209        &self.inner.memory_accounting
210    }
211
212    pub fn with_pool(
213        options: RedDBOptions,
214        pool_config: ConnectionPoolConfig,
215    ) -> RedDBResult<Self> {
216        Self::with_pool_and_limits(
217            options,
218            pool_config,
219            super::resource_limits::ResourceLimits::from_env(),
220        )
221    }
222
223    pub(super) fn with_pool_and_limits(
224        options: RedDBOptions,
225        pool_config: ConnectionPoolConfig,
226        resource_limits: super::resource_limits::ResourceLimits,
227    ) -> RedDBResult<Self> {
228        options
229            .replication
230            .validate_failover_profile()
231            .map_err(RedDBError::InvalidConfig)?;
232        // PLAN.md Phase 9.1 — capture wall-clock before storage
233        // open so the cold-start phase markers can be backfilled
234        // once Lifecycle is constructed below. Storage open
235        // encapsulates auto-restore + WAL replay; we treat the
236        // whole window as one combined "restore" + "wal_replay"
237        // phase split at the same boundary because the storage
238        // layer doesn't yet emit a finer signal.
239        let boot_open_start_ms = std::time::SystemTime::now()
240            .duration_since(std::time::UNIX_EPOCH)
241            .map(|d| d.as_millis() as u64)
242            .unwrap_or(0);
243        // ADR 0073 §1 — resolve the one memory budget before anything is
244        // opened, so a nonsensical operator value fails the boot before the
245        // process touches disk. The log line fires once per process.
246        let memory_budget = crate::storage::memory_budget::resolve_for_boot(
247            options.storage_profile.deploy_profile,
248            options.memory_budget_bytes,
249        )
250        .map_err(|err| RedDBError::InvalidConfig(err.to_string()))?;
251        crate::storage::memory_budget::log_resolved_once(&memory_budget);
252        // ADR 0073 §2 — divide the budget among the pools before any of them
253        // is constructed. `BudgetShares::resolve` asserts Σ(shares) ≤ budget;
254        // one boot log line per pool names its share.
255        let memory_shares = crate::storage::memory_pools::BudgetShares::resolve(
256            memory_budget,
257            options.storage_profile.deploy_profile,
258        );
259        memory_shares.log_once();
260        let embedded_single_file = options.storage_profile.deploy_profile
261            == crate::storage::DeployProfile::Embedded
262            && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
263        let db = Arc::new(RedDB::open_with_options(&options).map_err(|err| {
264            match err.downcast::<crate::storage::StoreError>() {
265                Ok(store_err) => RedDBError::from(*store_err),
266                Err(err) => RedDBError::Internal(err.to_string()),
267            }
268        })?);
269        let env_config_overrides = crate::runtime::config_overlay::collect_env_overrides();
270        // Query results are disposable. Do not create a second persistent
271        // database outside the .rdb and its WAL for the cache.
272        let cache_l1_share = memory_shares.blob_cache_l1_bytes();
273        let cache_l1_default = u64::try_from(cache_l1_share).unwrap_or(u64::MAX);
274        let configured_l1 = usize::try_from(boot_cache_u64(
275            db.store().as_ref(),
276            &env_config_overrides,
277            "cache.blob.l1_bytes_max",
278            cache_l1_default,
279        ))
280        .map_err(|_| RedDBError::InvalidConfig("cache.blob.l1_bytes_max is too large".into()))?;
281        let configured_l2 = boot_cache_u64(
282            db.store().as_ref(),
283            &env_config_overrides,
284            "cache.blob.l2_bytes_max",
285            crate::storage::cache::DEFAULT_BLOB_L2_BYTES_MAX,
286        );
287        let configured_namespaces = usize::try_from(boot_cache_u64(
288            db.store().as_ref(),
289            &env_config_overrides,
290            "cache.blob.max_namespaces",
291            u64::try_from(crate::storage::cache::DEFAULT_BLOB_MAX_NAMESPACES).unwrap_or(u64::MAX),
292        ))
293        .map_err(|_| RedDBError::InvalidConfig("cache.blob.max_namespaces is too large".into()))?;
294        if configured_namespaces == 0 {
295            return Err(RedDBError::InvalidConfig(
296                "cache.blob.max_namespaces must be greater than zero".into(),
297            ));
298        }
299        let configured_namespace_bytes = usize::try_from(boot_cache_u64(
300            db.store().as_ref(),
301            &env_config_overrides,
302            "cache.blob.namespace_bytes_max",
303            u64::try_from(configured_l1).unwrap_or(u64::MAX),
304        ))
305        .map_err(|_| {
306            RedDBError::InvalidConfig("cache.blob.namespace_bytes_max is too large".into())
307        })?;
308        if configured_namespace_bytes == 0 {
309            return Err(RedDBError::InvalidConfig(
310                "cache.blob.namespace_bytes_max must be greater than zero".into(),
311            ));
312        }
313        let result_blob_cache_config = crate::storage::cache::BlobCacheConfig::default()
314            // The cache-specific setting may reserve less memory, but never
315            // escapes the process-wide memory budget.
316            .with_l1_bytes_max(configured_l1.min(cache_l1_share))
317            .with_namespace_bytes_max(
318                configured_namespace_bytes
319                    .min(configured_l1)
320                    .min(cache_l1_share),
321            )
322            // Zero is the explicit off switch. Read-only runtimes cannot
323            // maintain expiry/invalidation state in the internal collection.
324            .with_l2_bytes_max(if options.read_only { 0 } else { configured_l2 })
325            .with_max_namespaces(configured_namespaces);
326        let result_blob_cache = crate::storage::cache::BlobCache::open_with_internal_l2(
327            result_blob_cache_config,
328            db.store(),
329        )
330        .map_err(|err| {
331            RedDBError::Internal(format!(
332                "open single-file result Blob Cache failed: {err:?}"
333            ))
334        })?;
335        let storage_ready_ms = std::time::SystemTime::now()
336            .duration_since(std::time::UNIX_EPOCH)
337            .map(|d| d.as_millis() as u64)
338            .unwrap_or(0);
339
340        let no_auth_boot = crate::service_cli::no_auth_active(&options);
341        let runtime = Self {
342            background_tasks: Some(Arc::new(background_tasks::RuntimeBackgroundTasks::default())),
343            inner: Arc::new(RuntimeInner {
344                functions: parking_lot::RwLock::new(
345                    super::function_catalog::FunctionCatalog::load(&db)?,
346                ),
347                db: db.clone(),
348                layout: PhysicalLayout::from_options(&options),
349                embedded_single_file,
350                memory_budget,
351                memory_accounting: Arc::new(
352                    crate::storage::memory_pools::MemoryAccounting::from_shares(
353                        memory_budget,
354                        memory_shares,
355                    ),
356                ),
357                memory_reservations: parking_lot::Mutex::new(
358                    super::memory_admission::MemoryReservations::default(),
359                ),
360                mvcc_maintenance: parking_lot::Mutex::new(Default::default()),
361                indices: IndexCatalog::register_default_vector_graph(
362                    options.has_capability(crate::api::Capability::Table),
363                    options.has_capability(crate::api::Capability::Graph),
364                ),
365                pool_config,
366                pool: Mutex::new(PoolState::default()),
367                execution_admission: parking_lot::RwLock::new(
368                    crate::transport::execution_admission::ExecutionAdmission::for_host(),
369                ),
370                execution_queue_timeout_ms: AtomicU64::new(30_000),
371                started_at_unix_ms: SystemTime::now()
372                    .duration_since(UNIX_EPOCH)
373                    .unwrap_or_default()
374                    .as_millis(),
375                probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
376                index_store: super::index_store::IndexStore::new(),
377                cdc: crate::replication::cdc::CdcBuffer::new(100_000),
378                checkpoint_projection_stats: super::CheckpointProjectionStats::default(),
379                scrub_state: parking_lot::Mutex::new(super::ScrubRuntimeState::default()),
380                checkpoint_columnar_emission_budget_chunks: options
381                    .checkpoint_columnar_emission_budget_chunks,
382                columnar_projection_size_floor_rows: options.columnar_projection_size_floor_rows,
383                backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
384                query_cache: parking_lot::RwLock::new(
385                    crate::storage::query::planner::cache::PlanCache::new(1000),
386                ),
387                result_blob_cache,
388                result_cache_format_lock: parking_lot::Mutex::new(()),
389                result_blob_promotion: parking_lot::Mutex::new(()),
390                result_blob_entries: parking_lot::RwLock::new((
391                    HashMap::new(),
392                    std::collections::VecDeque::new(),
393                    true,
394                    0,
395                    HashSet::new(),
396                    None,
397                    db.store().result_cache_epoch().unwrap_or(0),
398                )),
399                ask_answer_cache_entries: parking_lot::RwLock::new((
400                    HashSet::new(),
401                    std::collections::VecDeque::new(),
402                )),
403                result_cache_hits: std::sync::atomic::AtomicU64::new(0),
404                result_cache_misses: std::sync::atomic::AtomicU64::new(0),
405                result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
406                ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
407                queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
408                rmw_locks: RmwLockTable::new(),
409                planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
410                ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
411                config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
412                ec_worker: crate::ec::worker::EcWorker::new(),
413                auth_store: parking_lot::RwLock::new(None),
414                oauth_validator: parking_lot::RwLock::new(None),
415                browser_token_authority: parking_lot::RwLock::new(None),
416                views: parking_lot::RwLock::new(HashMap::new()),
417                materialized_views: parking_lot::RwLock::new(
418                    crate::storage::cache::result::MaterializedViewCache::new(),
419                ),
420                retention_sweeper: parking_lot::RwLock::new(
421                    crate::runtime::retention_sweeper::RetentionSweeperState::new(),
422                ),
423                transaction_state: crate::runtime::transaction_state::TransactionState::new(),
424                env_config_overrides,
425                lock_manager: Arc::new({
426                    // Sourced from the matrix: Tier B key
427                    // `concurrency.locking.deadlock_timeout_ms`
428                    // (default 5000). Env var wins at boot so
429                    // operators can tune without touching red.config.
430                    let env = crate::runtime::config_overlay::collect_env_overrides();
431                    let timeout_ms = env
432                        .get("concurrency.locking.deadlock_timeout_ms")
433                        .and_then(|raw| raw.parse::<u64>().ok())
434                        .unwrap_or_else(|| {
435                            match crate::runtime::config_matrix::default_for(
436                                "concurrency.locking.deadlock_timeout_ms",
437                            ) {
438                                Some(crate::serde_json::Value::Number(n)) => n as u64,
439                                _ => 5000,
440                            }
441                        });
442                    let cfg = crate::runtime::lock_manager::LockConfig {
443                        default_timeout: std::time::Duration::from_millis(timeout_ms),
444                        ..Default::default()
445                    };
446                    crate::runtime::lock_manager::LockManager::new(cfg)
447                }),
448                rls_policies: parking_lot::RwLock::new(HashMap::new()),
449                rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
450                foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
451                pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
452                pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
453                pending_queue_dedup: parking_lot::RwLock::new(HashMap::new()),
454                pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
455                pending_vault_writes: parking_lot::RwLock::new(HashMap::new()),
456                pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
457                pending_logical_records: parking_lot::Mutex::new(HashMap::new()),
458                pending_commit_policies: parking_lot::Mutex::new(HashMap::new()),
459                logical_publication: parking_lot::ReentrantMutex::new(()),
460                logical_allocated_lsn: std::sync::atomic::AtomicU64::new(0),
461                pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
462                queue_wait_registry: std::sync::Arc::new(
463                    crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
464                ),
465                pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
466                tenant_tables: parking_lot::RwLock::new(HashMap::new()),
467                ddl_epoch: std::sync::atomic::AtomicU64::new(0),
468                write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
469                    &options,
470                )),
471                lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
472                resource_limits,
473                query_budget_slots: AtomicU64::new(0),
474                audit_log: {
475                    // Default audit-log path for the in-memory case
476                    // sits in the system temp dir; persistent runs
477                    // place it next to the resolved data file.
478                    //
479                    // gh-471 iter 2: route through the resolved
480                    // `LogDestination`. Performance/Max tiers emit a
481                    // file-backed log destination under the file-owned
482                    // support-directory logs tier;
483                    // lower tiers / ephemeral runs report `Stderr`
484                    // and we keep the legacy file-next-to-data sink.
485                    // #1375 — single-file embedded mode keeps the data
486                    // directory to exactly the `.rdb` artifact, so the audit
487                    // log must NOT land as a sibling. Route it to a
488                    // process-unique temp location even when a data path is
489                    // set; only the non-embedded case uses the data dir.
490                    let data_path = if embedded_single_file || options.read_only {
491                        std::env::temp_dir()
492                            .join("reddb-embedded-runtime")
493                            .join(format!("audit-{}", std::process::id()))
494                    } else {
495                        options
496                            .data_path
497                            .clone()
498                            .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
499                    };
500                    let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
501                    let audit_dest = if options.read_only {
502                        crate::storage::layout::LogDestination::Stderr
503                    } else {
504                        audit_dest
505                    };
506                    if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
507                        && (embedded_single_file
508                            || options.read_only
509                            || options
510                                .metadata
511                                .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
512                    {
513                        // The Stderr/Syslog lower-tier sink resolves to a
514                        // `for_data_path` sibling that collides across concurrent
515                        // temp-dir runtimes — nextest's process-per-test model
516                        // truncates one shared file, flaking audit assertions.
517                        // Pin a unique sibling for these short-lived ephemeral /
518                        // single-file embedded runtimes. The file-owned support-
519                        // dir tier (`File`) is already per-data unique, so leave
520                        // it to `for_destination` (#1375: the embedded audit then
521                        // still never lands a sibling next to the `.rdb`).
522                        let audit_path = reddb_file::layout::sibling_path(
523                            &data_path,
524                            &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
525                        );
526                        Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
527                            audit_path,
528                        ))
529                    } else {
530                        Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
531                            &audit_dest,
532                            &data_path,
533                        ))
534                    }
535                },
536                control_event_ledger: parking_lot::RwLock::new(Arc::new(
537                    crate::runtime::control_events::RuntimeLedger::new(db.store()),
538                )),
539                control_event_config: options.control_events,
540                query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
541                    db.store(),
542                    options.query_audit.clone(),
543                )),
544                lease_lifecycle: std::sync::OnceLock::new(),
545                routing_primary_endpoints: parking_lot::RwLock::new(Default::default()),
546                replica_apply_metrics: std::sync::Arc::new(
547                    crate::replication::logical::ReplicaApplyMetrics::default(),
548                ),
549                replica_link_metrics: std::sync::Arc::new(
550                    crate::replication::reconnect::ReplicaLinkMetrics::default(),
551                ),
552                quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
553                schema_vocabulary: parking_lot::RwLock::new(
554                    crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
555                ),
556                description_enrichment: parking_lot::Mutex::new(Default::default()),
557                slow_query_logger: {
558                    // Issue #205 — slow-query sink lives in the same
559                    // directory the audit log uses, so backup/restore
560                    // ships them together. Threshold + sample-pct
561                    // default conservatively (1 s, 100% sampling) so
562                    // emitted lines are rare and complete. Operators
563                    // tune via env / config matrix in a follow-up.
564                    //
565                    // gh-471 iter 2: same routing as the audit log —
566                    // `LogDestination::File(...)` for Performance/Max
567                    // lands under the file-owned support-directory logs tier;
568                    // lower tiers fall back to `red-slow.log` in the
569                    // data directory.
570                    // #1375 — see the audit-log note above: single-file mode
571                    // never writes the slow-query log as a sibling of the
572                    // `.rdb`. Route to a process-unique temp dir when embedded,
573                    // regardless of the data path.
574                    let fallback_dir = if embedded_single_file || options.read_only {
575                        std::env::temp_dir()
576                            .join("reddb-embedded-runtime")
577                            .join(format!("slow-{}", std::process::id()))
578                    } else {
579                        options
580                            .data_path
581                            .as_ref()
582                            .and_then(|p| p.parent().map(std::path::PathBuf::from))
583                            .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
584                    };
585                    let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
586                        .ok()
587                        .and_then(|s| s.parse::<u64>().ok())
588                        .unwrap_or(1000);
589                    let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
590                        .ok()
591                        .and_then(|s| s.parse::<u8>().ok())
592                        .unwrap_or(100);
593                    let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
594                    let slow_dest = if options.read_only {
595                        crate::storage::layout::LogDestination::Stderr
596                    } else {
597                        slow_dest
598                    };
599                    crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
600                        &slow_dest,
601                        &fallback_dir,
602                        threshold_ms,
603                        sample_pct,
604                    )
605                },
606                slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
607                    crate::telemetry::slow_query_store::DEFAULT_CAP,
608                ),
609                kv_stats: crate::runtime::KvStatsCounters::default(),
610                metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
611                metrics_tenant_activity_stats:
612                    crate::runtime::MetricsTenantActivityCounters::default(),
613                claim_telemetry: Arc::new(
614                    crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
615                ),
616                queue_telemetry: Arc::new(
617                    crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
618                ),
619                query_latency_telemetry: Arc::new(
620                    crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
621                ),
622                occupancy_sampler: Arc::new(
623                    crate::runtime::occupancy_sampler::OccupancySampler::new(),
624                ),
625                node_load_telemetry: Arc::new(
626                    crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
627                ),
628                queue_presence: Arc::new(
629                    crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
630                ),
631                vector_introspection: Arc::new(
632                    crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
633                ),
634                kv_tag_index: crate::runtime::KvTagIndex::default(),
635                chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
636                chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
637                integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
638                integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
639            }),
640        };
641
642        runtime.install_hash_candidate_admission();
643
644        // Issue #205 — install the process-wide OperatorEvent sink so
645        // emit sites buried in storage / replication / signal handlers
646        // can record without threading an `&AuditLogger` through every
647        // call stack. First registration wins; subsequent in-memory
648        // runtimes (test harnesses) fall through to tracing+eprintln.
649        crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
650            &runtime.inner.audit_log,
651        ));
652
653        // Issue #1238 — wire the slow-query telemetry substrate (ADR 0060).
654        // The logger dual-writes: file sink (existing) + ring store (new).
655        runtime
656            .inner
657            .slow_query_logger
658            .attach_store(Arc::clone(&runtime.inner.slow_query_store));
659
660        // PLAN.md Phase 9.1 — backfill cold-start phase markers
661        // from the wall-clock captured before storage open. The
662        // entire `RedDB::open_with_options` call covers both
663        // auto-restore (when configured) and WAL replay. We
664        // record both phases against the same boundary today;
665        // a follow-up will split them once the storage layer
666        // surfaces a finer-grained event.
667        runtime
668            .inner
669            .lifecycle
670            .set_restore_started_at_ms(boot_open_start_ms);
671        runtime
672            .inner
673            .lifecycle
674            .set_restore_ready_at_ms(storage_ready_ms);
675        runtime
676            .inner
677            .lifecycle
678            .set_wal_replay_started_at_ms(boot_open_start_ms);
679        runtime
680            .inner
681            .lifecycle
682            .set_wal_replay_ready_at_ms(storage_ready_ms);
683
684        let inherited_replication_lsn = if runtime.inner.db.replication.is_some() {
685            let promotion_lsn = runtime
686                .primary_replica_timeline_history()?
687                .and_then(|history| history.entries.last().map(|entry| entry.fork_lsn))
688                .unwrap_or(0);
689            runtime
690                .config_u64("replication.last_applied_lsn", 0)
691                .max(promotion_lsn)
692        } else {
693            0
694        };
695        let restored_cdc_lsn = runtime
696            .inner
697            .db
698            .replication
699            .as_ref()
700            .map(|repl| {
701                repl.logical_wal_spool
702                    .as_ref()
703                    .map(|spool| spool.current_lsn())
704                    .unwrap_or(0)
705            })
706            .unwrap_or(0)
707            .max(runtime.config_u64("timeline.last_archived_lsn", 0))
708            .max(inherited_replication_lsn);
709        runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
710        runtime
711            .inner
712            .logical_allocated_lsn
713            .store(restored_cdc_lsn, std::sync::atomic::Ordering::Release);
714        runtime.rehydrate_snapshot_xid_floor()?;
715        assert!(runtime
716            .inner
717            .db
718            .runtime
719            .set(super::WeakRuntime(Arc::downgrade(&runtime.inner)))
720            .is_ok());
721        assert!(
722            runtime
723                .inner
724                .db
725                .store()
726                .snapshot_manager
727                .set(runtime.snapshot_manager())
728                .is_ok(),
729            "a new runtime owns the store transaction outcomes"
730        );
731        runtime
732            .bootstrap_system_keyed_collections()
733            .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
734        runtime.rehydrate_declared_column_schemas();
735        // Registry restoration can checkpoint missing required projections.
736        // Register their projectors before that epoch.
737        super::control_event_index::prepare(&runtime)?;
738        super::queue_storage_index::prepare(&runtime)?;
739        runtime.rehydrate_runtime_index_registry()?;
740        if runtime
741            .db()
742            .store()
743            .config()
744            .resident_memory_bytes_max
745            .is_some()
746        {
747            runtime
748                .db()
749                .store()
750                .checkpoint_missing_secondary_indices()?;
751        }
752        runtime.rehydrate_kv_tags()?;
753        runtime
754            .load_probabilistic_state()
755            .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
756
757        // Phase 2.5.4: replay `tenant_tables.{table}.column` markers so
758        // tables declared via `TENANT BY (col)` survive restart. Each
759        // entry re-registers the auto-policy and flips RLS on again.
760        runtime.rehydrate_tenant_tables();
761        // Issue #593 slice 9a — replay persisted materialized-view
762        // descriptors so `CREATE MATERIALIZED VIEW v AS …` survives a
763        // restart. Runs after the system-keyed collections bootstrap
764        // and before the API opens.
765        runtime.rehydrate_materialized_view_descriptors()?;
766        runtime.recover_store_forks()?;
767        if let Some(repl) = &runtime.inner.db.replication {
768            repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
769            if let Some(spool) = &repl.logical_wal_spool {
770                spool.restore_lsn_floor(restored_cdc_lsn);
771            }
772        }
773        runtime.recover_transactional_replication()?;
774
775        // Save system info to red.config on boot
776        {
777            let sys = SystemInfo::collect();
778            let store = runtime.inner.db.store();
779            store.with_boot_system_info_update(|| {
780                store.set_config_tree(
781                    "system",
782                    &crate::serde_json::json!({
783                        "pid": sys.pid,
784                        "cpu_cores": sys.cpu_cores,
785                        "total_memory_bytes": sys.total_memory_bytes,
786                        "available_memory_bytes": sys.available_memory_bytes,
787                        "os": sys.os,
788                        "arch": sys.arch,
789                        "hostname": sys.hostname,
790                        "started_at": SystemTime::now()
791                            .duration_since(UNIX_EPOCH)
792                            .unwrap_or_default()
793                            .as_millis() as u64
794                    }),
795                )
796            });
797
798            // Seed defaults on first boot (only if red.config is empty or missing defaults)
799            let store = runtime.inner.db.store();
800            if store
801                .get_collection("red.config")
802                .map(|m| m.query_all(|_| true).len())
803                .unwrap_or(0)
804                <= 10
805            {
806                store.set_config_tree("ai", &crate::json!({
807                    "default": crate::json!({
808                        "provider": "openai",
809                        "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
810                    }),
811                    "max_embedding_inputs": 256,
812                    "max_prompt_batch": 256,
813                    "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
814                }));
815                store.set_config_tree(
816                    "server",
817                    &crate::json!({
818                        "max_scan_limit": 1000,
819                        "max_body_size": 1048576,
820                        "read_timeout_ms": 5000,
821                        "write_timeout_ms": 5000
822                    }),
823                );
824                store.set_config_tree(
825                    "storage",
826                    &crate::json!({
827                        "page_size": 4096,
828                        "page_cache_capacity": 100000,
829                        "auto_checkpoint_pages": 1000,
830                        "snapshot_retention": 16,
831                        "verify_checksums": true,
832                        "segment": crate::json!({
833                            "max_entities": 100000,
834                            "max_bytes": 268435456_u64,
835                            "compression_level": 6
836                        }),
837                        "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
838                        "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
839                        "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
840                    }),
841                );
842                store.set_config_tree(
843                    "search",
844                    &crate::json!({
845                        "rag": crate::json!({
846                            "max_chunks_per_source": 10,
847                            "max_total_chunks": 25,
848                            "similarity_threshold": 0.8,
849                            "graph_depth": 2,
850                            "min_relevance": 0.3
851                        }),
852                        "fusion": crate::json!({
853                            "vector_weight": 0.5,
854                            "graph_weight": 0.3,
855                            "table_weight": 0.2,
856                            "dedup_threshold": 0.85
857                        })
858                    }),
859                );
860                store.set_config_tree(
861                    "auth",
862                    &crate::json!({
863                        "enabled": false,
864                        "session_ttl_secs": 3600,
865                        "require_auth": false
866                    }),
867                );
868                store.set_config_tree(
869                    "query",
870                    &crate::json!({
871                        "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
872                        "max_recursion_depth": 1000
873                    }),
874                );
875                store.set_config_tree(
876                    "indexes",
877                    &crate::json!({
878                        "auto_select": true,
879                        "bloom_filter": crate::json!({
880                            "enabled": true,
881                            "false_positive_rate": 0.01,
882                            "prune_on_scan": true
883                        }),
884                        "hash": crate::json!({ "enabled": true }),
885                        "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
886                        "spatial": crate::json!({ "enabled": true })
887                    }),
888                );
889                store.set_config_tree(
890                    "probabilistic",
891                    &crate::json!({
892                        "hll_registers": 16384,
893                        "sketch_default_width": 1000,
894                        "sketch_default_depth": 5,
895                        "filter_default_capacity": 100000
896                    }),
897                );
898                store.set_config_tree(
899                    "timeseries",
900                    &crate::json!({
901                        "default_chunk_size": 1024,
902                        "compression": crate::json!({
903                            "timestamps": "delta_of_delta",
904                            "values": "gorilla_xor"
905                        }),
906                        "default_retention_days": 0
907                    }),
908                );
909                store.set_config_tree(
910                    "queue",
911                    &crate::json!({
912                        "default_max_size": 0,
913                        "default_max_attempts": 3,
914                        "visibility_timeout_ms": 30000,
915                        "consumer_idle_timeout_ms": 60000
916                    }),
917                );
918                store.set_config_tree(
919                    "backup",
920                    &crate::json!({
921                        "enabled": false,
922                        "interval_secs": 3600,
923                        "retention_count": 24,
924                        "upload": false,
925                        "backend": "local"
926                    }),
927                );
928                store.set_config_tree(
929                    "wal",
930                    &crate::json!({
931                        "archive": crate::json!({
932                            "enabled": false,
933                            "retention_hours": 168,
934                            "prefix": reddb_file::backup_wal_prefix("")
935                        })
936                    }),
937                );
938                store.set_config_tree(
939                    "cdc",
940                    &crate::json!({
941                        "enabled": true,
942                        "buffer_size": 100000
943                    }),
944                );
945                store.set_config_tree(
946                    "secret",
947                    &crate::json!({
948                        "auto_encrypt": true,
949                        "auto_decrypt": true
950                    }),
951                );
952            }
953
954            // Perf-parity config matrix: heal the Tier A (critical)
955            // keys unconditionally on every boot. Idempotent — only
956            // writes the default when the key is missing. Keeps
957            // `SHOW CONFIG` showing every guarantee the operator has
958            // (durability.mode, concurrency.locking.enabled, …) even
959            // on long-running datadirs that predate the matrix.
960            crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
961            seed_storage_deploy_config(store.as_ref(), options.storage_profile);
962
963            // Phase 5 — Lehman-Yao runtime flag. Read the Tier A
964            // `storage.btree.lehman_yao` value from the matrix (env
965            // > file > red.config > default) and publish it to the
966            // storage layer's atomic so the B-tree read / split
967            // paths can branch without re-reading the config on
968            // every hot-path call.
969            let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
970            crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
971            if lehman_yao {
972                tracing::info!(
973                    "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
974                );
975            }
976
977            // Config file overlay — mounted `/etc/reddb/config.json`
978            // (override path via REDDB_CONFIG_FILE). Writes keys with
979            // write-if-absent semantics so a later user `SET CONFIG`
980            // always wins. Missing file = silent no-op.
981            let overlay_path = crate::runtime::config_overlay::config_file_path();
982            let _ =
983                crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
984        }
985
986        // VCS ("Git for Data") — create the `red_*` metadata
987        // collections on first boot. Idempotent: `get_or_create_collection`
988        // is a no-op if the collection already exists.
989        {
990            let store = runtime.inner.db.store();
991            for name in crate::application::vcs_collections::ALL {
992                let _ = store.get_or_create_collection(*name);
993            }
994            // Seed VCS config namespace with sensible defaults on first
995            // boot, matching the pattern used by ai / storage.
996            store.set_config_tree(
997                crate::application::vcs_collections::CONFIG_NAMESPACE,
998                &crate::json!({
999                    "default_branch": "main",
1000                    "author": crate::json!({
1001                        "name": "reddb",
1002                        "email": "reddb@localhost"
1003                    }),
1004                    "protected_branches": crate::json!(["main"]),
1005                    "closure": crate::json!({
1006                        "enabled": true,
1007                        "lazy": true
1008                    }),
1009                    "merge": crate::json!({
1010                        "default_strategy": "auto",
1011                        "fast_forward": true
1012                    })
1013                }),
1014            );
1015        }
1016
1017        // Migrations — create the `red.migration.definitions` / `red.migration.dependencies`
1018        // system collections on first boot. Idempotent.
1019        {
1020            let store = runtime.inner.db.store();
1021            for name in crate::application::migration_collections::ALL {
1022                let _ = store.get_or_create_collection(*name);
1023            }
1024        }
1025
1026        // Topology graph (#803) — ensure the built-in `red.topology.cluster`
1027        // graph collection (declared WITH ANALYTICS) and its metadata sidecar
1028        // exist. Idempotent and survives restarts via the WAL-backed contract.
1029        let _ = crate::application::topology_collections::ensure(&runtime);
1030
1031        // Bootstrapping topology can publish a metadata checkpoint. Register
1032        // the optional COUNT projection only after that publication, so old
1033        // data is not backfilled before readiness. Queries scan until the next
1034        // checkpoint publishes the projection.
1035        runtime.prepare_paged_counts()?;
1036
1037        // Anonymous access is the most consequential posture an operator can
1038        // choose. Record it before reserving the user-ID floor so the boot-time
1039        // audit entity remains in the internal range.
1040        if no_auth_boot {
1041            use crate::runtime::control_events::{
1042                ActorRef, ControlEvent, ControlEventCtx, EventKind, Outcome,
1043            };
1044            let ledger = std::sync::Arc::clone(&runtime.inner.control_event_ledger.read());
1045            let ctx = ControlEventCtx {
1046                actor: ActorRef::System("boot"),
1047                scope: None,
1048                request_id: None,
1049                trace_id: None,
1050            };
1051            let event = ControlEvent {
1052                kind: EventKind::AuthDisabled,
1053                outcome: Outcome::Allowed,
1054                action: std::borrow::Cow::Borrowed("auth.disabled"),
1055                resource: Some("server".to_string()),
1056                reason: Some("--no-auth / --dev".to_string()),
1057                matched_policy_id: None,
1058                fields: std::collections::HashMap::new(),
1059            };
1060            if let Err(err) = ledger.emit(&ctx, event) {
1061                tracing::warn!(
1062                    target: "reddb::security",
1063                    error = %err,
1064                    "could not record the auth-disabled boot in the control-event ledger"
1065                );
1066            }
1067        }
1068
1069        // Start background maintenance thread (context index refresh +
1070        // session purge). Held by a WEAK reference to `RuntimeInner`
1071        // to avoid an ownership cycle. The last foreground owner unparks
1072        // and joins the worker before releasing its inner reference, so
1073        // an active weak upgrade cannot retain the pager lock after close.
1074        // The 60-second work cadence is tracked via `last_work`.
1075        //
1076        // The previous version captured `rt = runtime.clone()` by
1077        // strong reference and ran an unterminated `loop`, which held
1078        // Arc<RuntimeInner> forever — reopening a persistent database
1079        // in the same process failed with "Database is locked" because
1080        // the pager could never drop. See the regression test
1081        // `finding_1_select_after_bulk_insert_persistent_reopen`.
1082        {
1083            let weak = Arc::downgrade(&runtime.inner);
1084            runtime
1085                .background_tasks
1086                .as_ref()
1087                .expect("foreground runtime")
1088                .spawn(
1089                    std::thread::Builder::new().name("reddb-maintenance".into()),
1090                    move |stopped| {
1091                        let tick = std::time::Duration::from_millis(200);
1092                        let work_interval = std::time::Duration::from_secs(60);
1093                        let mut last_work = std::time::Instant::now();
1094                        let mut allocator_pressure =
1095                            super::allocator_pressure::AllocatorPressure::default();
1096                        loop {
1097                            std::thread::park_timeout(tick);
1098                            if stopped.load(AtomicOrdering::Acquire) {
1099                                break;
1100                            }
1101                            let Some(inner) = weak.upgrade() else {
1102                                // All strong references dropped — the
1103                                // runtime is gone, exit cleanly.
1104                                break;
1105                            };
1106                            if last_work.elapsed() >= work_interval {
1107                                let _stats = inner.db.store().context_index().stats();
1108                                last_work = std::time::Instant::now();
1109                            }
1110                            let rt = RedDBRuntime {
1111                                background_tasks: None,
1112                                inner,
1113                            };
1114                            rt.try_reclaim_history_tick();
1115                            allocator_pressure.tick();
1116                        }
1117                    },
1118                );
1119        }
1120
1121        // Blob Cache expiration is active maintenance, not a read-path side
1122        // effect. Each pulse scans a rotating bounded slice so expired keys
1123        // release their L1 budget even when they are never accessed again.
1124        // A dedicated low-priority worker keeps this work isolated from query
1125        // execution workers.
1126        {
1127            let weak = Arc::downgrade(&runtime.inner);
1128            runtime
1129                .background_tasks
1130                .as_ref()
1131                .expect("foreground runtime")
1132                .spawn(
1133                    std::thread::Builder::new().name("reddb-cache-sweeper".into()),
1134                    move |stopped| loop {
1135                        std::thread::park_timeout(std::time::Duration::from_millis(250));
1136                        if stopped.load(AtomicOrdering::Acquire) {
1137                            break;
1138                        }
1139                        let Some(inner) = weak.upgrade() else {
1140                            break;
1141                        };
1142                        let _ = crate::storage::cache::sweeper::BlobCacheSweeper::sweep_expired(
1143                            &inner.result_blob_cache,
1144                            crate::storage::cache::sweeper::SweepLimit::Either {
1145                                entries: 512,
1146                                millis: 5,
1147                            },
1148                        );
1149                        let rt = RedDBRuntime {
1150                            background_tasks: None,
1151                            inner,
1152                        };
1153                        rt.promote_volatile_result_cache_entry();
1154                    },
1155                );
1156        }
1157
1158        // Start backup scheduler if enabled via red.config
1159        {
1160            let store = runtime.inner.db.store();
1161            let mut backup_enabled = false;
1162            let mut backup_interval = 3600u64;
1163
1164            if let Some(manager) = store.get_collection("red.config") {
1165                manager.for_each_entity(|entity| {
1166                    if let Some(row) = entity.data.as_row() {
1167                        let key = row.get_field("key").and_then(|v| match v {
1168                            reddb_types::Value::Text(s) => Some(s.as_ref()),
1169                            _ => None,
1170                        });
1171                        let val = row.get_field("value");
1172                        if key == Some("backup.enabled") {
1173                            backup_enabled = match val {
1174                                Some(reddb_types::Value::Boolean(true)) => true,
1175                                Some(reddb_types::Value::Text(s)) => &**s == "true",
1176                                _ => false,
1177                            };
1178                        } else if key == Some("backup.interval_secs") {
1179                            if let Some(reddb_types::Value::Integer(n)) = val {
1180                                backup_interval = *n as u64;
1181                            }
1182                        }
1183                    }
1184                    true
1185                });
1186            }
1187
1188            if backup_enabled {
1189                runtime.inner.backup_scheduler.set_interval(backup_interval);
1190                let rt = runtime.clone();
1191                runtime
1192                    .inner
1193                    .backup_scheduler
1194                    .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
1195            }
1196        }
1197
1198        // Load EC registry from red.config and start worker
1199        {
1200            runtime
1201                .inner
1202                .ec_registry
1203                .load_from_config_store(runtime.inner.db.store().as_ref());
1204            if !runtime.inner.db.options().read_only
1205                && !runtime.inner.ec_registry.async_configs().is_empty()
1206            {
1207                runtime.inner.ec_worker.start(
1208                    Arc::clone(&runtime.inner.ec_registry),
1209                    Arc::clone(&runtime.inner.db.store()),
1210                );
1211            }
1212        }
1213
1214        if let (true, crate::replication::ReplicationRole::Replica { primary_addr }) = (
1215            runtime.inner.db.options().replica_loop_enabled
1216                && !runtime.inner.db.options().read_only,
1217            runtime.inner.db.options().replication.role.clone(),
1218        ) {
1219            let rt = runtime.clone();
1220            std::thread::Builder::new()
1221                .name("reddb-replica".into())
1222                .spawn(move || rt.run_replica_loop(primary_addr))
1223                .ok();
1224        }
1225
1226        // PLAN.md Phase 1 — Lifecycle Contract. Mark Ready once every
1227        // boot stage above has completed (WAL replay, restore-from-
1228        // remote, replica-loop spawn). Health probes flip from 503 to
1229        // 200 here; shutdown begins from this state.
1230        runtime.inner.lifecycle.mark_ready();
1231
1232        #[cfg(not(test))]
1233        if runtime
1234            .db()
1235            .store()
1236            .config()
1237            .resident_memory_bytes_max
1238            .is_some()
1239            && !runtime.db().options().read_only
1240        {
1241            runtime.start_count_projection_worker();
1242        }
1243
1244        // Issue #583 slice 10 — ContinuousMaterializedView scheduler.
1245        // Low-priority background ticker that drains the cache's
1246        // `claim_due_at` set every ~50ms. Holds only a Weak<RuntimeInner>
1247        // to avoid an ownership cycle; foreground close unparks and drains
1248        // an active refresh before releasing storage. Materialized views without
1249        // a `REFRESH EVERY` clause stay on the manual-refresh path
1250        // and are skipped by `claim_due_at`, so the loop is a no-op
1251        // when no scheduled views exist.
1252        {
1253            let weak_inner = Arc::downgrade(&runtime.inner);
1254            runtime
1255                .background_tasks
1256                .as_ref()
1257                .expect("foreground runtime")
1258                .spawn(
1259                    std::thread::Builder::new()
1260                        .name("reddb-mv-scheduler".into())
1261                        // Rust's default for spawned threads is 2 MiB, which is too
1262                        // small for the query-execution path that refresh_due_materialized_views
1263                        // runs (StatementExecutionFrame + guards per view). Match the
1264                        // 8 MiB that the OS gives the main thread.
1265                        .stack_size(8 * 1024 * 1024),
1266                    move |stopped| loop {
1267                        std::thread::park_timeout(std::time::Duration::from_millis(50));
1268                        if stopped.load(AtomicOrdering::Acquire) {
1269                            break;
1270                        }
1271                        let Some(inner) = weak_inner.upgrade() else {
1272                            break;
1273                        };
1274                        let rt = RedDBRuntime {
1275                            background_tasks: None,
1276                            inner,
1277                        };
1278                        rt.refresh_due_materialized_views();
1279                    },
1280                );
1281        }
1282
1283        // Issue #584 slice 12 — DeclarativeRetention background sweeper.
1284        // Low-priority ticker that physically reclaims rows whose
1285        // timestamp has fallen beyond the retention window. Holds a
1286        // `Weak<RuntimeInner>`; foreground close drains an active tick
1287        // before releasing storage (shutdown leaves storage consistent
1288        // because each tick goes through the standard DELETE path —
1289        // there is no half-finished mutation state to clean up). The
1290        // tick interval is intentionally longer than the MV scheduler
1291        // (500ms) because retention is order-of-seconds at minimum.
1292        if !runtime.write_gate().is_read_only() {
1293            let weak_inner = Arc::downgrade(&runtime.inner);
1294            runtime
1295                .background_tasks
1296                .as_ref()
1297                .expect("foreground runtime")
1298                .spawn(
1299                    std::thread::Builder::new().name("reddb-retention-sweeper".into()),
1300                    move |stopped| loop {
1301                        std::thread::park_timeout(std::time::Duration::from_millis(500));
1302                        if stopped.load(AtomicOrdering::Acquire) {
1303                            break;
1304                        }
1305                        let Some(inner) = weak_inner.upgrade() else {
1306                            break;
1307                        };
1308                        let rt = RedDBRuntime {
1309                            background_tasks: None,
1310                            inner,
1311                        };
1312                        rt.sweep_retention_tick(
1313                            crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
1314                        );
1315                        rt.sweep_individual_ttl_tick(
1316                            crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
1317                        );
1318                    },
1319                );
1320        }
1321
1322        // #1369 — reserve the fixed internal-id floor only after every boot
1323        // stage has finished. Some persistent-only startup paths publish
1324        // internal entities, so reserving it earlier lets those paths consume
1325        // the first documented user `rid`. `register_entity_id` only raises
1326        // the allocator, leaving databases already past the floor untouched.
1327        runtime
1328            .inner
1329            .db
1330            .store()
1331            .register_entity_id(crate::storage::EntityId::new(
1332                crate::storage::FIRST_USER_ENTITY_ID - 1,
1333            ));
1334
1335        Ok(runtime)
1336    }
1337
1338    fn rehydrate_snapshot_xid_floor(&self) -> RedDBResult<()> {
1339        let store = self.inner.db.store();
1340        let snapshot_manager = self.inner.transaction_state.snapshot_manager();
1341        let base_high_water_mark = store.startup_xid_high_water_mark();
1342        if let Some(high_water_mark) = base_high_water_mark {
1343            snapshot_manager.observe_committed_xid(high_water_mark);
1344        }
1345        for collection in store.list_collections() {
1346            let Some(manager) = store.get_collection(&collection) else {
1347                continue;
1348            };
1349            let observe = |entity: &crate::storage::UnifiedEntity| {
1350                snapshot_manager.observe_committed_xid(entity.xmin);
1351                snapshot_manager.observe_committed_xid(entity.xmax);
1352                true
1353            };
1354            if base_high_water_mark.is_some() {
1355                // The catalog covers the exact immutable roots. Committed WAL
1356                // replay remains resident and must still advance the floor.
1357                manager.for_each_entity(observe);
1358            } else {
1359                // Older or unproven generations require complete physical
1360                // validation, one bounded record at a time, before filtering.
1361                manager.try_for_each_entity(usize::MAX, observe)?;
1362            }
1363        }
1364        if base_high_water_mark.is_none() {
1365            store.remember_startup_xid_high_water_mark(
1366                snapshot_manager.peek_next_xid().saturating_sub(1),
1367            );
1368        }
1369        Ok(())
1370    }
1371
1372    /// Provision an empty Table-shaped collection that backs a
1373    /// `CREATE MATERIALIZED VIEW v` (issue #594 slice 9b of #575).
1374    /// `SELECT FROM v` reads this collection directly; the rewriter is
1375    /// configured to skip materialized views so the body is no longer
1376    /// substituted. REFRESH still writes to the cache slot — wiring it
1377    /// into this backing collection is the job of slice 9c.
1378    ///
1379    /// Idempotent: re-running for the same name leaves the existing
1380    /// collection in place (mirrors `CREATE TABLE IF NOT EXISTS`
1381    /// semantics). This keeps `CREATE OR REPLACE MATERIALIZED VIEW v`
1382    /// cheap — the body change does not invalidate already-buffered
1383    /// rows. Until 9c lands the backing is always empty anyway.
1384    pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1385        let store = self.inner.db.store();
1386        let mut changed = false;
1387        if store.get_collection(name).is_none() {
1388            store.get_or_create_collection(name);
1389            changed = true;
1390        }
1391        if self.inner.db.collection_contract(name).is_none() {
1392            self.inner
1393                .db
1394                .save_collection_contract(system_keyed_collection_contract(
1395                    name,
1396                    crate::catalog::CollectionModel::Table,
1397                ))
1398                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1399            changed = true;
1400        }
1401        if changed {
1402            self.inner
1403                .db
1404                .persist_metadata()
1405                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1406        }
1407        Ok(())
1408    }
1409
1410    /// Inverse of [`ensure_materialized_view_backing`] — drops the
1411    /// backing collection on `DROP MATERIALIZED VIEW v`. No-op when
1412    /// the collection was never created (e.g. a `DROP MATERIALIZED
1413    /// VIEW IF EXISTS v` against an unknown name).
1414    pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1415        let store = self.inner.db.store();
1416        if store.get_collection(name).is_none() {
1417            return Ok(());
1418        }
1419        store
1420            .drop_collection(name)
1421            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1422        // The contract may have been dropped already (DROP TABLE path)
1423        // — ignore "not found" errors by checking presence first.
1424        if self.inner.db.collection_contract(name).is_some() {
1425            self.inner
1426                .db
1427                .remove_collection_contract(name)
1428                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1429        }
1430        self.invalidate_result_cache();
1431        self.inner
1432            .db
1433            .persist_metadata()
1434            .map_err(|err| RedDBError::Internal(err.to_string()))?;
1435        Ok(())
1436    }
1437
1438    fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
1439        let mut changed = false;
1440        for (name, model) in [
1441            ("red.config", crate::catalog::CollectionModel::Config),
1442            ("red.vault", crate::catalog::CollectionModel::Vault),
1443            // Issue #593 — materialized-view catalog. One row per
1444            // `CREATE MATERIALIZED VIEW`; rehydrated at boot before
1445            // the API opens.
1446            (
1447                crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
1448                crate::catalog::CollectionModel::Config,
1449            ),
1450        ] {
1451            if self.inner.db.store().get_collection(name).is_none() {
1452                self.inner.db.store().get_or_create_collection(name);
1453                changed = true;
1454            }
1455            if self.inner.db.collection_contract(name).is_none() {
1456                let contract = system_keyed_collection_contract(name, model);
1457                if self.inner.db.options().read_only {
1458                    self.inner.db.cache_read_only_bootstrap_contract(contract);
1459                } else {
1460                    self.inner
1461                        .db
1462                        .save_collection_contract(contract)
1463                        .map_err(|err| RedDBError::Internal(err.to_string()))?;
1464                }
1465                changed = true;
1466            }
1467        }
1468        if changed {
1469            self.inner
1470                .db
1471                .persist_metadata()
1472                .map_err(|err| RedDBError::Internal(err.to_string()))?;
1473        }
1474        Ok(())
1475    }
1476
1477    pub fn db(&self) -> Arc<RedDB> {
1478        Arc::clone(&self.inner.db)
1479    }
1480
1481    /// Direct access to the runtime's secondary-index store.
1482    /// Used by bulk-insert entry points (gRPC binary bulk, HTTP bulk,
1483    /// wire bulk) that need to push new rows through the per-index
1484    /// maintenance hook after `store.bulk_insert` returns.
1485    pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
1486        &self.inner.index_store
1487    }
1488
1489    /// Apply a DDL event to the schema-vocabulary reverse index
1490    /// (issue #120). Called by DDL execution paths after the catalog
1491    /// mutation has succeeded so the index never holds entries for
1492    /// half-applied DDL.
1493    pub(crate) fn schema_vocabulary_apply(
1494        &self,
1495        event: crate::runtime::schema_vocabulary::DdlEvent,
1496    ) {
1497        self.inner.schema_vocabulary.write().on_ddl(event.clone());
1498        self.schedule_collection_description_enrichment(&event);
1499    }
1500
1501    pub(crate) fn schema_vocabulary_sync_descriptions(&self, collection: &str) {
1502        let Some(contract) = self.inner.db.collection_contract(collection) else {
1503            return;
1504        };
1505        let columns = contract
1506            .declared_columns
1507            .iter()
1508            .filter_map(|column| {
1509                column
1510                    .description
1511                    .as_ref()
1512                    .map(|description| (column.name.clone(), description.clone()))
1513            })
1514            .collect::<Vec<_>>();
1515        self.inner.schema_vocabulary.write().update_descriptions(
1516            collection,
1517            contract.description.as_deref(),
1518            &columns,
1519        );
1520    }
1521
1522    /// Lookup `token` in the schema-vocabulary reverse index. Returns
1523    /// an owned `Vec<VocabHit>` because the underlying read lock
1524    /// cannot be borrowed across the call boundary; the slice from
1525    /// `SchemaVocabulary::lookup` is cloned per hit.
1526    pub fn schema_vocabulary_lookup(
1527        &self,
1528        token: &str,
1529    ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
1530        self.inner.schema_vocabulary.read().lookup(token).to_vec()
1531    }
1532
1533    /// Inject an AuthStore into the runtime. Called by server boot
1534    /// after the vault has been bootstrapped, so that `Value::Secret`
1535    /// auto-encrypt/decrypt can reach the vault AES key.
1536    pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
1537        *self.inner.auth_store.write() = Some(store);
1538    }
1539
1540    /// Snapshot the current AuthStore (if any). Used by the wire listener
1541    /// to validate bearer tokens issued via HTTP `/auth/login`.
1542    pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
1543        self.inner.auth_store.read().clone()
1544    }
1545
1546    /// Inject an `OAuthValidator` into the runtime. When set, HTTP and
1547    /// wire transports try OAuth JWT validation before falling back to
1548    /// the local AuthStore lookup. Pass `None` to disable.
1549    pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
1550        *self.inner.oauth_validator.write() = validator;
1551    }
1552
1553    /// Returns a clone of the configured `OAuthValidator` Arc, if any.
1554    /// Hot path: called per HTTP request when an Authorization header
1555    /// is present, so we hand back a cheap Arc clone.
1556    pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
1557        self.inner.oauth_validator.read().clone()
1558    }
1559
1560    /// Inject the browser-token authority (issue #936). When set, the
1561    /// RedWire WS handshake accepts the short-lived access JWT it mints
1562    /// (alongside, and tried before, the federated OAuth validator), and
1563    /// the `/auth/browser/*` HTTP endpoints can issue/rotate the pair.
1564    /// `None` leaves the browser credential flow inert.
1565    pub fn set_browser_token_authority(
1566        &self,
1567        authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
1568    ) {
1569        *self.inner.browser_token_authority.write() = authority;
1570    }
1571
1572    /// Snapshot the browser-token authority, if wired. Read on the WS
1573    /// handshake path and by the `/auth/browser/*` handlers; a cheap Arc
1574    /// clone keeps the lock hold short.
1575    pub fn browser_token_authority(
1576        &self,
1577    ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
1578        self.inner.browser_token_authority.read().clone()
1579    }
1580}
1581
1582#[cfg(test)]
1583mod cache_boot_tests {
1584    use super::*;
1585
1586    #[test]
1587    fn persisted_cache_limits_apply_on_next_boot_within_memory_budget() {
1588        let directory = tempfile::tempdir().expect("cache config fixture");
1589        let options = RedDBOptions::persistent(directory.path().join("cache-config.rdb"))
1590            .with_memory_budget(64 * 1024 * 1024);
1591        let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1592        runtime.db().store().set_config_tree(
1593            "cache.blob",
1594            &crate::json!({
1595                "l1_bytes_max": 1024 * 1024,
1596                "l2_bytes_max": 2 * 1024 * 1024,
1597                "namespace_bytes_max": 256 * 1024,
1598                "max_namespaces": 7
1599            }),
1600        );
1601        runtime.flush().expect("persist cache config");
1602        drop(runtime);
1603
1604        let reopened = RedDBRuntime::with_options(options).expect("configured runtime");
1605        let stats = reopened.result_blob_cache().stats();
1606        assert_eq!(stats.l1_bytes_max(), 1024 * 1024);
1607        assert_eq!(stats.l2_bytes_max(), 2 * 1024 * 1024);
1608        assert_eq!(stats.namespace_bytes_max(), 256 * 1024);
1609        assert_eq!(stats.max_namespaces(), 7);
1610    }
1611
1612    #[test]
1613    fn single_file_l2_survives_runtime_restart_without_sidecar() {
1614        let directory = tempfile::tempdir().expect("single-file L2 fixture");
1615        let path = directory.path().join("cache-l2.rdb");
1616        let options = RedDBOptions::persistent(&path);
1617        let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1618        runtime
1619            .result_blob_cache()
1620            .put(
1621                "tenant",
1622                "key",
1623                crate::storage::cache::BlobCachePut::new(b"durable".to_vec()).with_policy(
1624                    crate::storage::cache::BlobCachePolicy::default()
1625                        .l1_admission(crate::storage::cache::L1Admission::Never),
1626                ),
1627            )
1628            .expect("write internal L2");
1629        assert!(runtime.result_blob_cache().l2_path().is_none());
1630        runtime.flush().expect("checkpoint internal L2");
1631        drop(runtime);
1632
1633        let reopened = RedDBRuntime::with_options(options).expect("reopen runtime");
1634        assert_eq!(
1635            reopened
1636                .result_blob_cache()
1637                .get("tenant", "key")
1638                .expect("L2 hit after restart")
1639                .bytes()
1640                .as_ref(),
1641            b"durable"
1642        );
1643        drop(reopened);
1644        let siblings: Vec<_> = std::fs::read_dir(directory.path())
1645            .expect("list fixture")
1646            .filter_map(Result::ok)
1647            .map(|entry| entry.file_name().to_string_lossy().into_owned())
1648            .collect();
1649        assert_eq!(siblings, vec!["cache-l2.rdb"]);
1650    }
1651
1652    #[test]
1653    fn single_file_l2_deadline_survives_restart_and_sweeps_atomically() {
1654        let directory = tempfile::tempdir().expect("single-file L2 TTL fixture");
1655        let path = directory.path().join("cache-l2-ttl.rdb");
1656        let options = RedDBOptions::persistent(&path);
1657        let mut runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1658        drop(runtime.background_tasks.take());
1659        runtime
1660            .result_blob_cache()
1661            .put(
1662                "tenant",
1663                "expired",
1664                crate::storage::cache::BlobCachePut::new(b"expired".to_vec()).with_policy(
1665                    crate::storage::cache::BlobCachePolicy::default()
1666                        .expires_at_unix_ms(1)
1667                        .l1_admission(crate::storage::cache::L1Admission::Never),
1668                ),
1669            )
1670            .expect("write expiring L2 entry");
1671        runtime.flush().expect("checkpoint L2 deadline");
1672        drop(runtime);
1673
1674        let mut reopened = RedDBRuntime::with_options(options).expect("reopen runtime");
1675        drop(reopened.background_tasks.take());
1676        let manager = reopened
1677            .db()
1678            .store()
1679            .get_collection(crate::storage::cache::blob::INTERNAL_L2_COLLECTION)
1680            .expect("L2 collection");
1681        assert_eq!(manager.scheduled_metadata_expiry_count(), 1);
1682        let bytes_before = reopened.result_blob_cache().stats().l2_bytes_in_use();
1683        assert!(bytes_before > 0);
1684
1685        // Generic row TTL must not bypass the cache's atomic byte-counter
1686        // update. The dedicated cache sweeper owns this collection.
1687        assert_eq!(reopened.sweep_individual_ttl_tick_at(64, u64::MAX), 0);
1688        assert_eq!(
1689            reopened.result_blob_cache().stats().l2_bytes_in_use(),
1690            bytes_before
1691        );
1692        let report = crate::storage::cache::BlobCacheSweeper::sweep_expired(
1693            reopened.result_blob_cache(),
1694            crate::storage::cache::SweepLimit::Entries(1),
1695        );
1696        assert_eq!(report.entries_evicted, 1);
1697        assert_eq!(reopened.result_blob_cache().stats().l2_bytes_in_use(), 0);
1698        assert_eq!(manager.scheduled_metadata_expiry_count(), 0);
1699    }
1700
1701    #[test]
1702    fn expired_l2_payload_pages_are_reused() {
1703        let directory = tempfile::tempdir().expect("L2 page reuse fixture");
1704        let path = directory.path().join("cache-l2-reuse.rdb");
1705        let options = RedDBOptions::persistent(&path)
1706            .with_memory_budget(32 * 1024 * 1024)
1707            .with_bounded_disk_residency(true);
1708        let mut runtime = RedDBRuntime::with_options(options.clone()).expect("runtime");
1709        drop(runtime.background_tasks.take());
1710        let mut state = 0x9e37_79b9_7f4a_7c15u64;
1711        let payload: Vec<u8> = std::iter::repeat_with(|| {
1712            state ^= state << 13;
1713            state ^= state >> 7;
1714            state ^= state << 17;
1715            state.to_le_bytes()[0]
1716        })
1717        .take(64 * 1024)
1718        .collect();
1719        for id in 0..8 {
1720            runtime
1721                .result_blob_cache()
1722                .put(
1723                    "reuse",
1724                    format!("expired-{id}"),
1725                    crate::storage::cache::BlobCachePut::new(payload.clone()).with_policy(
1726                        crate::storage::cache::BlobCachePolicy::default()
1727                            .expires_at_unix_ms(1)
1728                            .l1_admission(crate::storage::cache::L1Admission::Never),
1729                    ),
1730                )
1731                .expect("expired L2 payload");
1732        }
1733        runtime.checkpoint().expect("initial L2 checkpoint");
1734        let initial_pages = runtime
1735            .db()
1736            .store()
1737            .pager()
1738            .expect("pager")
1739            .page_count()
1740            .expect("initial page count");
1741        drop(runtime);
1742
1743        let mut runtime = RedDBRuntime::with_options(options).expect("reopened runtime");
1744        drop(runtime.background_tasks.take());
1745        let store = runtime.db().store();
1746        let pager = store.pager().expect("pager");
1747        let report = crate::storage::cache::BlobCacheSweeper::sweep_expired(
1748            runtime.result_blob_cache(),
1749            crate::storage::cache::SweepLimit::Entries(64),
1750        );
1751        assert_eq!(report.entries_evicted, 8);
1752        runtime.checkpoint().expect("expired L2 checkpoint");
1753
1754        let mut cursor = store
1755            .mvcc_reclaim_cursor(crate::storage::cache::blob::INTERNAL_L2_COLLECTION.to_string())
1756            .expect("L2 reclaim cursor");
1757        for _ in 0..64 {
1758            store
1759                .reclaim_mvcc_tick(
1760                    &mut cursor,
1761                    &runtime.snapshot_manager(),
1762                    runtime.mvcc_vacuum_cutoff_xid(),
1763                    64,
1764                    8,
1765                )
1766                .expect("L2 MVCC reclaim");
1767            if cursor.finished() {
1768                break;
1769            }
1770        }
1771        assert!(cursor.finished());
1772        drop(cursor);
1773        for _ in 0..3 {
1774            runtime.checkpoint().expect("publish reusable L2 pages");
1775        }
1776        let free_before_reuse = pager.free_page_count().expect("free pages");
1777
1778        for id in 0..8 {
1779            runtime
1780                .result_blob_cache()
1781                .put(
1782                    "reuse",
1783                    format!("replacement-{id}"),
1784                    crate::storage::cache::BlobCachePut::new(payload.clone()).with_policy(
1785                        crate::storage::cache::BlobCachePolicy::default()
1786                            .expires_at_unix_ms(u64::MAX)
1787                            .l1_admission(crate::storage::cache::L1Admission::Never),
1788                    ),
1789                )
1790                .expect("replacement L2 payload");
1791        }
1792        runtime.checkpoint().expect("replacement L2 checkpoint");
1793        let pages_after_reuse = pager.page_count().expect("page count after reuse");
1794        assert!(
1795            pages_after_reuse <= initial_pages.saturating_add(16),
1796            "expired L2 pages were not reused: initial={initial_pages}, free_before={free_before_reuse}, after={pages_after_reuse}"
1797        );
1798    }
1799
1800    #[test]
1801    fn zero_cache_namespace_limit_is_rejected_on_boot() {
1802        let directory = tempfile::tempdir().expect("cache config fixture");
1803        let options = RedDBOptions::persistent(directory.path().join("cache-config-zero.rdb"));
1804        let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1805        runtime
1806            .db()
1807            .store()
1808            .set_config_tree("cache.blob.max_namespaces", &crate::json!(0));
1809        runtime.flush().expect("persist cache config");
1810        drop(runtime);
1811
1812        let error = match RedDBRuntime::with_options(options) {
1813            Ok(_) => panic!("zero limit must fail boot"),
1814            Err(error) => error,
1815        };
1816        assert!(error
1817            .to_string()
1818            .contains("cache.blob.max_namespaces must be greater than zero"));
1819    }
1820
1821    #[test]
1822    fn zero_cache_namespace_memory_limit_is_rejected_on_boot() {
1823        let directory = tempfile::tempdir().expect("cache config fixture");
1824        let options = RedDBOptions::persistent(directory.path().join("cache-bytes-zero.rdb"));
1825        let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1826        runtime
1827            .db()
1828            .store()
1829            .set_config_tree("cache.blob.namespace_bytes_max", &crate::json!(0));
1830        runtime.flush().expect("persist cache config");
1831        drop(runtime);
1832
1833        let error = match RedDBRuntime::with_options(options) {
1834            Ok(_) => panic!("zero namespace memory limit must fail boot"),
1835            Err(error) => error,
1836        };
1837        assert!(error
1838            .to_string()
1839            .contains("cache.blob.namespace_bytes_max must be greater than zero"));
1840    }
1841}
1842
1843#[cfg(test)]
1844mod xid_resume_tests {
1845    use super::*;
1846    use reddb_engine::storage::collection_read::{CollectionCursor, CollectionRead, EncodedRecord};
1847
1848    struct Unreadable;
1849
1850    fn unavailable() -> crate::storage::StoreError {
1851        crate::storage::StoreError::Io(std::io::Error::other("XID bootstrap read a base payload"))
1852    }
1853
1854    impl CollectionRead for Unreadable {
1855        fn get_record(
1856            &self,
1857            id: crate::storage::EntityId,
1858            _: usize,
1859        ) -> Result<Option<EncodedRecord>, crate::storage::StoreError> {
1860            if id.raw() == 999 {
1861                Ok(None)
1862            } else {
1863                Err(unavailable())
1864            }
1865        }
1866        fn cursor(&self) -> Result<Box<dyn CollectionCursor + '_>, crate::storage::StoreError> {
1867            Err(unavailable())
1868        }
1869        fn owned_cursor(&self) -> Result<Box<dyn CollectionCursor>, crate::storage::StoreError> {
1870            Err(unavailable())
1871        }
1872    }
1873
1874    #[test]
1875    fn xid_resume_survives_pending_checkpoint_process_exit() {
1876        const FIXTURE: &str = "REDDB_XID_RESUME_CRASH_FIXTURE";
1877        let open = |path: std::path::PathBuf| {
1878            let options = RedDBOptions::persistent(path)
1879                .with_memory_budget(64 * 1024 * 1024)
1880                .with_storage_profile(
1881                    crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
1882                )
1883                .expect("profile");
1884            crate::storage::RedDB::with_disk_residency_for_qualification(|| {
1885                RedDBRuntime::with_options(options)
1886            })
1887            .expect("crash recovery runtime")
1888        };
1889        if let Some(root) = std::env::var_os(FIXTURE) {
1890            let root = std::path::PathBuf::from(root);
1891            assert_eq!(
1892                std::fs::read_to_string(root.join("owned-fixture")).expect("owned fixture"),
1893                "xid-resume-crash-v1"
1894            );
1895            let runtime = open(root.join("crash.rdb"));
1896            for sql in [
1897                "CREATE TABLE crash_rows (id INTEGER, payload TEXT)",
1898                "INSERT INTO crash_rows (id, payload) VALUES (1, 'committed')",
1899                "BEGIN",
1900                "UPDATE crash_rows SET payload = 'pending' WHERE id = 1",
1901                "INSERT INTO crash_rows (id, payload) VALUES (2, 'pending')",
1902            ] {
1903                runtime.execute_query(sql).expect("crash fixture statement");
1904            }
1905            runtime
1906                .flush()
1907                .expect("checkpoint with pending transaction");
1908            std::fs::write(
1909                root.join("checkpoint-complete"),
1910                "pending transaction remains open",
1911            )
1912            .expect("acknowledged fixture checkpoint");
1913            // Deliberately skip all runtime destructors and rollback. The
1914            // parent must recover the committed preimage from the native file.
1915            std::process::exit(0);
1916        }
1917        let directory = tempfile::tempdir().expect("owned crash fixture");
1918        std::fs::write(
1919            directory.path().join("owned-fixture"),
1920            "xid-resume-crash-v1",
1921        )
1922        .expect("ownership marker");
1923        let child = std::process::Command::new(std::env::current_exe().expect("test executable"))
1924            .args([
1925                "--exact",
1926                "runtime::impl_lifecycle::xid_resume_tests::xid_resume_survives_pending_checkpoint_process_exit",
1927                "--test-threads=1",
1928            ])
1929            .env(FIXTURE, directory.path())
1930            .output()
1931            .expect("crashing owned child");
1932        assert!(
1933            child.status.success(),
1934            "{}",
1935            String::from_utf8_lossy(&child.stderr)
1936        );
1937        assert!(directory.path().join("checkpoint-complete").is_file());
1938        let path = directory.path().join("crash.rdb");
1939        assert!(path.is_file());
1940        let runtime = open(path.clone());
1941        let result = runtime
1942            .execute_query("SELECT id, payload FROM crash_rows ORDER BY id")
1943            .expect("committed preimage");
1944        assert_eq!(
1945            result.result.records.len(),
1946            1,
1947            "pending insert must stay invisible"
1948        );
1949        assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
1950        assert_eq!(
1951            result.result.records[0].get("payload"),
1952            Some(&Value::text("committed"))
1953        );
1954        runtime
1955            .execute_query("INSERT INTO crash_rows (id, payload) VALUES (3, 'after')")
1956            .expect("new writer after recovery");
1957        runtime.flush().expect("recovered checkpoint");
1958        drop(runtime);
1959        let reopened = open(path);
1960        let result = reopened
1961            .execute_query("SELECT id FROM crash_rows ORDER BY id")
1962            .expect("second recovery");
1963        assert_eq!(result.result.records.len(), 2);
1964        assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
1965        assert_eq!(result.result.records[1].get("id"), Some(&Value::Integer(3)));
1966    }
1967
1968    #[test]
1969    fn xid_resume_skips_known_base_payloads_but_rejects_an_unproven_root() {
1970        let directory = tempfile::tempdir().expect("resume fixture");
1971        let options = RedDBOptions::persistent(directory.path().join("resume.rdb"))
1972            .with_memory_budget(64 * 1024 * 1024)
1973            .with_storage_profile(
1974                crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
1975            )
1976            .expect("profile");
1977        let runtime = crate::storage::RedDB::with_disk_residency_for_qualification(|| {
1978            RedDBRuntime::with_options(options)
1979        })
1980        .expect("runtime");
1981        runtime
1982            .execute_query("CREATE TABLE resume_rows (id INTEGER)")
1983            .expect("table");
1984        runtime
1985            .execute_query("INSERT INTO resume_rows (id) VALUES (1)")
1986            .expect("row");
1987        runtime.flush().expect("publish complete XID summary");
1988        let store = runtime.db().store();
1989        assert!(store.startup_xid_high_water_mark().is_some());
1990        let root = store
1991            .collection_root_page("resume_rows")
1992            .expect("published root");
1993        let manager = store.get_collection("resume_rows").expect("manager");
1994        for (candidate, proven) in [(root, true), (root + 1, false)] {
1995            manager.install_backing(
1996                Arc::new(Unreadable),
1997                candidate,
1998                store.format_version(),
1999                4096,
2000                1,
2001            );
2002            let mut overlay = crate::storage::UnifiedEntity::table_row(
2003                crate::storage::EntityId::new(999),
2004                "resume_rows",
2005                999,
2006                vec![Value::Integer(2)],
2007            );
2008            overlay.xmin = 500_000;
2009            overlay.xmax = 600_000;
2010            manager.insert(overlay).expect("resident WAL overlay");
2011            let result = runtime.rehydrate_snapshot_xid_floor();
2012            assert_eq!(result.is_ok(), proven, "root={candidate}: {result:?}");
2013            if proven {
2014                assert!(runtime.snapshot_manager().peek_next_xid() > 600_000);
2015            }
2016        }
2017    }
2018}