Skip to main content

reddb_server/runtime/
impl_lifecycle.rs

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