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 = crate::runtime::execution_context::capture_current_snapshot()
259 .unwrap_or_else(|| {
260 let context = self
261 .inner
262 .transaction_state
263 .context(crate::runtime::execution_context::current_connection_id());
264 let serializable_reader = context.as_ref().and_then(|context| {
265 (context.isolation == crate::storage::transaction::IsolationLevel::Serializable)
266 .then_some(context.xid)
267 });
268 crate::runtime::execution_context::SnapshotContext {
269 snapshot: self.current_snapshot(),
270 manager: self.snapshot_manager(),
271 own_xids: self.own_transaction_xids(),
272 requires_index_fallback: true,
273 serializable_reader,
274 }
275 });
276 let apply_rls = crate::runtime::execution_context::current_auth_identity().is_some()
277 && crate::runtime::rls_injection::rls_is_enabled(self, collection);
278 let (total, items) = manager.try_page_in_id_order_where_at_snapshot(
279 Some(&snapshot),
280 offset,
281 limit,
282 read_bytes,
283 |entity| {
284 !apply_rls
285 || crate::runtime::rls_injection::entity_passes_rls_action(
286 self,
287 collection,
288 PolicyAction::Select,
289 entity,
290 )
291 },
292 )?;
293 let end = total.min(offset.saturating_add(limit));
294 let next = (end < total).then_some(ScanCursor { offset: end });
295
296 Ok(ScanPage {
297 collection: collection.to_string(),
298 items,
299 next,
300 total,
301 })
302 }
303
304 pub fn get_collection_entity(
305 &self,
306 collection: &str,
307 id: crate::storage::EntityId,
308 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
309 use reddb_rql::ast::PolicyAction;
310
311 self.check_collection_privilege(PolicyAction::Select, collection)?;
312 let entity = self.inner.db.store().get(collection, id);
313 Ok(entity.filter(|entity| {
314 crate::runtime::rls_injection::entity_passes_rls_action(
315 self,
316 collection,
317 PolicyAction::Select,
318 entity,
319 )
320 }))
321 }
322
323 pub fn catalog(&self) -> CatalogModelSnapshot {
324 self.inner.db.catalog_model_snapshot()
325 }
326
327 pub fn try_catalog(&self) -> RedDBResult<CatalogModelSnapshot> {
330 if self
331 .inner
332 .db
333 .store()
334 .config()
335 .resident_memory_bytes_max
336 .is_none()
337 {
338 return Ok(self.catalog());
339 }
340 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
341 .unwrap_or(usize::MAX)
342 .clamp(1, 16 * 1024 * 1024);
343 let _scratch = self
344 .try_reserve_query_buffer(read_bytes * 2)
345 .ok_or_else(|| {
346 self.memory_accounting().record_admission_denied();
347 RedDBError::MaterializationLimitExceeded {
348 executor: "catalog",
349 limit: usize::try_from(self.memory_budget().resolved_bytes)
350 .unwrap_or(usize::MAX),
351 current: usize::try_from(self.memory_accounting().total_used_bytes())
352 .unwrap_or(usize::MAX)
353 .saturating_add(1),
354 }
355 })?;
356 Ok(self.inner.db.try_catalog_model_snapshot(read_bytes)?)
357 }
358
359 pub(crate) fn try_catalog_for_collections(
363 &self,
364 collection_names: &std::collections::HashSet<String>,
365 observe_legacy_payloads: bool,
366 ) -> RedDBResult<CatalogModelSnapshot> {
367 if self
368 .inner
369 .db
370 .store()
371 .config()
372 .resident_memory_bytes_max
373 .is_none()
374 {
375 return Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
376 collection_names,
377 observe_legacy_payloads,
378 usize::MAX,
379 )?);
380 }
381 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
382 .unwrap_or(usize::MAX)
383 .clamp(1, 16 * 1024 * 1024);
384 let _scratch = self
385 .try_reserve_query_buffer(read_bytes * 2)
386 .ok_or_else(|| {
387 self.memory_accounting().record_admission_denied();
388 RedDBError::MaterializationLimitExceeded {
389 executor: "catalog",
390 limit: usize::try_from(self.memory_budget().resolved_bytes)
391 .unwrap_or(usize::MAX),
392 current: usize::try_from(self.memory_accounting().total_used_bytes())
393 .unwrap_or(usize::MAX)
394 .saturating_add(1),
395 }
396 })?;
397 Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
398 collection_names,
399 observe_legacy_payloads,
400 read_bytes,
401 )?)
402 }
403
404 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
405 self.inner.db.catalog_consistency_report()
406 }
407
408 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
409 crate::catalog::attention_summary(&self.catalog())
410 }
411
412 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
413 crate::catalog::collection_attention(&self.catalog())
414 }
415
416 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
417 crate::catalog::index_attention(&self.catalog())
418 }
419
420 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
421 crate::catalog::graph_projection_attention(&self.catalog())
422 }
423
424 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
425 crate::catalog::analytics_job_attention(&self.catalog())
426 }
427
428 pub fn stats(&self) -> RuntimeStats {
429 let pool = runtime_pool_lock(self);
430 RuntimeStats {
431 active_connections: pool.active,
432 idle_connections: pool.idle.len(),
433 total_checkouts: pool.total_checkouts,
434 paged_mode: self.inner.db.is_paged(),
435 started_at_unix_ms: self.inner.started_at_unix_ms,
436 store: self.inner.db.stats(),
437 system: SystemInfo::collect(),
438 result_blob_cache: self.inner.result_blob_cache.stats(),
439 kv: self.inner.kv_stats.snapshot(),
440 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
441 }
442 }
443}
444
445#[cfg(test)]
446mod security_boundary_tests {
447 use super::*;
448 use crate::application::entity::{DeleteEntityInput, PatchEntityInput};
449 use crate::application::ports::RuntimeEntityPort;
450 use crate::auth::Role;
451 use crate::runtime::execution_context::{with_snapshot_bundle, SnapshotBundle};
452 use crate::storage::{EntityData, UnifiedEntity};
453 use reddb_types::Value;
454
455 fn text_field<'a>(entity: &'a UnifiedEntity, name: &str) -> Option<&'a str> {
456 let EntityData::Row(row) = &entity.data else {
457 return None;
458 };
459 let value = row
460 .named
461 .as_ref()
462 .and_then(|named| named.get(name))
463 .or_else(|| {
464 row.schema.as_ref().and_then(|schema| {
465 schema
466 .iter()
467 .position(|column| column == name)
468 .and_then(|index| row.columns.get(index))
469 })
470 })?;
471 match value {
472 Value::Text(value) => Some(value.as_ref()),
473 _ => None,
474 }
475 }
476
477 #[test]
478 fn typed_entity_boundary_filters_before_pagination_and_hides_foreign_ids() {
479 let runtime = RedDBRuntime::in_memory().expect("runtime");
480 runtime
481 .execute_query(
482 "CREATE TABLE docs (id INT, tenant_id TEXT, body TEXT) TENANT BY (tenant_id)",
483 )
484 .expect("tenant table");
485 runtime
486 .execute_query(
487 "INSERT INTO docs (id, tenant_id, body) VALUES \
488 (1, 'acme', 'first'), (2, 'globex', 'hidden'), (3, 'acme', 'second')",
489 )
490 .expect("seed platform rows");
491
492 let unfiltered = runtime
493 .scan_collection("docs", None, 10)
494 .expect("platform scan");
495 let foreign_id = unfiltered
496 .items
497 .iter()
498 .find(|entity| text_field(entity, "tenant_id") == Some("globex"))
499 .map(|entity| entity.id)
500 .expect("foreign row");
501
502 let bundle = SnapshotBundle {
503 snapshot: None,
504 auth: Some(("alice".to_string(), Role::Write)),
505 auth_principal: Some(crate::auth::UserId::scoped("acme", "alice")),
506 tenant: Some("acme".to_string()),
507 };
508 with_snapshot_bundle(&bundle, || {
509 let first = runtime
510 .scan_collection("docs", None, 1)
511 .expect("first visible page");
512 assert_eq!(first.total, 2);
513 assert_eq!(first.items.len(), 1);
514 assert_eq!(text_field(&first.items[0], "tenant_id"), Some("acme"));
515 assert_eq!(first.next, Some(ScanCursor { offset: 1 }));
516
517 let second = runtime
518 .scan_collection("docs", first.next, 1)
519 .expect("second visible page");
520 assert_eq!(second.total, 2);
521 assert_eq!(second.items.len(), 1);
522 assert_eq!(text_field(&second.items[0], "tenant_id"), Some("acme"));
523 assert_eq!(second.next, None);
524
525 assert!(runtime
526 .get_collection_entity("docs", foreign_id)
527 .expect("typed get")
528 .is_none());
529 assert!(matches!(
530 runtime.patch_entity(PatchEntityInput {
531 collection: "docs".to_string(),
532 id: foreign_id,
533 payload: crate::json::Value::Object(Default::default()),
534 operations: Vec::new(),
535 }),
536 Err(crate::RedDBError::NotFound(_))
537 ));
538 let deleted = runtime
539 .delete_entity(DeleteEntityInput {
540 collection: "docs".to_string(),
541 id: foreign_id,
542 })
543 .expect("typed delete");
544 assert!(!deleted.deleted);
545 });
546
547 assert!(runtime.db().store().get("docs", foreign_id).is_some());
548 }
549
550 #[test]
551 fn scan_collection_pages_the_mvcc_view_across_rollback() {
552 let runtime = RedDBRuntime::in_memory().expect("runtime");
553 runtime
554 .execute_query("CREATE TABLE docs (id INT, body TEXT)")
555 .expect("table");
556 runtime
557 .execute_query("INSERT INTO docs (id, body) VALUES (1, 'kept')")
558 .expect("seed row");
559
560 runtime.execute_query("BEGIN").expect("begin");
561 runtime
562 .execute_query("DELETE FROM docs WHERE id = 1")
563 .expect("delete in transaction");
564 let deleted = runtime
565 .scan_collection("docs", None, 1)
566 .expect("scan own transaction");
567 assert_eq!(deleted.total, 0);
568 assert!(deleted.items.is_empty());
569
570 runtime.execute_query("ROLLBACK").expect("rollback");
571 let restored = runtime
572 .scan_collection("docs", None, 1)
573 .expect("scan after rollback");
574 assert_eq!(restored.total, 1);
575 assert_eq!(restored.items.len(), 1);
576 assert_eq!(text_field(&restored.items[0], "body"), Some("kept"));
577 assert_eq!(restored.next, None);
578 }
579
580 #[test]
581 fn typed_entity_boundary_uses_model_specific_rls_policies() {
582 let runtime = RedDBRuntime::in_memory().expect("runtime");
583 for query in [
584 "CREATE GRAPH links",
585 "INSERT INTO links NODE (label,node_type,visible) VALUES ('shown','Item',true)",
586 "INSERT INTO links NODE (label,node_type,visible) VALUES ('hidden','Item',false)",
587 "CREATE POLICY visible_nodes ON NODES OF links FOR SELECT USING (properties.visible=true)",
588 "ALTER TABLE links ENABLE ROW LEVEL SECURITY",
589 "CREATE DOCUMENT papers",
590 "INSERT INTO papers DOCUMENT VALUES ({name:'shown',visible:true})",
591 "INSERT INTO papers DOCUMENT VALUES ({name:'hidden',visible:false})",
592 "CREATE POLICY visible_documents ON DOCUMENTS OF papers FOR SELECT USING (visible=true)",
593 "ALTER TABLE papers ENABLE ROW LEVEL SECURITY",
594 ] {
595 runtime.execute_query(query).expect(query);
596 }
597
598 let bundle = SnapshotBundle {
599 snapshot: None,
600 auth: Some(("alice".to_string(), Role::Read)),
601 auth_principal: Some(crate::auth::UserId::platform("alice")),
602 tenant: None,
603 };
604 with_snapshot_bundle(&bundle, || {
605 let nodes = runtime
606 .scan_collection("links", None, 10)
607 .expect("typed graph scan");
608 assert_eq!(nodes.total, 1);
609 assert_eq!(nodes.items.len(), 1);
610
611 let documents = runtime
612 .scan_collection("papers", None, 10)
613 .expect("typed document scan");
614 assert_eq!(documents.total, 1);
615 assert_eq!(documents.items.len(), 1);
616 });
617 }
618}