Skip to main content

reddb_server/runtime/
impl_catalog_accessors.rs

1//! Runtime catalog / stats / maintenance accessors.
2//!
3//! Extracted verbatim from `impl_core.rs` (impl_core slice 8/10, issue #1629).
4//! Houses connection `acquire`, `checkpoint`, the remote-write assertion,
5//! `run_maintenance`, `scan_collection`, and the catalog /
6//! attention-summary / `stats` readers.
7use 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    // Transport admission already bounds wire sessions. Share connection IDs
23    // and rollback cleanup without imposing the embedded pool's separate cap.
24    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        // Issue #1245 — record the connection acquisition after releasing
55        // the pool lock so the lock hold time is unchanged.
56        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        // Local fsync always allowed — losing the lease shouldn't
68        // prevent us from durably persisting what's already in memory.
69        // The remote upload is the side-effect that risks clobbering a
70        // peer's state, so it's behind the lease gate.
71        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        // Collections created since startup need the compact visibility
94        // inventory before their first resident-to-disk publication too.
95        self.prepare_paged_counts()?;
96        self.persist_probabilistic_snapshots()?;
97        self.inner.db.flush_local_only().map_err(|err| {
98            // Issue #205 — local flush failure is a CheckpointFailed
99            // operator-grade event. The local-flush path also covers
100            // the WAL fsync we depend on, so a failure here doubles as
101            // the WalFsyncFailed signal for the runtime entry point.
102            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    /// Guard remote-mutating operations on the writer lease.
124    /// Returns `Ok(())` when no remote backend is configured (the
125    /// lease is irrelevant) or the lease state is `NotRequired` /
126    /// `Held`. Returns `RedDBError::ReadOnly` when the lease is
127    /// `NotHeld`, with an audit-friendly action label so the caller
128    /// can record the rejection.
129    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        let store = self.inner.db.store();
164        let manager = store
165            .get_collection(collection)
166            .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
167
168        let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
169        let limit = limit.max(1);
170        let (total, items) = manager.page_in_id_order(offset, limit);
171        let end = total.min(offset.saturating_add(limit));
172        let next = (end < total).then_some(ScanCursor { offset: end });
173
174        Ok(ScanPage {
175            collection: collection.to_string(),
176            items,
177            next,
178            total,
179        })
180    }
181
182    pub fn catalog(&self) -> CatalogModelSnapshot {
183        self.inner.db.catalog_model_snapshot()
184    }
185
186    /// Fallible catalog for disk-resident databases. The decode workspace shares
187    /// query admission with concurrent reads and is released on every error.
188    pub fn try_catalog(&self) -> RedDBResult<CatalogModelSnapshot> {
189        if self
190            .inner
191            .db
192            .store()
193            .config()
194            .resident_memory_bytes_max
195            .is_none()
196        {
197            return Ok(self.catalog());
198        }
199        let read_bytes = usize::try_from(self.memory_budget().resolved_bytes / 16)
200            .unwrap_or(usize::MAX)
201            .clamp(1, 16 * 1024 * 1024);
202        let _scratch = self
203            .try_reserve_query_buffer(read_bytes * 2)
204            .ok_or_else(|| {
205                self.memory_accounting().record_admission_denied();
206                RedDBError::MaterializationLimitExceeded {
207                    executor: "catalog",
208                    limit: usize::try_from(self.memory_budget().resolved_bytes)
209                        .unwrap_or(usize::MAX),
210                    current: usize::try_from(self.memory_accounting().total_used_bytes())
211                        .unwrap_or(usize::MAX)
212                        .saturating_add(1),
213                }
214            })?;
215        Ok(self.inner.db.try_catalog_model_snapshot(read_bytes)?)
216    }
217
218    pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
219        self.inner.db.catalog_consistency_report()
220    }
221
222    pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
223        crate::catalog::attention_summary(&self.catalog())
224    }
225
226    pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
227        crate::catalog::collection_attention(&self.catalog())
228    }
229
230    pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
231        crate::catalog::index_attention(&self.catalog())
232    }
233
234    pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
235        crate::catalog::graph_projection_attention(&self.catalog())
236    }
237
238    pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
239        crate::catalog::analytics_job_attention(&self.catalog())
240    }
241
242    pub fn stats(&self) -> RuntimeStats {
243        let pool = runtime_pool_lock(self);
244        RuntimeStats {
245            active_connections: pool.active,
246            idle_connections: pool.idle.len(),
247            total_checkouts: pool.total_checkouts,
248            paged_mode: self.inner.db.is_paged(),
249            started_at_unix_ms: self.inner.started_at_unix_ms,
250            store: self.inner.db.stats(),
251            system: SystemInfo::collect(),
252            result_blob_cache: self.inner.result_blob_cache.stats(),
253            kv: self.inner.kv_stats.snapshot(),
254            metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
255        }
256    }
257}