1use 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
32pub(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 pub fn lock_manager(&self) -> std::sync::Arc<crate::runtime::lock_manager::LockManager> {
152 self.inner.lock_manager.clone()
153 }
154
155 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 pub fn memory_budget(&self) -> crate::storage::memory_budget::MemoryBudget {
195 self.inner.memory_budget
196 }
197
198 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 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 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 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 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 .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 .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_cache: parking_lot::RwLock::new((
381 HashMap::new(),
382 std::collections::VecDeque::new(),
383 )),
384 result_blob_cache,
385 result_blob_entries: parking_lot::RwLock::new((
386 HashMap::new(),
387 std::collections::VecDeque::new(),
388 )),
389 ask_answer_cache_entries: parking_lot::RwLock::new((
390 HashSet::new(),
391 std::collections::VecDeque::new(),
392 )),
393 result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
394 result_cache_hits: std::sync::atomic::AtomicU64::new(0),
395 result_cache_misses: std::sync::atomic::AtomicU64::new(0),
396 result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
397 ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
398 queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
399 rmw_locks: RmwLockTable::new(),
400 planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
401 ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
402 config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
403 ec_worker: crate::ec::worker::EcWorker::new(),
404 auth_store: parking_lot::RwLock::new(None),
405 oauth_validator: parking_lot::RwLock::new(None),
406 browser_token_authority: parking_lot::RwLock::new(None),
407 views: parking_lot::RwLock::new(HashMap::new()),
408 materialized_views: parking_lot::RwLock::new(
409 crate::storage::cache::result::MaterializedViewCache::new(),
410 ),
411 retention_sweeper: parking_lot::RwLock::new(
412 crate::runtime::retention_sweeper::RetentionSweeperState::new(),
413 ),
414 transaction_state: crate::runtime::transaction_state::TransactionState::new(),
415 env_config_overrides,
416 lock_manager: Arc::new({
417 let env = crate::runtime::config_overlay::collect_env_overrides();
422 let timeout_ms = env
423 .get("concurrency.locking.deadlock_timeout_ms")
424 .and_then(|raw| raw.parse::<u64>().ok())
425 .unwrap_or_else(|| {
426 match crate::runtime::config_matrix::default_for(
427 "concurrency.locking.deadlock_timeout_ms",
428 ) {
429 Some(crate::serde_json::Value::Number(n)) => n as u64,
430 _ => 5000,
431 }
432 });
433 let cfg = crate::runtime::lock_manager::LockConfig {
434 default_timeout: std::time::Duration::from_millis(timeout_ms),
435 ..Default::default()
436 };
437 crate::runtime::lock_manager::LockManager::new(cfg)
438 }),
439 rls_policies: parking_lot::RwLock::new(HashMap::new()),
440 rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
441 foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
442 pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
443 pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
444 pending_queue_dedup: parking_lot::RwLock::new(HashMap::new()),
445 pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
446 pending_vault_writes: parking_lot::RwLock::new(HashMap::new()),
447 pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
448 pending_logical_records: parking_lot::Mutex::new(HashMap::new()),
449 pending_commit_policies: parking_lot::Mutex::new(HashMap::new()),
450 logical_publication: parking_lot::ReentrantMutex::new(()),
451 logical_allocated_lsn: std::sync::atomic::AtomicU64::new(0),
452 pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
453 queue_wait_registry: std::sync::Arc::new(
454 crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
455 ),
456 pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
457 tenant_tables: parking_lot::RwLock::new(HashMap::new()),
458 ddl_epoch: std::sync::atomic::AtomicU64::new(0),
459 write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
460 &options,
461 )),
462 lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
463 resource_limits,
464 query_budget_slots: AtomicU64::new(0),
465 audit_log: {
466 let data_path = if embedded_single_file || options.read_only {
482 std::env::temp_dir()
483 .join("reddb-embedded-runtime")
484 .join(format!("audit-{}", std::process::id()))
485 } else {
486 options
487 .data_path
488 .clone()
489 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
490 };
491 let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
492 let audit_dest = if options.read_only {
493 crate::storage::layout::LogDestination::Stderr
494 } else {
495 audit_dest
496 };
497 if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
498 && (embedded_single_file
499 || options.read_only
500 || options
501 .metadata
502 .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
503 {
504 let audit_path = reddb_file::layout::sibling_path(
514 &data_path,
515 &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
516 );
517 Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
518 audit_path,
519 ))
520 } else {
521 Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
522 &audit_dest,
523 &data_path,
524 ))
525 }
526 },
527 control_event_ledger: parking_lot::RwLock::new(Arc::new(
528 crate::runtime::control_events::RuntimeLedger::new(db.store()),
529 )),
530 control_event_config: options.control_events,
531 query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
532 db.store(),
533 options.query_audit.clone(),
534 )),
535 lease_lifecycle: std::sync::OnceLock::new(),
536 routing_primary_endpoints: parking_lot::RwLock::new(Default::default()),
537 replica_apply_metrics: std::sync::Arc::new(
538 crate::replication::logical::ReplicaApplyMetrics::default(),
539 ),
540 replica_link_metrics: std::sync::Arc::new(
541 crate::replication::reconnect::ReplicaLinkMetrics::default(),
542 ),
543 quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
544 schema_vocabulary: parking_lot::RwLock::new(
545 crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
546 ),
547 slow_query_logger: {
548 let fallback_dir = if embedded_single_file || options.read_only {
565 std::env::temp_dir()
566 .join("reddb-embedded-runtime")
567 .join(format!("slow-{}", std::process::id()))
568 } else {
569 options
570 .data_path
571 .as_ref()
572 .and_then(|p| p.parent().map(std::path::PathBuf::from))
573 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
574 };
575 let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
576 .ok()
577 .and_then(|s| s.parse::<u64>().ok())
578 .unwrap_or(1000);
579 let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
580 .ok()
581 .and_then(|s| s.parse::<u8>().ok())
582 .unwrap_or(100);
583 let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
584 let slow_dest = if options.read_only {
585 crate::storage::layout::LogDestination::Stderr
586 } else {
587 slow_dest
588 };
589 crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
590 &slow_dest,
591 &fallback_dir,
592 threshold_ms,
593 sample_pct,
594 )
595 },
596 slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
597 crate::telemetry::slow_query_store::DEFAULT_CAP,
598 ),
599 kv_stats: crate::runtime::KvStatsCounters::default(),
600 metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
601 metrics_tenant_activity_stats:
602 crate::runtime::MetricsTenantActivityCounters::default(),
603 claim_telemetry: Arc::new(
604 crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
605 ),
606 queue_telemetry: Arc::new(
607 crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
608 ),
609 query_latency_telemetry: Arc::new(
610 crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
611 ),
612 occupancy_sampler: Arc::new(
613 crate::runtime::occupancy_sampler::OccupancySampler::new(),
614 ),
615 node_load_telemetry: Arc::new(
616 crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
617 ),
618 queue_presence: Arc::new(
619 crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
620 ),
621 vector_introspection: Arc::new(
622 crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
623 ),
624 kv_tag_index: crate::runtime::KvTagIndex::default(),
625 chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
626 chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
627 integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
628 integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
629 }),
630 };
631
632 runtime.install_hash_candidate_admission();
633
634 crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
640 &runtime.inner.audit_log,
641 ));
642
643 runtime
646 .inner
647 .slow_query_logger
648 .attach_store(Arc::clone(&runtime.inner.slow_query_store));
649
650 runtime
658 .inner
659 .lifecycle
660 .set_restore_started_at_ms(boot_open_start_ms);
661 runtime
662 .inner
663 .lifecycle
664 .set_restore_ready_at_ms(storage_ready_ms);
665 runtime
666 .inner
667 .lifecycle
668 .set_wal_replay_started_at_ms(boot_open_start_ms);
669 runtime
670 .inner
671 .lifecycle
672 .set_wal_replay_ready_at_ms(storage_ready_ms);
673
674 let restored_cdc_lsn = runtime
675 .inner
676 .db
677 .replication
678 .as_ref()
679 .map(|repl| {
680 repl.logical_wal_spool
681 .as_ref()
682 .map(|spool| spool.current_lsn())
683 .unwrap_or(0)
684 })
685 .unwrap_or(0)
686 .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
687 runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
688 runtime
689 .inner
690 .logical_allocated_lsn
691 .store(restored_cdc_lsn, std::sync::atomic::Ordering::Release);
692 runtime.rehydrate_snapshot_xid_floor()?;
693 assert!(runtime
694 .inner
695 .db
696 .runtime
697 .set(super::WeakRuntime(Arc::downgrade(&runtime.inner)))
698 .is_ok());
699 assert!(
700 runtime
701 .inner
702 .db
703 .store()
704 .snapshot_manager
705 .set(runtime.snapshot_manager())
706 .is_ok(),
707 "a new runtime owns the store transaction outcomes"
708 );
709 runtime
710 .bootstrap_system_keyed_collections()
711 .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
712 runtime.rehydrate_declared_column_schemas();
713 super::control_event_index::prepare(&runtime)?;
716 super::queue_storage_index::prepare(&runtime)?;
717 runtime.rehydrate_runtime_index_registry()?;
718 if runtime
719 .db()
720 .store()
721 .config()
722 .resident_memory_bytes_max
723 .is_some()
724 {
725 runtime
726 .db()
727 .store()
728 .checkpoint_missing_secondary_indices()?;
729 }
730 runtime.rehydrate_kv_tags()?;
731 runtime
732 .load_probabilistic_state()
733 .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
734
735 runtime.rehydrate_tenant_tables();
739 runtime.rehydrate_materialized_view_descriptors()?;
744 runtime.recover_store_forks()?;
745 if let Some(repl) = &runtime.inner.db.replication {
746 repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
747 }
748 runtime.recover_transactional_replication()?;
749
750 {
752 let sys = SystemInfo::collect();
753 runtime.inner.db.store().set_config_tree(
754 "red.system",
755 &crate::serde_json::json!({
756 "pid": sys.pid,
757 "cpu_cores": sys.cpu_cores,
758 "total_memory_bytes": sys.total_memory_bytes,
759 "available_memory_bytes": sys.available_memory_bytes,
760 "os": sys.os,
761 "arch": sys.arch,
762 "hostname": sys.hostname,
763 "started_at": SystemTime::now()
764 .duration_since(UNIX_EPOCH)
765 .unwrap_or_default()
766 .as_millis() as u64
767 }),
768 );
769
770 let store = runtime.inner.db.store();
772 if store
773 .get_collection("red_config")
774 .map(|m| m.query_all(|_| true).len())
775 .unwrap_or(0)
776 <= 10
777 {
778 store.set_config_tree("red.ai", &crate::json!({
779 "default": crate::json!({
780 "provider": "openai",
781 "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
782 }),
783 "max_embedding_inputs": 256,
784 "max_prompt_batch": 256,
785 "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
786 }));
787 store.set_config_tree(
788 "red.server",
789 &crate::json!({
790 "max_scan_limit": 1000,
791 "max_body_size": 1048576,
792 "read_timeout_ms": 5000,
793 "write_timeout_ms": 5000
794 }),
795 );
796 store.set_config_tree(
797 "red.storage",
798 &crate::json!({
799 "page_size": 4096,
800 "page_cache_capacity": 100000,
801 "auto_checkpoint_pages": 1000,
802 "snapshot_retention": 16,
803 "verify_checksums": true,
804 "segment": crate::json!({
805 "max_entities": 100000,
806 "max_bytes": 268435456_u64,
807 "compression_level": 6
808 }),
809 "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
810 "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
811 "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
812 }),
813 );
814 store.set_config_tree(
815 "red.search",
816 &crate::json!({
817 "rag": crate::json!({
818 "max_chunks_per_source": 10,
819 "max_total_chunks": 25,
820 "similarity_threshold": 0.8,
821 "graph_depth": 2,
822 "min_relevance": 0.3
823 }),
824 "fusion": crate::json!({
825 "vector_weight": 0.5,
826 "graph_weight": 0.3,
827 "table_weight": 0.2,
828 "dedup_threshold": 0.85
829 })
830 }),
831 );
832 store.set_config_tree(
833 "red.auth",
834 &crate::json!({
835 "enabled": false,
836 "session_ttl_secs": 3600,
837 "require_auth": false
838 }),
839 );
840 store.set_config_tree(
841 "red.query",
842 &crate::json!({
843 "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
844 "max_recursion_depth": 1000
845 }),
846 );
847 store.set_config_tree(
848 "red.indexes",
849 &crate::json!({
850 "auto_select": true,
851 "bloom_filter": crate::json!({
852 "enabled": true,
853 "false_positive_rate": 0.01,
854 "prune_on_scan": true
855 }),
856 "hash": crate::json!({ "enabled": true }),
857 "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
858 "spatial": crate::json!({ "enabled": true })
859 }),
860 );
861 store.set_config_tree(
862 "red.probabilistic",
863 &crate::json!({
864 "hll_registers": 16384,
865 "sketch_default_width": 1000,
866 "sketch_default_depth": 5,
867 "filter_default_capacity": 100000
868 }),
869 );
870 store.set_config_tree(
871 "red.timeseries",
872 &crate::json!({
873 "default_chunk_size": 1024,
874 "compression": crate::json!({
875 "timestamps": "delta_of_delta",
876 "values": "gorilla_xor"
877 }),
878 "default_retention_days": 0
879 }),
880 );
881 store.set_config_tree(
882 "red.queue",
883 &crate::json!({
884 "default_max_size": 0,
885 "default_max_attempts": 3,
886 "visibility_timeout_ms": 30000,
887 "consumer_idle_timeout_ms": 60000
888 }),
889 );
890 store.set_config_tree(
891 "red.backup",
892 &crate::json!({
893 "enabled": false,
894 "interval_secs": 3600,
895 "retention_count": 24,
896 "upload": false,
897 "backend": "local"
898 }),
899 );
900 store.set_config_tree(
901 "red.wal",
902 &crate::json!({
903 "archive": crate::json!({
904 "enabled": false,
905 "retention_hours": 168,
906 "prefix": reddb_file::backup_wal_prefix("")
907 })
908 }),
909 );
910 store.set_config_tree(
911 "red.cdc",
912 &crate::json!({
913 "enabled": true,
914 "buffer_size": 100000
915 }),
916 );
917 store.set_config_tree(
918 "red.config.secret",
919 &crate::json!({
920 "auto_encrypt": true,
921 "auto_decrypt": true
922 }),
923 );
924 }
925
926 crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
933 seed_storage_deploy_config(store.as_ref(), options.storage_profile);
934
935 let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
942 crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
943 if lehman_yao {
944 tracing::info!(
945 "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
946 );
947 }
948
949 let overlay_path = crate::runtime::config_overlay::config_file_path();
954 let _ =
955 crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
956 }
957
958 {
962 let store = runtime.inner.db.store();
963 for name in crate::application::vcs_collections::ALL {
964 let _ = store.get_or_create_collection(*name);
965 }
966 store.set_config_tree(
969 crate::application::vcs_collections::CONFIG_NAMESPACE,
970 &crate::json!({
971 "default_branch": "main",
972 "author": crate::json!({
973 "name": "reddb",
974 "email": "reddb@localhost"
975 }),
976 "protected_branches": crate::json!(["main"]),
977 "closure": crate::json!({
978 "enabled": true,
979 "lazy": true
980 }),
981 "merge": crate::json!({
982 "default_strategy": "auto",
983 "fast_forward": true
984 })
985 }),
986 );
987 }
988
989 {
992 let store = runtime.inner.db.store();
993 for name in crate::application::migration_collections::ALL {
994 let _ = store.get_or_create_collection(*name);
995 }
996 }
997
998 let _ = crate::application::topology_collections::ensure(&runtime);
1002
1003 runtime.prepare_paged_counts()?;
1008
1009 runtime
1017 .inner
1018 .db
1019 .store()
1020 .register_entity_id(crate::storage::EntityId::new(
1021 crate::storage::FIRST_USER_ENTITY_ID - 1,
1022 ));
1023
1024 {
1038 let weak = Arc::downgrade(&runtime.inner);
1039 runtime
1040 .background_tasks
1041 .as_ref()
1042 .expect("foreground runtime")
1043 .spawn(
1044 std::thread::Builder::new().name("reddb-maintenance".into()),
1045 move |stopped| {
1046 let tick = std::time::Duration::from_millis(200);
1047 let work_interval = std::time::Duration::from_secs(60);
1048 let mut last_work = std::time::Instant::now();
1049 let mut allocator_pressure =
1050 super::allocator_pressure::AllocatorPressure::default();
1051 loop {
1052 std::thread::park_timeout(tick);
1053 if stopped.load(AtomicOrdering::Acquire) {
1054 break;
1055 }
1056 let Some(inner) = weak.upgrade() else {
1057 break;
1060 };
1061 if last_work.elapsed() >= work_interval {
1062 let _stats = inner.db.store().context_index().stats();
1063 last_work = std::time::Instant::now();
1064 }
1065 let rt = RedDBRuntime {
1066 background_tasks: None,
1067 inner,
1068 };
1069 rt.try_reclaim_history_tick();
1070 allocator_pressure.tick();
1071 }
1072 },
1073 );
1074 }
1075
1076 {
1082 let weak = Arc::downgrade(&runtime.inner);
1083 runtime
1084 .background_tasks
1085 .as_ref()
1086 .expect("foreground runtime")
1087 .spawn(
1088 std::thread::Builder::new().name("reddb-cache-sweeper".into()),
1089 move |stopped| loop {
1090 std::thread::park_timeout(std::time::Duration::from_millis(250));
1091 if stopped.load(AtomicOrdering::Acquire) {
1092 break;
1093 }
1094 let Some(inner) = weak.upgrade() else {
1095 break;
1096 };
1097 let _ = crate::storage::cache::sweeper::BlobCacheSweeper::sweep_expired(
1098 &inner.result_blob_cache,
1099 crate::storage::cache::sweeper::SweepLimit::Either {
1100 entries: 512,
1101 millis: 5,
1102 },
1103 );
1104 },
1105 );
1106 }
1107
1108 {
1110 let store = runtime.inner.db.store();
1111 let mut backup_enabled = false;
1112 let mut backup_interval = 3600u64;
1113
1114 if let Some(manager) = store.get_collection("red_config") {
1115 manager.for_each_entity(|entity| {
1116 if let Some(row) = entity.data.as_row() {
1117 let key = row.get_field("key").and_then(|v| match v {
1118 reddb_types::Value::Text(s) => Some(s.as_ref()),
1119 _ => None,
1120 });
1121 let val = row.get_field("value");
1122 if key == Some("red.config.backup.enabled") {
1123 backup_enabled = match val {
1124 Some(reddb_types::Value::Boolean(true)) => true,
1125 Some(reddb_types::Value::Text(s)) => &**s == "true",
1126 _ => false,
1127 };
1128 } else if key == Some("red.config.backup.interval_secs") {
1129 if let Some(reddb_types::Value::Integer(n)) = val {
1130 backup_interval = *n as u64;
1131 }
1132 }
1133 }
1134 true
1135 });
1136 }
1137
1138 if backup_enabled {
1139 runtime.inner.backup_scheduler.set_interval(backup_interval);
1140 let rt = runtime.clone();
1141 runtime
1142 .inner
1143 .backup_scheduler
1144 .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
1145 }
1146 }
1147
1148 {
1150 runtime
1151 .inner
1152 .ec_registry
1153 .load_from_config_store(runtime.inner.db.store().as_ref());
1154 if !runtime.inner.db.options().read_only
1155 && !runtime.inner.ec_registry.async_configs().is_empty()
1156 {
1157 runtime.inner.ec_worker.start(
1158 Arc::clone(&runtime.inner.ec_registry),
1159 Arc::clone(&runtime.inner.db.store()),
1160 );
1161 }
1162 }
1163
1164 if let (true, crate::replication::ReplicationRole::Replica { primary_addr }) = (
1165 runtime.inner.db.options().replica_loop_enabled
1166 && !runtime.inner.db.options().read_only,
1167 runtime.inner.db.options().replication.role.clone(),
1168 ) {
1169 let rt = runtime.clone();
1170 std::thread::Builder::new()
1171 .name("reddb-replica".into())
1172 .spawn(move || rt.run_replica_loop(primary_addr))
1173 .ok();
1174 }
1175
1176 runtime.inner.lifecycle.mark_ready();
1181
1182 #[cfg(not(test))]
1183 if runtime
1184 .db()
1185 .store()
1186 .config()
1187 .resident_memory_bytes_max
1188 .is_some()
1189 && !runtime.db().options().read_only
1190 {
1191 runtime.start_count_projection_worker();
1192 }
1193
1194 {
1203 let weak_inner = Arc::downgrade(&runtime.inner);
1204 runtime
1205 .background_tasks
1206 .as_ref()
1207 .expect("foreground runtime")
1208 .spawn(
1209 std::thread::Builder::new()
1210 .name("reddb-mv-scheduler".into())
1211 .stack_size(8 * 1024 * 1024),
1216 move |stopped| loop {
1217 std::thread::park_timeout(std::time::Duration::from_millis(50));
1218 if stopped.load(AtomicOrdering::Acquire) {
1219 break;
1220 }
1221 let Some(inner) = weak_inner.upgrade() else {
1222 break;
1223 };
1224 let rt = RedDBRuntime {
1225 background_tasks: None,
1226 inner,
1227 };
1228 rt.refresh_due_materialized_views();
1229 },
1230 );
1231 }
1232
1233 if !runtime.write_gate().is_read_only() {
1243 let weak_inner = Arc::downgrade(&runtime.inner);
1244 runtime
1245 .background_tasks
1246 .as_ref()
1247 .expect("foreground runtime")
1248 .spawn(
1249 std::thread::Builder::new().name("reddb-retention-sweeper".into()),
1250 move |stopped| loop {
1251 std::thread::park_timeout(std::time::Duration::from_millis(500));
1252 if stopped.load(AtomicOrdering::Acquire) {
1253 break;
1254 }
1255 let Some(inner) = weak_inner.upgrade() else {
1256 break;
1257 };
1258 let rt = RedDBRuntime {
1259 background_tasks: None,
1260 inner,
1261 };
1262 rt.sweep_retention_tick(
1263 crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
1264 );
1265 rt.sweep_individual_ttl_tick(
1266 crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
1267 );
1268 },
1269 );
1270 }
1271
1272 if no_auth_boot {
1276 use crate::runtime::control_events::{
1277 ActorRef, ControlEvent, ControlEventCtx, EventKind, Outcome,
1278 };
1279 let ledger = std::sync::Arc::clone(&runtime.inner.control_event_ledger.read());
1280 let ctx = ControlEventCtx {
1281 actor: ActorRef::System("boot"),
1282 scope: None,
1283 request_id: None,
1284 trace_id: None,
1285 };
1286 let event = ControlEvent {
1287 kind: EventKind::AuthDisabled,
1288 outcome: Outcome::Allowed,
1289 action: std::borrow::Cow::Borrowed("auth.disabled"),
1290 resource: Some("server".to_string()),
1291 reason: Some("--no-auth / --dev".to_string()),
1292 matched_policy_id: None,
1293 fields: std::collections::HashMap::new(),
1294 };
1295 if let Err(err) = ledger.emit(&ctx, event) {
1296 tracing::warn!(
1297 target: "reddb::security",
1298 error = %err,
1299 "could not record the auth-disabled boot in the control-event ledger"
1300 );
1301 }
1302 }
1303
1304 Ok(runtime)
1305 }
1306
1307 fn rehydrate_snapshot_xid_floor(&self) -> RedDBResult<()> {
1308 let store = self.inner.db.store();
1309 let snapshot_manager = self.inner.transaction_state.snapshot_manager();
1310 let base_high_water_mark = store.startup_xid_high_water_mark();
1311 if let Some(high_water_mark) = base_high_water_mark {
1312 snapshot_manager.observe_committed_xid(high_water_mark);
1313 }
1314 for collection in store.list_collections() {
1315 let Some(manager) = store.get_collection(&collection) else {
1316 continue;
1317 };
1318 let observe = |entity: &crate::storage::UnifiedEntity| {
1319 snapshot_manager.observe_committed_xid(entity.xmin);
1320 snapshot_manager.observe_committed_xid(entity.xmax);
1321 true
1322 };
1323 if base_high_water_mark.is_some() {
1324 manager.for_each_entity(observe);
1327 } else {
1328 manager.try_for_each_entity(usize::MAX, observe)?;
1331 }
1332 }
1333 if base_high_water_mark.is_none() {
1334 store.remember_startup_xid_high_water_mark(
1335 snapshot_manager.peek_next_xid().saturating_sub(1),
1336 );
1337 }
1338 Ok(())
1339 }
1340
1341 pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1354 let store = self.inner.db.store();
1355 let mut changed = false;
1356 if store.get_collection(name).is_none() {
1357 store.get_or_create_collection(name);
1358 changed = true;
1359 }
1360 if self.inner.db.collection_contract(name).is_none() {
1361 self.inner
1362 .db
1363 .save_collection_contract(system_keyed_collection_contract(
1364 name,
1365 crate::catalog::CollectionModel::Table,
1366 ))
1367 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1368 changed = true;
1369 }
1370 if changed {
1371 self.inner
1372 .db
1373 .persist_metadata()
1374 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1375 }
1376 Ok(())
1377 }
1378
1379 pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
1384 let store = self.inner.db.store();
1385 if store.get_collection(name).is_none() {
1386 return Ok(());
1387 }
1388 store
1389 .drop_collection(name)
1390 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1391 if self.inner.db.collection_contract(name).is_some() {
1394 self.inner
1395 .db
1396 .remove_collection_contract(name)
1397 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1398 }
1399 self.invalidate_result_cache();
1400 self.inner
1401 .db
1402 .persist_metadata()
1403 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1404 Ok(())
1405 }
1406
1407 fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
1408 let mut changed = false;
1409 for (name, model) in [
1410 ("red.config", crate::catalog::CollectionModel::Config),
1411 ("red.vault", crate::catalog::CollectionModel::Vault),
1412 (
1416 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
1417 crate::catalog::CollectionModel::Config,
1418 ),
1419 ] {
1420 if self.inner.db.store().get_collection(name).is_none() {
1421 self.inner.db.store().get_or_create_collection(name);
1422 changed = true;
1423 }
1424 if self.inner.db.collection_contract(name).is_none() {
1425 let contract = system_keyed_collection_contract(name, model);
1426 if self.inner.db.options().read_only {
1427 self.inner.db.cache_read_only_bootstrap_contract(contract);
1428 } else {
1429 self.inner
1430 .db
1431 .save_collection_contract(contract)
1432 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1433 }
1434 changed = true;
1435 }
1436 }
1437 if changed {
1438 self.inner
1439 .db
1440 .persist_metadata()
1441 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1442 }
1443 Ok(())
1444 }
1445
1446 pub fn db(&self) -> Arc<RedDB> {
1447 Arc::clone(&self.inner.db)
1448 }
1449
1450 pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
1455 &self.inner.index_store
1456 }
1457
1458 pub(crate) fn schema_vocabulary_apply(
1463 &self,
1464 event: crate::runtime::schema_vocabulary::DdlEvent,
1465 ) {
1466 self.inner.schema_vocabulary.write().on_ddl(event);
1467 }
1468
1469 pub fn schema_vocabulary_lookup(
1474 &self,
1475 token: &str,
1476 ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
1477 self.inner.schema_vocabulary.read().lookup(token).to_vec()
1478 }
1479
1480 pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
1484 *self.inner.auth_store.write() = Some(store);
1485 }
1486
1487 pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
1490 self.inner.auth_store.read().clone()
1491 }
1492
1493 pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
1497 *self.inner.oauth_validator.write() = validator;
1498 }
1499
1500 pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
1504 self.inner.oauth_validator.read().clone()
1505 }
1506
1507 pub fn set_browser_token_authority(
1513 &self,
1514 authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
1515 ) {
1516 *self.inner.browser_token_authority.write() = authority;
1517 }
1518
1519 pub fn browser_token_authority(
1523 &self,
1524 ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
1525 self.inner.browser_token_authority.read().clone()
1526 }
1527}
1528
1529#[cfg(test)]
1530mod cache_boot_tests {
1531 use super::*;
1532
1533 #[test]
1534 fn persisted_cache_limits_apply_on_next_boot_within_memory_budget() {
1535 let directory = tempfile::tempdir().expect("cache config fixture");
1536 let options = RedDBOptions::persistent(directory.path().join("cache-config.rdb"))
1537 .with_memory_budget(64 * 1024 * 1024);
1538 let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1539 runtime.db().store().set_config_tree(
1540 "cache.blob",
1541 &crate::json!({
1542 "l1_bytes_max": 1024 * 1024,
1543 "l2_bytes_max": 2 * 1024 * 1024,
1544 "namespace_bytes_max": 256 * 1024,
1545 "max_namespaces": 7
1546 }),
1547 );
1548 runtime.flush().expect("persist cache config");
1549 drop(runtime);
1550
1551 let reopened = RedDBRuntime::with_options(options).expect("configured runtime");
1552 let stats = reopened.result_blob_cache().stats();
1553 assert_eq!(stats.l1_bytes_max(), 1024 * 1024);
1554 assert_eq!(stats.l2_bytes_max(), 2 * 1024 * 1024);
1555 assert_eq!(stats.namespace_bytes_max(), 256 * 1024);
1556 assert_eq!(stats.max_namespaces(), 7);
1557 }
1558
1559 #[test]
1560 fn single_file_l2_survives_runtime_restart_without_sidecar() {
1561 let directory = tempfile::tempdir().expect("single-file L2 fixture");
1562 let path = directory.path().join("cache-l2.rdb");
1563 let options = RedDBOptions::persistent(&path);
1564 let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1565 runtime
1566 .result_blob_cache()
1567 .put(
1568 "tenant",
1569 "key",
1570 crate::storage::cache::BlobCachePut::new(b"durable".to_vec()).with_policy(
1571 crate::storage::cache::BlobCachePolicy::default()
1572 .l1_admission(crate::storage::cache::L1Admission::Never),
1573 ),
1574 )
1575 .expect("write internal L2");
1576 assert!(runtime.result_blob_cache().l2_path().is_none());
1577 runtime.flush().expect("checkpoint internal L2");
1578 drop(runtime);
1579
1580 let reopened = RedDBRuntime::with_options(options).expect("reopen runtime");
1581 assert_eq!(
1582 reopened
1583 .result_blob_cache()
1584 .get("tenant", "key")
1585 .expect("L2 hit after restart")
1586 .bytes()
1587 .as_ref(),
1588 b"durable"
1589 );
1590 drop(reopened);
1591 let siblings: Vec<_> = std::fs::read_dir(directory.path())
1592 .expect("list fixture")
1593 .filter_map(Result::ok)
1594 .map(|entry| entry.file_name().to_string_lossy().into_owned())
1595 .collect();
1596 assert_eq!(siblings, vec!["cache-l2.rdb"]);
1597 }
1598
1599 #[test]
1600 fn single_file_l2_deadline_survives_restart_and_sweeps_atomically() {
1601 let directory = tempfile::tempdir().expect("single-file L2 TTL fixture");
1602 let path = directory.path().join("cache-l2-ttl.rdb");
1603 let options = RedDBOptions::persistent(&path);
1604 let mut runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1605 drop(runtime.background_tasks.take());
1606 runtime
1607 .result_blob_cache()
1608 .put(
1609 "tenant",
1610 "expired",
1611 crate::storage::cache::BlobCachePut::new(b"expired".to_vec()).with_policy(
1612 crate::storage::cache::BlobCachePolicy::default()
1613 .expires_at_unix_ms(1)
1614 .l1_admission(crate::storage::cache::L1Admission::Never),
1615 ),
1616 )
1617 .expect("write expiring L2 entry");
1618 runtime.flush().expect("checkpoint L2 deadline");
1619 drop(runtime);
1620
1621 let mut reopened = RedDBRuntime::with_options(options).expect("reopen runtime");
1622 drop(reopened.background_tasks.take());
1623 let manager = reopened
1624 .db()
1625 .store()
1626 .get_collection(crate::storage::cache::blob::INTERNAL_L2_COLLECTION)
1627 .expect("L2 collection");
1628 assert_eq!(manager.scheduled_metadata_expiry_count(), 1);
1629 let bytes_before = reopened.result_blob_cache().stats().l2_bytes_in_use();
1630 assert!(bytes_before > 0);
1631
1632 assert_eq!(reopened.sweep_individual_ttl_tick_at(64, u64::MAX), 0);
1635 assert_eq!(
1636 reopened.result_blob_cache().stats().l2_bytes_in_use(),
1637 bytes_before
1638 );
1639 let report = crate::storage::cache::BlobCacheSweeper::sweep_expired(
1640 reopened.result_blob_cache(),
1641 crate::storage::cache::SweepLimit::Entries(1),
1642 );
1643 assert_eq!(report.entries_evicted, 1);
1644 assert_eq!(reopened.result_blob_cache().stats().l2_bytes_in_use(), 0);
1645 assert_eq!(manager.scheduled_metadata_expiry_count(), 0);
1646 }
1647
1648 #[test]
1649 fn expired_l2_payload_pages_are_reused() {
1650 let directory = tempfile::tempdir().expect("L2 page reuse fixture");
1651 let path = directory.path().join("cache-l2-reuse.rdb");
1652 let options = RedDBOptions::persistent(&path)
1653 .with_memory_budget(32 * 1024 * 1024)
1654 .with_bounded_disk_residency(true);
1655 let mut runtime = RedDBRuntime::with_options(options.clone()).expect("runtime");
1656 drop(runtime.background_tasks.take());
1657 let mut state = 0x9e37_79b9_7f4a_7c15u64;
1658 let payload: Vec<u8> = std::iter::repeat_with(|| {
1659 state ^= state << 13;
1660 state ^= state >> 7;
1661 state ^= state << 17;
1662 state.to_le_bytes()[0]
1663 })
1664 .take(64 * 1024)
1665 .collect();
1666 for id in 0..8 {
1667 runtime
1668 .result_blob_cache()
1669 .put(
1670 "reuse",
1671 format!("expired-{id}"),
1672 crate::storage::cache::BlobCachePut::new(payload.clone()).with_policy(
1673 crate::storage::cache::BlobCachePolicy::default()
1674 .expires_at_unix_ms(1)
1675 .l1_admission(crate::storage::cache::L1Admission::Never),
1676 ),
1677 )
1678 .expect("expired L2 payload");
1679 }
1680 runtime.checkpoint().expect("initial L2 checkpoint");
1681 let initial_pages = runtime
1682 .db()
1683 .store()
1684 .pager()
1685 .expect("pager")
1686 .page_count()
1687 .expect("initial page count");
1688 drop(runtime);
1689
1690 let mut runtime = RedDBRuntime::with_options(options).expect("reopened runtime");
1691 drop(runtime.background_tasks.take());
1692 let store = runtime.db().store();
1693 let pager = store.pager().expect("pager");
1694 let report = crate::storage::cache::BlobCacheSweeper::sweep_expired(
1695 runtime.result_blob_cache(),
1696 crate::storage::cache::SweepLimit::Entries(64),
1697 );
1698 assert_eq!(report.entries_evicted, 8);
1699 runtime.checkpoint().expect("expired L2 checkpoint");
1700
1701 let mut cursor = store
1702 .mvcc_reclaim_cursor(crate::storage::cache::blob::INTERNAL_L2_COLLECTION.to_string())
1703 .expect("L2 reclaim cursor");
1704 for _ in 0..64 {
1705 store
1706 .reclaim_mvcc_tick(
1707 &mut cursor,
1708 &runtime.snapshot_manager(),
1709 runtime.mvcc_vacuum_cutoff_xid(),
1710 64,
1711 8,
1712 )
1713 .expect("L2 MVCC reclaim");
1714 if cursor.finished() {
1715 break;
1716 }
1717 }
1718 assert!(cursor.finished());
1719 drop(cursor);
1720 for _ in 0..3 {
1721 runtime.checkpoint().expect("publish reusable L2 pages");
1722 }
1723 let free_before_reuse = pager.free_page_count().expect("free pages");
1724
1725 for id in 0..8 {
1726 runtime
1727 .result_blob_cache()
1728 .put(
1729 "reuse",
1730 format!("replacement-{id}"),
1731 crate::storage::cache::BlobCachePut::new(payload.clone()).with_policy(
1732 crate::storage::cache::BlobCachePolicy::default()
1733 .expires_at_unix_ms(u64::MAX)
1734 .l1_admission(crate::storage::cache::L1Admission::Never),
1735 ),
1736 )
1737 .expect("replacement L2 payload");
1738 }
1739 runtime.checkpoint().expect("replacement L2 checkpoint");
1740 let pages_after_reuse = pager.page_count().expect("page count after reuse");
1741 assert!(
1742 pages_after_reuse <= initial_pages.saturating_add(16),
1743 "expired L2 pages were not reused: initial={initial_pages}, free_before={free_before_reuse}, after={pages_after_reuse}"
1744 );
1745 }
1746
1747 #[test]
1748 fn zero_cache_namespace_limit_is_rejected_on_boot() {
1749 let directory = tempfile::tempdir().expect("cache config fixture");
1750 let options = RedDBOptions::persistent(directory.path().join("cache-config-zero.rdb"));
1751 let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1752 runtime
1753 .db()
1754 .store()
1755 .set_config_tree("cache.blob.max_namespaces", &crate::json!(0));
1756 runtime.flush().expect("persist cache config");
1757 drop(runtime);
1758
1759 let error = match RedDBRuntime::with_options(options) {
1760 Ok(_) => panic!("zero limit must fail boot"),
1761 Err(error) => error,
1762 };
1763 assert!(error
1764 .to_string()
1765 .contains("cache.blob.max_namespaces must be greater than zero"));
1766 }
1767
1768 #[test]
1769 fn zero_cache_namespace_memory_limit_is_rejected_on_boot() {
1770 let directory = tempfile::tempdir().expect("cache config fixture");
1771 let options = RedDBOptions::persistent(directory.path().join("cache-bytes-zero.rdb"));
1772 let runtime = RedDBRuntime::with_options(options.clone()).expect("initial runtime");
1773 runtime
1774 .db()
1775 .store()
1776 .set_config_tree("cache.blob.namespace_bytes_max", &crate::json!(0));
1777 runtime.flush().expect("persist cache config");
1778 drop(runtime);
1779
1780 let error = match RedDBRuntime::with_options(options) {
1781 Ok(_) => panic!("zero namespace memory limit must fail boot"),
1782 Err(error) => error,
1783 };
1784 assert!(error
1785 .to_string()
1786 .contains("cache.blob.namespace_bytes_max must be greater than zero"));
1787 }
1788}
1789
1790#[cfg(test)]
1791mod xid_resume_tests {
1792 use super::*;
1793 use reddb_engine::storage::collection_read::{CollectionCursor, CollectionRead, EncodedRecord};
1794
1795 struct Unreadable;
1796
1797 fn unavailable() -> crate::storage::StoreError {
1798 crate::storage::StoreError::Io(std::io::Error::other("XID bootstrap read a base payload"))
1799 }
1800
1801 impl CollectionRead for Unreadable {
1802 fn get_record(
1803 &self,
1804 id: crate::storage::EntityId,
1805 _: usize,
1806 ) -> Result<Option<EncodedRecord>, crate::storage::StoreError> {
1807 if id.raw() == 999 {
1808 Ok(None)
1809 } else {
1810 Err(unavailable())
1811 }
1812 }
1813 fn cursor(&self) -> Result<Box<dyn CollectionCursor + '_>, crate::storage::StoreError> {
1814 Err(unavailable())
1815 }
1816 fn owned_cursor(&self) -> Result<Box<dyn CollectionCursor>, crate::storage::StoreError> {
1817 Err(unavailable())
1818 }
1819 }
1820
1821 #[test]
1822 fn xid_resume_survives_pending_checkpoint_process_exit() {
1823 const FIXTURE: &str = "REDDB_XID_RESUME_CRASH_FIXTURE";
1824 let open = |path: std::path::PathBuf| {
1825 let options = RedDBOptions::persistent(path)
1826 .with_memory_budget(64 * 1024 * 1024)
1827 .with_storage_profile(
1828 crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
1829 )
1830 .expect("profile");
1831 crate::storage::RedDB::with_disk_residency_for_qualification(|| {
1832 RedDBRuntime::with_options(options)
1833 })
1834 .expect("crash recovery runtime")
1835 };
1836 if let Some(root) = std::env::var_os(FIXTURE) {
1837 let root = std::path::PathBuf::from(root);
1838 assert_eq!(
1839 std::fs::read_to_string(root.join("owned-fixture")).expect("owned fixture"),
1840 "xid-resume-crash-v1"
1841 );
1842 let runtime = open(root.join("crash.rdb"));
1843 for sql in [
1844 "CREATE TABLE crash_rows (id INTEGER, payload TEXT)",
1845 "INSERT INTO crash_rows (id, payload) VALUES (1, 'committed')",
1846 "BEGIN",
1847 "UPDATE crash_rows SET payload = 'pending' WHERE id = 1",
1848 "INSERT INTO crash_rows (id, payload) VALUES (2, 'pending')",
1849 ] {
1850 runtime.execute_query(sql).expect("crash fixture statement");
1851 }
1852 runtime
1853 .flush()
1854 .expect("checkpoint with pending transaction");
1855 std::fs::write(
1856 root.join("checkpoint-complete"),
1857 "pending transaction remains open",
1858 )
1859 .expect("acknowledged fixture checkpoint");
1860 std::process::exit(0);
1863 }
1864 let directory = tempfile::tempdir().expect("owned crash fixture");
1865 std::fs::write(
1866 directory.path().join("owned-fixture"),
1867 "xid-resume-crash-v1",
1868 )
1869 .expect("ownership marker");
1870 let child = std::process::Command::new(std::env::current_exe().expect("test executable"))
1871 .args([
1872 "--exact",
1873 "runtime::impl_lifecycle::xid_resume_tests::xid_resume_survives_pending_checkpoint_process_exit",
1874 "--test-threads=1",
1875 ])
1876 .env(FIXTURE, directory.path())
1877 .output()
1878 .expect("crashing owned child");
1879 assert!(
1880 child.status.success(),
1881 "{}",
1882 String::from_utf8_lossy(&child.stderr)
1883 );
1884 assert!(directory.path().join("checkpoint-complete").is_file());
1885 let path = directory.path().join("crash.rdb");
1886 assert!(path.is_file());
1887 let runtime = open(path.clone());
1888 let result = runtime
1889 .execute_query("SELECT id, payload FROM crash_rows ORDER BY id")
1890 .expect("committed preimage");
1891 assert_eq!(
1892 result.result.records.len(),
1893 1,
1894 "pending insert must stay invisible"
1895 );
1896 assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
1897 assert_eq!(
1898 result.result.records[0].get("payload"),
1899 Some(&Value::text("committed"))
1900 );
1901 runtime
1902 .execute_query("INSERT INTO crash_rows (id, payload) VALUES (3, 'after')")
1903 .expect("new writer after recovery");
1904 runtime.flush().expect("recovered checkpoint");
1905 drop(runtime);
1906 let reopened = open(path);
1907 let result = reopened
1908 .execute_query("SELECT id FROM crash_rows ORDER BY id")
1909 .expect("second recovery");
1910 assert_eq!(result.result.records.len(), 2);
1911 assert_eq!(result.result.records[0].get("id"), Some(&Value::Integer(1)));
1912 assert_eq!(result.result.records[1].get("id"), Some(&Value::Integer(3)));
1913 }
1914
1915 #[test]
1916 fn xid_resume_skips_known_base_payloads_but_rejects_an_unproven_root() {
1917 let directory = tempfile::tempdir().expect("resume fixture");
1918 let options = RedDBOptions::persistent(directory.path().join("resume.rdb"))
1919 .with_memory_budget(64 * 1024 * 1024)
1920 .with_storage_profile(
1921 crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
1922 )
1923 .expect("profile");
1924 let runtime = crate::storage::RedDB::with_disk_residency_for_qualification(|| {
1925 RedDBRuntime::with_options(options)
1926 })
1927 .expect("runtime");
1928 runtime
1929 .execute_query("CREATE TABLE resume_rows (id INTEGER)")
1930 .expect("table");
1931 runtime
1932 .execute_query("INSERT INTO resume_rows (id) VALUES (1)")
1933 .expect("row");
1934 runtime.flush().expect("publish complete XID summary");
1935 let store = runtime.db().store();
1936 assert!(store.startup_xid_high_water_mark().is_some());
1937 let root = store
1938 .collection_root_page("resume_rows")
1939 .expect("published root");
1940 let manager = store.get_collection("resume_rows").expect("manager");
1941 for (candidate, proven) in [(root, true), (root + 1, false)] {
1942 manager.install_backing(
1943 Arc::new(Unreadable),
1944 candidate,
1945 store.format_version(),
1946 4096,
1947 1,
1948 );
1949 let mut overlay = crate::storage::UnifiedEntity::table_row(
1950 crate::storage::EntityId::new(999),
1951 "resume_rows",
1952 999,
1953 vec![Value::Integer(2)],
1954 );
1955 overlay.xmin = 500_000;
1956 overlay.xmax = 600_000;
1957 manager.insert(overlay).expect("resident WAL overlay");
1958 let result = runtime.rehydrate_snapshot_xid_floor();
1959 assert_eq!(result.is_ok(), proven, "root={candidate}: {result:?}");
1960 if proven {
1961 assert!(runtime.snapshot_manager().peek_next_xid() > 600_000);
1962 }
1963 }
1964 }
1965}