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