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 observe_legacy_payloads: bool,
263 ) -> RedDBResult<CatalogModelSnapshot> {
264 if self
265 .inner
266 .db
267 .store()
268 .config()
269 .resident_memory_bytes_max
270 .is_none()
271 {
272 return Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
273 collection_names,
274 observe_legacy_payloads,
275 usize::MAX,
276 )?);
277 }
278 let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
279 .unwrap_or(usize::MAX)
280 .clamp(1, 16 * 1024 * 1024);
281 let _scratch = self
282 .try_reserve_query_buffer(read_bytes * 2)
283 .ok_or_else(|| {
284 self.memory_accounting().record_admission_denied();
285 RedDBError::MaterializationLimitExceeded {
286 executor: "catalog",
287 limit: usize::try_from(self.memory_budget().resolved_bytes)
288 .unwrap_or(usize::MAX),
289 current: usize::try_from(self.memory_accounting().total_used_bytes())
290 .unwrap_or(usize::MAX)
291 .saturating_add(1),
292 }
293 })?;
294 Ok(self.inner.db.try_catalog_model_snapshot_for_collections(
295 collection_names,
296 observe_legacy_payloads,
297 read_bytes,
298 )?)
299 }
300
301 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
302 self.inner.db.catalog_consistency_report()
303 }
304
305 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
306 crate::catalog::attention_summary(&self.catalog())
307 }
308
309 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
310 crate::catalog::collection_attention(&self.catalog())
311 }
312
313 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
314 crate::catalog::index_attention(&self.catalog())
315 }
316
317 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
318 crate::catalog::graph_projection_attention(&self.catalog())
319 }
320
321 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
322 crate::catalog::analytics_job_attention(&self.catalog())
323 }
324
325 pub fn stats(&self) -> RuntimeStats {
326 let pool = runtime_pool_lock(self);
327 RuntimeStats {
328 active_connections: pool.active,
329 idle_connections: pool.idle.len(),
330 total_checkouts: pool.total_checkouts,
331 paged_mode: self.inner.db.is_paged(),
332 started_at_unix_ms: self.inner.started_at_unix_ms,
333 store: self.inner.db.stats(),
334 system: SystemInfo::collect(),
335 result_blob_cache: self.inner.result_blob_cache.stats(),
336 kv: self.inner.kv_stats.snapshot(),
337 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
338 }
339 }
340}
341
342#[cfg(test)]
343mod security_boundary_tests {
344 use super::*;
345 use crate::application::entity::{DeleteEntityInput, PatchEntityInput};
346 use crate::application::ports::RuntimeEntityPort;
347 use crate::auth::Role;
348 use crate::runtime::execution_context::{with_snapshot_bundle, SnapshotBundle};
349 use crate::storage::{EntityData, UnifiedEntity};
350 use reddb_types::Value;
351
352 fn text_field<'a>(entity: &'a UnifiedEntity, name: &str) -> Option<&'a str> {
353 let EntityData::Row(row) = &entity.data else {
354 return None;
355 };
356 let value = row
357 .named
358 .as_ref()
359 .and_then(|named| named.get(name))
360 .or_else(|| {
361 row.schema.as_ref().and_then(|schema| {
362 schema
363 .iter()
364 .position(|column| column == name)
365 .and_then(|index| row.columns.get(index))
366 })
367 })?;
368 match value {
369 Value::Text(value) => Some(value.as_ref()),
370 _ => None,
371 }
372 }
373
374 #[test]
375 fn typed_entity_boundary_filters_before_pagination_and_hides_foreign_ids() {
376 let runtime = RedDBRuntime::in_memory().expect("runtime");
377 runtime
378 .execute_query(
379 "CREATE TABLE docs (id INT, tenant_id TEXT, body TEXT) TENANT BY (tenant_id)",
380 )
381 .expect("tenant table");
382 runtime
383 .execute_query(
384 "INSERT INTO docs (id, tenant_id, body) VALUES \
385 (1, 'acme', 'first'), (2, 'globex', 'hidden'), (3, 'acme', 'second')",
386 )
387 .expect("seed platform rows");
388
389 let unfiltered = runtime
390 .scan_collection("docs", None, 10)
391 .expect("platform scan");
392 let foreign_id = unfiltered
393 .items
394 .iter()
395 .find(|entity| text_field(entity, "tenant_id") == Some("globex"))
396 .map(|entity| entity.id)
397 .expect("foreign row");
398
399 let bundle = SnapshotBundle {
400 snapshot: None,
401 auth: Some(("alice".to_string(), Role::Write)),
402 auth_principal: Some(crate::auth::UserId::scoped("acme", "alice")),
403 tenant: Some("acme".to_string()),
404 };
405 with_snapshot_bundle(&bundle, || {
406 let first = runtime
407 .scan_collection("docs", None, 1)
408 .expect("first visible page");
409 assert_eq!(first.total, 2);
410 assert_eq!(first.items.len(), 1);
411 assert_eq!(text_field(&first.items[0], "tenant_id"), Some("acme"));
412 assert_eq!(first.next, Some(ScanCursor { offset: 1 }));
413
414 let second = runtime
415 .scan_collection("docs", first.next, 1)
416 .expect("second visible page");
417 assert_eq!(second.total, 2);
418 assert_eq!(second.items.len(), 1);
419 assert_eq!(text_field(&second.items[0], "tenant_id"), Some("acme"));
420 assert_eq!(second.next, None);
421
422 assert!(runtime
423 .get_collection_entity("docs", foreign_id)
424 .expect("typed get")
425 .is_none());
426 assert!(matches!(
427 runtime.patch_entity(PatchEntityInput {
428 collection: "docs".to_string(),
429 id: foreign_id,
430 payload: crate::json::Value::Object(Default::default()),
431 operations: Vec::new(),
432 }),
433 Err(crate::RedDBError::NotFound(_))
434 ));
435 let deleted = runtime
436 .delete_entity(DeleteEntityInput {
437 collection: "docs".to_string(),
438 id: foreign_id,
439 })
440 .expect("typed delete");
441 assert!(!deleted.deleted);
442 });
443
444 assert!(runtime.db().store().get("docs", foreign_id).is_some());
445 }
446
447 #[test]
448 fn typed_entity_boundary_uses_model_specific_rls_policies() {
449 let runtime = RedDBRuntime::in_memory().expect("runtime");
450 for query in [
451 "CREATE GRAPH links",
452 "INSERT INTO links NODE (label,node_type,visible) VALUES ('shown','Item',true)",
453 "INSERT INTO links NODE (label,node_type,visible) VALUES ('hidden','Item',false)",
454 "CREATE POLICY visible_nodes ON NODES OF links FOR SELECT USING (properties.visible=true)",
455 "ALTER TABLE links ENABLE ROW LEVEL SECURITY",
456 "CREATE DOCUMENT papers",
457 "INSERT INTO papers DOCUMENT VALUES ({name:'shown',visible:true})",
458 "INSERT INTO papers DOCUMENT VALUES ({name:'hidden',visible:false})",
459 "CREATE POLICY visible_documents ON DOCUMENTS OF papers FOR SELECT USING (visible=true)",
460 "ALTER TABLE papers ENABLE ROW LEVEL SECURITY",
461 ] {
462 runtime.execute_query(query).expect(query);
463 }
464
465 let bundle = SnapshotBundle {
466 snapshot: None,
467 auth: Some(("alice".to_string(), Role::Read)),
468 auth_principal: Some(crate::auth::UserId::platform("alice")),
469 tenant: None,
470 };
471 with_snapshot_bundle(&bundle, || {
472 let nodes = runtime
473 .scan_collection("links", None, 10)
474 .expect("typed graph scan");
475 assert_eq!(nodes.total, 1);
476 assert_eq!(nodes.items.len(), 1);
477
478 let documents = runtime
479 .scan_collection("papers", None, 10)
480 .expect("typed document scan");
481 assert_eq!(documents.total, 1);
482 assert_eq!(documents.items.len(), 1);
483 });
484 }
485}