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<()> {
135 if self.inner.db.remote_backend.is_none() {
136 return Ok(());
137 }
138 match self.inner.write_gate.lease_state() {
139 crate::runtime::write_gate::LeaseGateState::NotHeld => {
140 self.inner.audit_log.record(
141 action,
142 "system",
143 "remote_backend",
144 "err: writer lease not held",
145 crate::json::Value::Null,
146 );
147 Err(RedDBError::ReadOnly(format!(
148 "writer lease not held — {action} blocked (serverless fence)"
149 )))
150 }
151 _ => Ok(()),
152 }
153 }
154
155 pub(super) fn publish_remote_snapshot(&self) -> RedDBResult<()> {
156 if let Some(lifecycle) = self.lease_lifecycle() {
157 let store = self.inner.db.store();
158 let _publication = store.vault_publication_lock.lock();
161 if lifecycle.publish_snapshot(|| self.capture_serverless_snapshot())? {
162 return Ok(());
163 }
164 }
165 self.inner
166 .db
167 .upload_to_remote_backend()
168 .map_err(|err| RedDBError::Engine(err.to_string()))
169 }
170
171 pub(super) fn capture_serverless_snapshot(
172 &self,
173 ) -> RedDBResult<reddb_file::ServerlessWriterLeaseTempFile> {
174 use reddb_file::VfsFile;
175 use std::io::Write;
176 let store = self.inner.db.store();
177 let _publication = store.vault_publication_lock.lock();
178 self.checkpoint_local()?;
179 let snapshot = reddb_file::ServerlessWriterLeaseTempFile::new("snapshot-payload");
180 if let Some(pager) = store.pager() {
181 pager
184 .with_file(|source| -> std::io::Result<()> {
185 let mut output = std::fs::File::create(snapshot.path())?;
186 source.seek(std::io::SeekFrom::Start(0))?;
187 let mut buffer = [0u8; 65536];
188 loop {
189 let count = source.read(&mut buffer)?;
190 if count == 0 {
191 break;
192 }
193 output.write_all(&buffer[..count])?;
194 }
195 output.sync_all()
196 })
197 .map_err(|err| RedDBError::Internal(err.to_string()))?
198 .map_err(|err| RedDBError::Internal(err.to_string()))?;
199 } else {
200 let path = self.inner.db.path().ok_or_else(|| {
201 RedDBError::InvalidOperation(
202 "serverless snapshots require persistent local storage".into(),
203 )
204 })?;
205 std::fs::copy(path, snapshot.path())
206 .map_err(|err| RedDBError::Internal(err.to_string()))?;
207 }
208 Ok(snapshot)
209 }
210
211 pub fn run_maintenance(&self) -> RedDBResult<()> {
212 self.inner
213 .db
214 .run_maintenance()
215 .map_err(|err| RedDBError::Internal(err.to_string()))
216 }
217
218 pub fn scan_collection(
219 &self,
220 collection: &str,
221 cursor: Option<ScanCursor>,
222 limit: usize,
223 ) -> RedDBResult<ScanPage> {
224 use reddb_rql::ast::PolicyAction;
225
226 self.check_collection_privilege(PolicyAction::Select, collection)?;
227 let store = self.inner.db.store();
228 let manager = store
229 .get_collection(collection)
230 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
231
232 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
233 let limit = limit.max(1);
234 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
235 .unwrap_or(usize::MAX)
236 .clamp(1, 16 * 1024 * 1024);
237 let (total, items) = if crate::runtime::execution_context::current_auth_identity().is_some()
238 && crate::runtime::rls_injection::rls_is_enabled(self, collection)
239 {
240 manager.try_page_in_id_order_where(offset, limit, read_bytes, |entity| {
241 crate::runtime::rls_injection::entity_passes_rls_action(
242 self,
243 collection,
244 PolicyAction::Select,
245 entity,
246 )
247 })?
248 } else {
249 manager.try_page_in_id_order(offset, limit, read_bytes)?
250 };
251 let end = total.min(offset.saturating_add(limit));
252 let next = (end < total).then_some(ScanCursor { offset: end });
253
254 Ok(ScanPage {
255 collection: collection.to_string(),
256 items,
257 next,
258 total,
259 })
260 }
261
262 pub fn get_collection_entity(
263 &self,
264 collection: &str,
265 id: crate::storage::EntityId,
266 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
267 use reddb_rql::ast::PolicyAction;
268
269 self.check_collection_privilege(PolicyAction::Select, collection)?;
270 let entity = self.inner.db.store().get(collection, id);
271 Ok(entity.filter(|entity| {
272 crate::runtime::rls_injection::entity_passes_rls_action(
273 self,
274 collection,
275 PolicyAction::Select,
276 entity,
277 )
278 }))
279 }
280
281 pub fn catalog(&self) -> CatalogModelSnapshot {
282 self.inner.db.catalog_model_snapshot()
283 }
284
285 pub fn try_catalog(&self) -> RedDBResult<CatalogModelSnapshot> {
288 if self
289 .inner
290 .db
291 .store()
292 .config()
293 .resident_memory_bytes_max
294 .is_none()
295 {
296 return Ok(self.catalog());
297 }
298 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
299 .unwrap_or(usize::MAX)
300 .clamp(1, 16 * 1024 * 1024);
301 let _scratch = self
302 .try_reserve_query_buffer(read_bytes * 2)
303 .ok_or_else(|| {
304 self.memory_accounting().record_admission_denied();
305 RedDBError::MaterializationLimitExceeded {
306 executor: "catalog",
307 limit: usize::try_from(self.memory_budget().resolved_bytes)
308 .unwrap_or(usize::MAX),
309 current: usize::try_from(self.memory_accounting().total_used_bytes())
310 .unwrap_or(usize::MAX)
311 .saturating_add(1),
312 }
313 })?;
314 Ok(self.inner.db.try_catalog_model_snapshot(read_bytes)?)
315 }
316
317 pub(crate) fn try_catalog_for_collections(
321 &self,
322 collection_names: &std::collections::HashSet<String>,
323 observe_legacy_payloads: bool,
324 ) -> RedDBResult<CatalogModelSnapshot> {
325 if self
326 .inner
327 .db
328 .store()
329 .config()
330 .resident_memory_bytes_max
331 .is_none()
332 {
333 return Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
334 collection_names,
335 observe_legacy_payloads,
336 usize::MAX,
337 )?);
338 }
339 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
340 .unwrap_or(usize::MAX)
341 .clamp(1, 16 * 1024 * 1024);
342 let _scratch = self
343 .try_reserve_query_buffer(read_bytes * 2)
344 .ok_or_else(|| {
345 self.memory_accounting().record_admission_denied();
346 RedDBError::MaterializationLimitExceeded {
347 executor: "catalog",
348 limit: usize::try_from(self.memory_budget().resolved_bytes)
349 .unwrap_or(usize::MAX),
350 current: usize::try_from(self.memory_accounting().total_used_bytes())
351 .unwrap_or(usize::MAX)
352 .saturating_add(1),
353 }
354 })?;
355 Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
356 collection_names,
357 observe_legacy_payloads,
358 read_bytes,
359 )?)
360 }
361
362 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
363 self.inner.db.catalog_consistency_report()
364 }
365
366 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
367 crate::catalog::attention_summary(&self.catalog())
368 }
369
370 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
371 crate::catalog::collection_attention(&self.catalog())
372 }
373
374 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
375 crate::catalog::index_attention(&self.catalog())
376 }
377
378 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
379 crate::catalog::graph_projection_attention(&self.catalog())
380 }
381
382 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
383 crate::catalog::analytics_job_attention(&self.catalog())
384 }
385
386 pub fn stats(&self) -> RuntimeStats {
387 let pool = runtime_pool_lock(self);
388 RuntimeStats {
389 active_connections: pool.active,
390 idle_connections: pool.idle.len(),
391 total_checkouts: pool.total_checkouts,
392 paged_mode: self.inner.db.is_paged(),
393 started_at_unix_ms: self.inner.started_at_unix_ms,
394 store: self.inner.db.stats(),
395 system: SystemInfo::collect(),
396 result_blob_cache: self.inner.result_blob_cache.stats(),
397 kv: self.inner.kv_stats.snapshot(),
398 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
399 }
400 }
401}
402
403#[cfg(test)]
404mod security_boundary_tests {
405 use super::*;
406 use crate::application::entity::{DeleteEntityInput, PatchEntityInput};
407 use crate::application::ports::RuntimeEntityPort;
408 use crate::auth::Role;
409 use crate::runtime::execution_context::{with_snapshot_bundle, SnapshotBundle};
410 use crate::storage::{EntityData, UnifiedEntity};
411 use reddb_types::Value;
412
413 fn text_field<'a>(entity: &'a UnifiedEntity, name: &str) -> Option<&'a str> {
414 let EntityData::Row(row) = &entity.data else {
415 return None;
416 };
417 let value = row
418 .named
419 .as_ref()
420 .and_then(|named| named.get(name))
421 .or_else(|| {
422 row.schema.as_ref().and_then(|schema| {
423 schema
424 .iter()
425 .position(|column| column == name)
426 .and_then(|index| row.columns.get(index))
427 })
428 })?;
429 match value {
430 Value::Text(value) => Some(value.as_ref()),
431 _ => None,
432 }
433 }
434
435 #[test]
436 fn typed_entity_boundary_filters_before_pagination_and_hides_foreign_ids() {
437 let runtime = RedDBRuntime::in_memory().expect("runtime");
438 runtime
439 .execute_query(
440 "CREATE TABLE docs (id INT, tenant_id TEXT, body TEXT) TENANT BY (tenant_id)",
441 )
442 .expect("tenant table");
443 runtime
444 .execute_query(
445 "INSERT INTO docs (id, tenant_id, body) VALUES \
446 (1, 'acme', 'first'), (2, 'globex', 'hidden'), (3, 'acme', 'second')",
447 )
448 .expect("seed platform rows");
449
450 let unfiltered = runtime
451 .scan_collection("docs", None, 10)
452 .expect("platform scan");
453 let foreign_id = unfiltered
454 .items
455 .iter()
456 .find(|entity| text_field(entity, "tenant_id") == Some("globex"))
457 .map(|entity| entity.id)
458 .expect("foreign row");
459
460 let bundle = SnapshotBundle {
461 snapshot: None,
462 auth: Some(("alice".to_string(), Role::Write)),
463 auth_principal: Some(crate::auth::UserId::scoped("acme", "alice")),
464 tenant: Some("acme".to_string()),
465 };
466 with_snapshot_bundle(&bundle, || {
467 let first = runtime
468 .scan_collection("docs", None, 1)
469 .expect("first visible page");
470 assert_eq!(first.total, 2);
471 assert_eq!(first.items.len(), 1);
472 assert_eq!(text_field(&first.items[0], "tenant_id"), Some("acme"));
473 assert_eq!(first.next, Some(ScanCursor { offset: 1 }));
474
475 let second = runtime
476 .scan_collection("docs", first.next, 1)
477 .expect("second visible page");
478 assert_eq!(second.total, 2);
479 assert_eq!(second.items.len(), 1);
480 assert_eq!(text_field(&second.items[0], "tenant_id"), Some("acme"));
481 assert_eq!(second.next, None);
482
483 assert!(runtime
484 .get_collection_entity("docs", foreign_id)
485 .expect("typed get")
486 .is_none());
487 assert!(matches!(
488 runtime.patch_entity(PatchEntityInput {
489 collection: "docs".to_string(),
490 id: foreign_id,
491 payload: crate::json::Value::Object(Default::default()),
492 operations: Vec::new(),
493 }),
494 Err(crate::RedDBError::NotFound(_))
495 ));
496 let deleted = runtime
497 .delete_entity(DeleteEntityInput {
498 collection: "docs".to_string(),
499 id: foreign_id,
500 })
501 .expect("typed delete");
502 assert!(!deleted.deleted);
503 });
504
505 assert!(runtime.db().store().get("docs", foreign_id).is_some());
506 }
507
508 #[test]
509 fn typed_entity_boundary_uses_model_specific_rls_policies() {
510 let runtime = RedDBRuntime::in_memory().expect("runtime");
511 for query in [
512 "CREATE GRAPH links",
513 "INSERT INTO links NODE (label,node_type,visible) VALUES ('shown','Item',true)",
514 "INSERT INTO links NODE (label,node_type,visible) VALUES ('hidden','Item',false)",
515 "CREATE POLICY visible_nodes ON NODES OF links FOR SELECT USING (properties.visible=true)",
516 "ALTER TABLE links ENABLE ROW LEVEL SECURITY",
517 "CREATE DOCUMENT papers",
518 "INSERT INTO papers DOCUMENT VALUES ({name:'shown',visible:true})",
519 "INSERT INTO papers DOCUMENT VALUES ({name:'hidden',visible:false})",
520 "CREATE POLICY visible_documents ON DOCUMENTS OF papers FOR SELECT USING (visible=true)",
521 "ALTER TABLE papers ENABLE ROW LEVEL SECURITY",
522 ] {
523 runtime.execute_query(query).expect(query);
524 }
525
526 let bundle = SnapshotBundle {
527 snapshot: None,
528 auth: Some(("alice".to_string(), Role::Read)),
529 auth_principal: Some(crate::auth::UserId::platform("alice")),
530 tenant: None,
531 };
532 with_snapshot_bundle(&bundle, || {
533 let nodes = runtime
534 .scan_collection("links", None, 10)
535 .expect("typed graph scan");
536 assert_eq!(nodes.total, 1);
537 assert_eq!(nodes.items.len(), 1);
538
539 let documents = runtime
540 .scan_collection("papers", None, 10)
541 .expect("typed document scan");
542 assert_eq!(documents.total, 1);
543 assert_eq!(documents.items.len(), 1);
544 });
545 }
546}