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_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 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 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 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 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 crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
642 &runtime.inner.audit_log,
643 ));
644
645 runtime
648 .inner
649 .slow_query_logger
650 .attach_store(Arc::clone(&runtime.inner.slow_query_store));
651
652 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 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 runtime.rehydrate_tenant_tables();
741 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 {
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 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 crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
938 seed_storage_deploy_config(store.as_ref(), options.storage_profile);
939
940 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 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 {
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 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 {
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 let _ = crate::application::topology_collections::ensure(&runtime);
1007
1008 runtime.prepare_paged_counts()?;
1013
1014 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 {
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 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 {
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 {
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 {
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 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 {
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 .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 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 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 manager.for_each_entity(observe);
1337 } else {
1338 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 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 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 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 (
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 pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
1465 &self.inner.index_store
1466 }
1467
1468 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 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 pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
1494 *self.inner.auth_store.write() = Some(store);
1495 }
1496
1497 pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
1500 self.inner.auth_store.read().clone()
1501 }
1502
1503 pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
1507 *self.inner.oauth_validator.write() = validator;
1508 }
1509
1510 pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
1514 self.inner.oauth_validator.read().clone()
1515 }
1516
1517 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 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 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 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}