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