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