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.inner
81 .db
82 .upload_to_remote_backend()
83 .map_err(|err| RedDBError::Engine(err.to_string()))
84 }
85
86 pub(super) fn checkpoint_local(&self) -> RedDBResult<()> {
87 let started = std::time::Instant::now();
88 self.seal_hypertable_chunks_for_checkpoint(
89 self.inner.checkpoint_columnar_emission_budget_chunks,
90 )?;
91 let store = self.inner.db.store();
92 let publication = store.vault_publication_lock.lock();
93 self.prepare_paged_counts()?;
96 self.persist_probabilistic_snapshots()?;
97 self.inner
98 .db
99 .flush_local_after_runtime_snapshots()
100 .map_err(|err| {
101 let msg = err.to_string();
106 crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
107 lsn: 0,
108 error: msg.clone(),
109 }
110 .emit_global();
111 crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
112 path: "<flush_local_only>".to_string(),
113 error: msg.clone(),
114 }
115 .emit_global();
116 RedDBError::Engine(msg)
117 })?;
118 self.index_store_ref().clear_paged_btree_overlays();
122 self.inner.probabilistic.replayed_names.lock().clear();
123 drop(publication);
124 let checkpoint_lsn = self.cdc_current_lsn();
125 self.inner
126 .checkpoint_projection_stats
127 .record_checkpoint(checkpoint_lsn, started.elapsed().as_millis() as u64);
128 Ok(())
129 }
130
131 pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
138 if self.inner.db.remote_backend.is_none() {
139 return Ok(());
140 }
141 match self.inner.write_gate.lease_state() {
142 crate::runtime::write_gate::LeaseGateState::NotHeld => {
143 self.inner.audit_log.record(
144 action,
145 "system",
146 "remote_backend",
147 "err: writer lease not held",
148 crate::json::Value::Null,
149 );
150 Err(RedDBError::ReadOnly(format!(
151 "writer lease not held — {action} blocked (serverless fence)"
152 )))
153 }
154 _ => Ok(()),
155 }
156 }
157
158 pub fn run_maintenance(&self) -> RedDBResult<()> {
159 self.inner
160 .db
161 .run_maintenance()
162 .map_err(|err| RedDBError::Internal(err.to_string()))
163 }
164
165 pub fn scan_collection(
166 &self,
167 collection: &str,
168 cursor: Option<ScanCursor>,
169 limit: usize,
170 ) -> RedDBResult<ScanPage> {
171 use reddb_rql::ast::PolicyAction;
172
173 self.check_collection_privilege(PolicyAction::Select, collection)?;
174 let store = self.inner.db.store();
175 let manager = store
176 .get_collection(collection)
177 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
178
179 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
180 let limit = limit.max(1);
181 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
182 .unwrap_or(usize::MAX)
183 .clamp(1, 16 * 1024 * 1024);
184 let (total, items) = if crate::runtime::execution_context::current_auth_identity().is_some()
185 && crate::runtime::rls_injection::rls_is_enabled(self, collection)
186 {
187 manager.try_page_in_id_order_where(offset, limit, read_bytes, |entity| {
188 crate::runtime::rls_injection::entity_passes_rls_action(
189 self,
190 collection,
191 PolicyAction::Select,
192 entity,
193 )
194 })?
195 } else {
196 manager.try_page_in_id_order(offset, limit, read_bytes)?
197 };
198 let end = total.min(offset.saturating_add(limit));
199 let next = (end < total).then_some(ScanCursor { offset: end });
200
201 Ok(ScanPage {
202 collection: collection.to_string(),
203 items,
204 next,
205 total,
206 })
207 }
208
209 pub fn get_collection_entity(
210 &self,
211 collection: &str,
212 id: crate::storage::EntityId,
213 ) -> RedDBResult<Option<crate::storage::UnifiedEntity>> {
214 use reddb_rql::ast::PolicyAction;
215
216 self.check_collection_privilege(PolicyAction::Select, collection)?;
217 let entity = self.inner.db.store().get(collection, id);
218 Ok(entity.filter(|entity| {
219 crate::runtime::rls_injection::entity_passes_rls_action(
220 self,
221 collection,
222 PolicyAction::Select,
223 entity,
224 )
225 }))
226 }
227
228 pub fn catalog(&self) -> CatalogModelSnapshot {
229 self.inner.db.catalog_model_snapshot()
230 }
231
232 pub fn try_catalog(&self) -> RedDBResult<CatalogModelSnapshot> {
235 if self
236 .inner
237 .db
238 .store()
239 .config()
240 .resident_memory_bytes_max
241 .is_none()
242 {
243 return Ok(self.catalog());
244 }
245 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
246 .unwrap_or(usize::MAX)
247 .clamp(1, 16 * 1024 * 1024);
248 let _scratch = self
249 .try_reserve_query_buffer(read_bytes * 2)
250 .ok_or_else(|| {
251 self.memory_accounting().record_admission_denied();
252 RedDBError::MaterializationLimitExceeded {
253 executor: "catalog",
254 limit: usize::try_from(self.memory_budget().resolved_bytes)
255 .unwrap_or(usize::MAX),
256 current: usize::try_from(self.memory_accounting().total_used_bytes())
257 .unwrap_or(usize::MAX)
258 .saturating_add(1),
259 }
260 })?;
261 Ok(self.inner.db.try_catalog_model_snapshot(read_bytes)?)
262 }
263
264 pub(crate) fn try_catalog_for_collections(
268 &self,
269 collection_names: &std::collections::HashSet<String>,
270 observe_legacy_payloads: bool,
271 ) -> RedDBResult<CatalogModelSnapshot> {
272 if self
273 .inner
274 .db
275 .store()
276 .config()
277 .resident_memory_bytes_max
278 .is_none()
279 {
280 return Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
281 collection_names,
282 observe_legacy_payloads,
283 usize::MAX,
284 )?);
285 }
286 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
287 .unwrap_or(usize::MAX)
288 .clamp(1, 16 * 1024 * 1024);
289 let _scratch = self
290 .try_reserve_query_buffer(read_bytes * 2)
291 .ok_or_else(|| {
292 self.memory_accounting().record_admission_denied();
293 RedDBError::MaterializationLimitExceeded {
294 executor: "catalog",
295 limit: usize::try_from(self.memory_budget().resolved_bytes)
296 .unwrap_or(usize::MAX),
297 current: usize::try_from(self.memory_accounting().total_used_bytes())
298 .unwrap_or(usize::MAX)
299 .saturating_add(1),
300 }
301 })?;
302 Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
303 collection_names,
304 observe_legacy_payloads,
305 read_bytes,
306 )?)
307 }
308
309 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
310 self.inner.db.catalog_consistency_report()
311 }
312
313 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
314 crate::catalog::attention_summary(&self.catalog())
315 }
316
317 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
318 crate::catalog::collection_attention(&self.catalog())
319 }
320
321 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
322 crate::catalog::index_attention(&self.catalog())
323 }
324
325 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
326 crate::catalog::graph_projection_attention(&self.catalog())
327 }
328
329 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
330 crate::catalog::analytics_job_attention(&self.catalog())
331 }
332
333 pub fn stats(&self) -> RuntimeStats {
334 let pool = runtime_pool_lock(self);
335 RuntimeStats {
336 active_connections: pool.active,
337 idle_connections: pool.idle.len(),
338 total_checkouts: pool.total_checkouts,
339 paged_mode: self.inner.db.is_paged(),
340 started_at_unix_ms: self.inner.started_at_unix_ms,
341 store: self.inner.db.stats(),
342 system: SystemInfo::collect(),
343 result_blob_cache: self.inner.result_blob_cache.stats(),
344 kv: self.inner.kv_stats.snapshot(),
345 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
346 }
347 }
348}
349
350#[cfg(test)]
351mod security_boundary_tests {
352 use super::*;
353 use crate::application::entity::{DeleteEntityInput, PatchEntityInput};
354 use crate::application::ports::RuntimeEntityPort;
355 use crate::auth::Role;
356 use crate::runtime::execution_context::{with_snapshot_bundle, SnapshotBundle};
357 use crate::storage::{EntityData, UnifiedEntity};
358 use reddb_types::Value;
359
360 fn text_field<'a>(entity: &'a UnifiedEntity, name: &str) -> Option<&'a str> {
361 let EntityData::Row(row) = &entity.data else {
362 return None;
363 };
364 let value = row
365 .named
366 .as_ref()
367 .and_then(|named| named.get(name))
368 .or_else(|| {
369 row.schema.as_ref().and_then(|schema| {
370 schema
371 .iter()
372 .position(|column| column == name)
373 .and_then(|index| row.columns.get(index))
374 })
375 })?;
376 match value {
377 Value::Text(value) => Some(value.as_ref()),
378 _ => None,
379 }
380 }
381
382 #[test]
383 fn typed_entity_boundary_filters_before_pagination_and_hides_foreign_ids() {
384 let runtime = RedDBRuntime::in_memory().expect("runtime");
385 runtime
386 .execute_query(
387 "CREATE TABLE docs (id INT, tenant_id TEXT, body TEXT) TENANT BY (tenant_id)",
388 )
389 .expect("tenant table");
390 runtime
391 .execute_query(
392 "INSERT INTO docs (id, tenant_id, body) VALUES \
393 (1, 'acme', 'first'), (2, 'globex', 'hidden'), (3, 'acme', 'second')",
394 )
395 .expect("seed platform rows");
396
397 let unfiltered = runtime
398 .scan_collection("docs", None, 10)
399 .expect("platform scan");
400 let foreign_id = unfiltered
401 .items
402 .iter()
403 .find(|entity| text_field(entity, "tenant_id") == Some("globex"))
404 .map(|entity| entity.id)
405 .expect("foreign row");
406
407 let bundle = SnapshotBundle {
408 snapshot: None,
409 auth: Some(("alice".to_string(), Role::Write)),
410 auth_principal: Some(crate::auth::UserId::scoped("acme", "alice")),
411 tenant: Some("acme".to_string()),
412 };
413 with_snapshot_bundle(&bundle, || {
414 let first = runtime
415 .scan_collection("docs", None, 1)
416 .expect("first visible page");
417 assert_eq!(first.total, 2);
418 assert_eq!(first.items.len(), 1);
419 assert_eq!(text_field(&first.items[0], "tenant_id"), Some("acme"));
420 assert_eq!(first.next, Some(ScanCursor { offset: 1 }));
421
422 let second = runtime
423 .scan_collection("docs", first.next, 1)
424 .expect("second visible page");
425 assert_eq!(second.total, 2);
426 assert_eq!(second.items.len(), 1);
427 assert_eq!(text_field(&second.items[0], "tenant_id"), Some("acme"));
428 assert_eq!(second.next, None);
429
430 assert!(runtime
431 .get_collection_entity("docs", foreign_id)
432 .expect("typed get")
433 .is_none());
434 assert!(matches!(
435 runtime.patch_entity(PatchEntityInput {
436 collection: "docs".to_string(),
437 id: foreign_id,
438 payload: crate::json::Value::Object(Default::default()),
439 operations: Vec::new(),
440 }),
441 Err(crate::RedDBError::NotFound(_))
442 ));
443 let deleted = runtime
444 .delete_entity(DeleteEntityInput {
445 collection: "docs".to_string(),
446 id: foreign_id,
447 })
448 .expect("typed delete");
449 assert!(!deleted.deleted);
450 });
451
452 assert!(runtime.db().store().get("docs", foreign_id).is_some());
453 }
454
455 #[test]
456 fn typed_entity_boundary_uses_model_specific_rls_policies() {
457 let runtime = RedDBRuntime::in_memory().expect("runtime");
458 for query in [
459 "CREATE GRAPH links",
460 "INSERT INTO links NODE (label,node_type,visible) VALUES ('shown','Item',true)",
461 "INSERT INTO links NODE (label,node_type,visible) VALUES ('hidden','Item',false)",
462 "CREATE POLICY visible_nodes ON NODES OF links FOR SELECT USING (properties.visible=true)",
463 "ALTER TABLE links ENABLE ROW LEVEL SECURITY",
464 "CREATE DOCUMENT papers",
465 "INSERT INTO papers DOCUMENT VALUES ({name:'shown',visible:true})",
466 "INSERT INTO papers DOCUMENT VALUES ({name:'hidden',visible:false})",
467 "CREATE POLICY visible_documents ON DOCUMENTS OF papers FOR SELECT USING (visible=true)",
468 "ALTER TABLE papers ENABLE ROW LEVEL SECURITY",
469 ] {
470 runtime.execute_query(query).expect(query);
471 }
472
473 let bundle = SnapshotBundle {
474 snapshot: None,
475 auth: Some(("alice".to_string(), Role::Read)),
476 auth_principal: Some(crate::auth::UserId::platform("alice")),
477 tenant: None,
478 };
479 with_snapshot_bundle(&bundle, || {
480 let nodes = runtime
481 .scan_collection("links", None, 10)
482 .expect("typed graph scan");
483 assert_eq!(nodes.total, 1);
484 assert_eq!(nodes.items.len(), 1);
485
486 let documents = runtime
487 .scan_collection("papers", None, 10)
488 .expect("typed document scan");
489 assert_eq!(documents.total, 1);
490 assert_eq!(documents.items.len(), 1);
491 });
492 }
493}