1use super::*;
8
9fn runtime_pool_lock(runtime: &RedDBRuntime) -> std::sync::MutexGuard<'_, PoolState> {
10 runtime
11 .inner
12 .pool
13 .lock()
14 .unwrap_or_else(|poisoned| poisoned.into_inner())
15}
16
17impl RedDBRuntime {
18 pub fn acquire(&self) -> RedDBResult<RuntimeConnection> {
19 self.acquire_connection(true)
20 }
21
22 pub(crate) fn acquire_wire_connection(&self) -> RedDBResult<RuntimeConnection> {
25 self.acquire_connection(false)
26 }
27
28 fn acquire_connection(&self, uses_pool_slot: bool) -> RedDBResult<RuntimeConnection> {
29 let mut pool = self
30 .inner
31 .pool
32 .lock()
33 .map_err(|e| RedDBError::Internal(format!("connection pool lock poisoned: {e}")))?;
34 if uses_pool_slot && pool.pooled_active >= self.inner.pool_config.max_connections {
35 return Err(RedDBError::Internal(
36 "connection pool exhausted".to_string(),
37 ));
38 }
39
40 let id = if let Some(id) = pool.idle.pop() {
41 id
42 } else {
43 let id = pool.next_id;
44 pool.next_id += 1;
45 id
46 };
47 pool.active += 1;
48 if uses_pool_slot {
49 pool.pooled_active += 1;
50 }
51 pool.total_checkouts += 1;
52 drop(pool);
53
54 self.inner.node_load_telemetry.record_connect();
57
58 Ok(RuntimeConnection {
59 background_tasks: self.background_tasks.clone(),
60 id,
61 uses_pool_slot,
62 inner: Arc::clone(&self.inner),
63 })
64 }
65
66 pub fn checkpoint(&self) -> RedDBResult<()> {
67 self.checkpoint_local()?;
72 if let Err(err) = self.assert_remote_write_allowed("checkpoint") {
73 tracing::warn!(
74 target: "reddb::serverless::lease",
75 error = %err,
76 "checkpoint: skipping remote upload — lease not held"
77 );
78 return Ok(());
79 }
80 self.publish_remote_snapshot()
81 }
82
83 pub(super) fn checkpoint_local(&self) -> RedDBResult<()> {
84 let started = std::time::Instant::now();
85 self.seal_hypertable_chunks_for_checkpoint(
86 self.inner.checkpoint_columnar_emission_budget_chunks,
87 )?;
88 let store = self.inner.db.store();
89 let publication = store.vault_publication_lock.lock();
90 self.prepare_paged_counts()?;
93 self.persist_probabilistic_snapshots()?;
94 self.inner
95 .db
96 .flush_local_after_runtime_snapshots()
97 .map_err(|err| {
98 let msg = err.to_string();
103 crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
104 lsn: 0,
105 error: msg.clone(),
106 }
107 .emit_global();
108 crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
109 path: "<flush_local_only>".to_string(),
110 error: msg.clone(),
111 }
112 .emit_global();
113 RedDBError::Engine(msg)
114 })?;
115 self.index_store_ref().clear_paged_btree_overlays();
119 self.inner.probabilistic.replayed_names.lock().clear();
120 drop(publication);
121 let checkpoint_lsn = self.replication_commit_lsn();
122 self.inner
123 .checkpoint_projection_stats
124 .record_checkpoint(checkpoint_lsn, started.elapsed().as_millis() as u64);
125 Ok(())
126 }
127
128 pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
133 if self.inner.db.remote_backend.is_none() {
134 return Ok(());
135 }
136 if matches!(
137 self.inner.write_gate.role(),
138 crate::replication::ReplicationRole::Replica { .. }
139 ) {
140 return Err(RedDBError::ReadOnly(format!(
141 "replica cannot publish remote data — {action} blocked"
142 )));
143 }
144 match self.inner.write_gate.lease_state() {
145 crate::runtime::write_gate::LeaseGateState::NotHeld => {
146 self.inner.audit_log.record(
147 action,
148 "system",
149 "remote_backend",
150 "err: writer lease not held",
151 crate::json::Value::Null,
152 );
153 Err(RedDBError::ReadOnly(format!(
154 "writer lease not held — {action} blocked (serverless fence)"
155 )))
156 }
157 _ => Ok(()),
158 }
159 }
160
161 pub(super) fn publish_remote_snapshot(&self) -> RedDBResult<()> {
162 self.assert_remote_write_allowed("snapshot publication")?;
163 if let Some(lifecycle) = self.lease_lifecycle() {
164 let store = self.inner.db.store();
165 let _publication = store.vault_publication_lock.lock();
168 if lifecycle.publish_snapshot(|| self.capture_serverless_snapshot())? {
169 return Ok(());
170 }
171 }
172 if self.inner.db.is_paged()
173 && self.inner.db.remote_backend.is_some()
174 && self.inner.db.remote_key.is_some()
175 {
176 let snapshot = self.capture_serverless_snapshot()?;
179 self.assert_remote_write_allowed("checkpoint upload")?;
180 return self
181 .inner
182 .db
183 .upload_checkpoint_to_remote_backend(snapshot.path())
184 .map_err(|err| RedDBError::Engine(err.to_string()));
185 }
186 self.inner
187 .db
188 .upload_to_remote_backend()
189 .map_err(|err| RedDBError::Engine(err.to_string()))
190 }
191
192 pub(crate) fn capture_serverless_snapshot(
193 &self,
194 ) -> RedDBResult<reddb_file::ServerlessWriterLeaseTempFile> {
195 use reddb_file::VfsFile;
196 use std::io::Write;
197 let store = self.inner.db.store();
198 let _publication = store.vault_publication_lock.lock();
199 self.checkpoint_local()?;
200 let snapshot = reddb_file::ServerlessWriterLeaseTempFile::new("snapshot-payload");
201 if let Some(pager) = store.pager() {
202 pager
205 .with_file(|source| -> std::io::Result<()> {
206 let mut output = std::fs::File::create(snapshot.path())?;
207 source.seek(std::io::SeekFrom::Start(0))?;
208 let mut buffer = [0u8; 65536];
209 loop {
210 let count = source.read(&mut buffer)?;
211 if count == 0 {
212 break;
213 }
214 output.write_all(&buffer[..count])?;
215 }
216 output.sync_all()
217 })
218 .map_err(|err| RedDBError::Internal(err.to_string()))?
219 .map_err(|err| RedDBError::Internal(err.to_string()))?;
220 } else {
221 let path = self.inner.db.path().ok_or_else(|| {
222 RedDBError::InvalidOperation(
223 "serverless snapshots require persistent local storage".into(),
224 )
225 })?;
226 std::fs::copy(path, snapshot.path())
227 .map_err(|err| RedDBError::Internal(err.to_string()))?;
228 }
229 Ok(snapshot)
230 }
231
232 pub fn run_maintenance(&self) -> RedDBResult<()> {
233 self.inner
234 .db
235 .run_maintenance()
236 .map_err(|err| RedDBError::Internal(err.to_string()))
237 }
238
239 pub fn scan_collection(
240 &self,
241 collection: &str,
242 cursor: Option<ScanCursor>,
243 limit: usize,
244 ) -> RedDBResult<ScanPage> {
245 use reddb_rql::ast::PolicyAction;
246
247 self.check_collection_privilege(PolicyAction::Select, collection)?;
248 let store = self.inner.db.store();
249 let manager = store
250 .get_collection(collection)
251 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
252
253 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
254 let limit = limit.max(1);
255 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
256 .unwrap_or(usize::MAX)
257 .clamp(1, 16 * 1024 * 1024);
258 let (snapshot, _retention) = self.native_read_snapshot()?;
259 let _snapshot_guard = super::impl_core::CurrentSnapshotGuard::install(snapshot.clone());
260 let (total, items) = manager.try_page_in_id_order_where_at_snapshot(
261 Some(&snapshot),
262 offset,
263 limit,
264 read_bytes,
265 |entity| {
266 crate::runtime::rls_injection::entity_passes_rls_action(
267 self,
268 collection,
269 PolicyAction::Select,
270 entity,
271 )
272 },
273 )?;
274 let end = total.min(offset.saturating_add(limit));
275 let next = (end < total).then_some(ScanCursor { offset: end });
276
277 Ok(ScanPage {
278 collection: collection.to_string(),
279 items,
280 next,
281 total,
282 })
283 }
284
285 pub fn get_collection_entity(
286 &self,
287 collection: &str,
288 id: crate::storage::EntityId,
289 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
290 self.get_collection_entity_for_action(collection, id, reddb_rql::ast::PolicyAction::Select)
291 }
292
293 pub(crate) fn get_collection_entity_for_action(
294 &self,
295 collection: &str,
296 id: crate::storage::EntityId,
297 action: reddb_rql::ast::PolicyAction,
298 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
299 self.check_collection_privilege(action, collection)?;
300 let store = self.inner.db.store();
301 let Some(manager) = store.get_collection(collection) else {
302 return Ok(None);
303 };
304 let (snapshot, _retention) = self.native_read_snapshot()?;
305 let _snapshot_guard = super::impl_core::CurrentSnapshotGuard::install(snapshot.clone());
306 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
307 .unwrap_or(usize::MAX)
308 .clamp(1, 16 * 1024 * 1024);
309 let mut entity = manager.try_get(id, read_bytes)?.filter(|candidate| {
310 candidate.logical_id() == id
311 && super::impl_core::entity_visible_with_context(Some(&snapshot), candidate)
312 });
313 if entity.is_none() {
316 manager.try_for_each_entity(read_bytes, |candidate| {
317 if candidate.logical_id() == id
318 && super::impl_core::entity_visible_with_context(Some(&snapshot), candidate)
319 && entity.as_ref().is_none_or(|selected: &UnifiedEntity| {
320 (candidate.xmin, candidate.id) > (selected.xmin, selected.id)
321 })
322 {
323 entity = Some(candidate.clone());
324 }
325 true
326 })?;
327 }
328 manager.restore_owned_row_schemas(entity.iter_mut());
329 Ok(entity.filter(|entity| {
330 crate::runtime::rls_injection::entity_passes_rls_action(
331 self, collection, action, entity,
332 )
333 }))
334 }
335
336 pub(crate) fn native_read_snapshot(
337 &self,
338 ) -> RedDBResult<(
339 super::impl_core::SnapshotContext,
340 crate::storage::transaction::snapshot::SnapshotRetentionGuard,
341 )> {
342 let state = &self.inner.transaction_state;
343 let manager = state.snapshot_manager();
344 if let Some(context) = super::impl_core::capture_current_snapshot()
345 .filter(|context| Arc::ptr_eq(&context.manager, &manager))
346 {
347 let (_, retention) = manager.retain_snapshot(context.snapshot.clone());
348 return Ok((context, retention));
349 }
350 let connection_id = super::impl_core::current_connection_id();
351 let store = self.inner.db.store();
352 let uses_vcs = self.vcs_has_versioned_collections()?;
353 let _publication = uses_vcs.then(|| store.vault_publication_lock.lock());
354 let (snapshot, retention) = state.retained_current_snapshot(connection_id);
355 let branch = if uses_vcs {
356 self.vcs_branch_read_view(&snapshot, None)?
357 } else {
358 None
359 };
360 let context = state.context(connection_id);
361 Ok((
362 super::impl_core::SnapshotContext {
363 branch,
364 snapshot,
365 manager,
366 own_xids: self.own_transaction_xids(),
367 requires_index_fallback: true,
368 serializable_reader: context
369 .filter(|context| {
370 context.isolation
371 == crate::storage::transaction::IsolationLevel::Serializable
372 })
373 .map(|context| context.xid),
374 },
375 retention,
376 ))
377 }
378
379 pub fn catalog(&self) -> CatalogModelSnapshot {
380 self.inner.db.catalog_model_snapshot()
381 }
382
383 pub fn try_catalog(&self) -> RedDBResult<CatalogModelSnapshot> {
386 if self
387 .inner
388 .db
389 .store()
390 .config()
391 .resident_memory_bytes_max
392 .is_none()
393 {
394 return Ok(self.catalog());
395 }
396 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
397 .unwrap_or(usize::MAX)
398 .clamp(1, 16 * 1024 * 1024);
399 let _scratch = self
400 .try_reserve_query_buffer(read_bytes * 2)
401 .ok_or_else(|| {
402 self.memory_accounting().record_admission_denied();
403 RedDBError::MaterializationLimitExceeded {
404 executor: "catalog",
405 limit: usize::try_from(self.memory_budget().resolved_bytes)
406 .unwrap_or(usize::MAX),
407 current: usize::try_from(self.memory_accounting().total_used_bytes())
408 .unwrap_or(usize::MAX)
409 .saturating_add(1),
410 }
411 })?;
412 Ok(self.inner.db.try_catalog_model_snapshot(read_bytes)?)
413 }
414
415 pub(crate) fn try_catalog_for_collections(
419 &self,
420 collection_names: &std::collections::HashSet<String>,
421 observe_legacy_payloads: bool,
422 ) -> RedDBResult<CatalogModelSnapshot> {
423 if self
424 .inner
425 .db
426 .store()
427 .config()
428 .resident_memory_bytes_max
429 .is_none()
430 {
431 return Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
432 collection_names,
433 observe_legacy_payloads,
434 usize::MAX,
435 )?);
436 }
437 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
438 .unwrap_or(usize::MAX)
439 .clamp(1, 16 * 1024 * 1024);
440 let _scratch = self
441 .try_reserve_query_buffer(read_bytes * 2)
442 .ok_or_else(|| {
443 self.memory_accounting().record_admission_denied();
444 RedDBError::MaterializationLimitExceeded {
445 executor: "catalog",
446 limit: usize::try_from(self.memory_budget().resolved_bytes)
447 .unwrap_or(usize::MAX),
448 current: usize::try_from(self.memory_accounting().total_used_bytes())
449 .unwrap_or(usize::MAX)
450 .saturating_add(1),
451 }
452 })?;
453 Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
454 collection_names,
455 observe_legacy_payloads,
456 read_bytes,
457 )?)
458 }
459
460 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
461 self.inner.db.catalog_consistency_report()
462 }
463
464 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
465 crate::catalog::attention_summary(&self.catalog())
466 }
467
468 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
469 crate::catalog::collection_attention(&self.catalog())
470 }
471
472 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
473 crate::catalog::index_attention(&self.catalog())
474 }
475
476 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
477 crate::catalog::graph_projection_attention(&self.catalog())
478 }
479
480 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
481 crate::catalog::analytics_job_attention(&self.catalog())
482 }
483
484 pub fn stats(&self) -> RuntimeStats {
485 let pool = runtime_pool_lock(self);
486 RuntimeStats {
487 active_connections: pool.active,
488 idle_connections: pool.idle.len(),
489 total_checkouts: pool.total_checkouts,
490 paged_mode: self.inner.db.is_paged(),
491 started_at_unix_ms: self.inner.started_at_unix_ms,
492 store: self.inner.db.stats(),
493 system: SystemInfo::collect(),
494 result_blob_cache: self.inner.result_blob_cache.stats(),
495 kv: self.inner.kv_stats.snapshot(),
496 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
497 }
498 }
499}
500
501#[cfg(test)]
502mod security_boundary_tests {
503 use super::*;
504 use crate::application::entity::{DeleteEntityInput, PatchEntityInput};
505 use crate::application::ports::RuntimeEntityPort;
506 use crate::auth::Role;
507 use crate::runtime::execution_context::{with_snapshot_bundle, SnapshotBundle};
508 use crate::storage::{EntityData, UnifiedEntity};
509 use reddb_types::Value;
510
511 fn text_field<'a>(entity: &'a UnifiedEntity, name: &str) -> Option<&'a str> {
512 let EntityData::Row(row) = &entity.data else {
513 return None;
514 };
515 let value = row
516 .named
517 .as_ref()
518 .and_then(|named| named.get(name))
519 .or_else(|| {
520 row.schema.as_ref().and_then(|schema| {
521 schema
522 .iter()
523 .position(|column| column == name)
524 .and_then(|index| row.columns.get(index))
525 })
526 })?;
527 match value {
528 Value::Text(value) => Some(value.as_ref()),
529 _ => None,
530 }
531 }
532
533 #[test]
534 fn typed_entity_boundary_filters_before_pagination_and_hides_foreign_ids() {
535 let runtime = RedDBRuntime::in_memory().expect("runtime");
536 runtime
537 .execute_query(
538 "CREATE TABLE docs (id INT, tenant_id TEXT, body TEXT) TENANT BY (tenant_id)",
539 )
540 .expect("tenant table");
541 runtime
542 .execute_query(
543 "INSERT INTO docs (id, tenant_id, body) VALUES \
544 (1, 'acme', 'first'), (2, 'globex', 'hidden'), (3, 'acme', 'second')",
545 )
546 .expect("seed platform rows");
547
548 let unfiltered = runtime
549 .scan_collection("docs", None, 10)
550 .expect("platform scan");
551 let foreign_id = unfiltered
552 .items
553 .iter()
554 .find(|entity| text_field(entity, "tenant_id") == Some("globex"))
555 .map(|entity| entity.id)
556 .expect("foreign row");
557
558 let bundle = SnapshotBundle {
559 snapshot: None,
560 auth: Some(("alice".to_string(), Role::Write)),
561 auth_principal: Some(crate::auth::UserId::scoped("acme", "alice")),
562 tenant: Some("acme".to_string()),
563 };
564 with_snapshot_bundle(&bundle, || {
565 let first = runtime
566 .scan_collection("docs", None, 1)
567 .expect("first visible page");
568 assert_eq!(first.total, 2);
569 assert_eq!(first.items.len(), 1);
570 assert_eq!(text_field(&first.items[0], "tenant_id"), Some("acme"));
571 assert_eq!(first.next, Some(ScanCursor { offset: 1 }));
572
573 let second = runtime
574 .scan_collection("docs", first.next, 1)
575 .expect("second visible page");
576 assert_eq!(second.total, 2);
577 assert_eq!(second.items.len(), 1);
578 assert_eq!(text_field(&second.items[0], "tenant_id"), Some("acme"));
579 assert_eq!(second.next, None);
580
581 assert!(runtime
582 .get_collection_entity("docs", foreign_id)
583 .expect("typed get")
584 .is_none());
585 assert!(matches!(
586 runtime.patch_entity(PatchEntityInput {
587 collection: "docs".to_string(),
588 id: foreign_id,
589 payload: crate::json::Value::Object(Default::default()),
590 operations: Vec::new(),
591 }),
592 Err(crate::RedDBError::NotFound(_))
593 ));
594 let deleted = runtime
595 .delete_entity(DeleteEntityInput {
596 collection: "docs".to_string(),
597 id: foreign_id,
598 })
599 .expect("typed delete");
600 assert!(!deleted.deleted);
601 });
602
603 assert!(runtime.db().store().get("docs", foreign_id).is_some());
604 }
605
606 #[test]
607 fn scan_collection_pages_the_mvcc_view_across_rollback() {
608 let runtime = RedDBRuntime::in_memory().expect("runtime");
609 runtime
610 .execute_query("CREATE TABLE docs (id INT, body TEXT)")
611 .expect("table");
612 runtime
613 .execute_query("INSERT INTO docs (id, body) VALUES (1, 'kept')")
614 .expect("seed row");
615
616 runtime.execute_query("BEGIN").expect("begin");
617 runtime
618 .execute_query("DELETE FROM docs WHERE id = 1")
619 .expect("delete in transaction");
620 let deleted = runtime
621 .scan_collection("docs", None, 1)
622 .expect("scan own transaction");
623 assert_eq!(deleted.total, 0);
624 assert!(deleted.items.is_empty());
625
626 runtime.execute_query("ROLLBACK").expect("rollback");
627 let restored = runtime
628 .scan_collection("docs", None, 1)
629 .expect("scan after rollback");
630 assert_eq!(restored.total, 1);
631 assert_eq!(restored.items.len(), 1);
632 assert_eq!(text_field(&restored.items[0], "body"), Some("kept"));
633 assert_eq!(restored.next, None);
634 }
635
636 #[test]
637 fn typed_entity_boundary_uses_model_specific_rls_policies() {
638 let runtime = RedDBRuntime::in_memory().expect("runtime");
639 for query in [
640 "CREATE GRAPH links",
641 "INSERT INTO links NODE (label,node_type,visible) VALUES ('shown','Item',true)",
642 "INSERT INTO links NODE (label,node_type,visible) VALUES ('hidden','Item',false)",
643 "CREATE POLICY visible_nodes ON NODES OF links FOR SELECT USING (properties.visible=true)",
644 "ALTER TABLE links ENABLE ROW LEVEL SECURITY",
645 "CREATE DOCUMENT papers",
646 "INSERT INTO papers DOCUMENT VALUES ({name:'shown',visible:true})",
647 "INSERT INTO papers DOCUMENT VALUES ({name:'hidden',visible:false})",
648 "CREATE POLICY visible_documents ON DOCUMENTS OF papers FOR SELECT USING (visible=true)",
649 "ALTER TABLE papers ENABLE ROW LEVEL SECURITY",
650 ] {
651 runtime.execute_query(query).expect(query);
652 }
653
654 let bundle = SnapshotBundle {
655 snapshot: None,
656 auth: Some(("alice".to_string(), Role::Read)),
657 auth_principal: Some(crate::auth::UserId::platform("alice")),
658 tenant: None,
659 };
660 with_snapshot_bundle(&bundle, || {
661 let nodes = runtime
662 .scan_collection("links", None, 10)
663 .expect("typed graph scan");
664 assert_eq!(nodes.total, 1);
665 assert_eq!(nodes.items.len(), 1);
666
667 let documents = runtime
668 .scan_collection("papers", None, 10)
669 .expect("typed document scan");
670 assert_eq!(documents.total, 1);
671 assert_eq!(documents.items.len(), 1);
672 });
673 }
674}