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