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        let mut pool = self
20            .inner
21            .pool
22            .lock()
23            .map_err(|e| RedDBError::Internal(format!("connection pool lock poisoned: {e}")))?;
24        if pool.active >= self.inner.pool_config.max_connections {
25            return Err(RedDBError::Internal(
26                "connection pool exhausted".to_string(),
27            ));
28        }
29
30        let id = if let Some(id) = pool.idle.pop() {
31            id
32        } else {
33            let id = pool.next_id;
34            pool.next_id += 1;
35            id
36        };
37        pool.active += 1;
38        pool.total_checkouts += 1;
39        drop(pool);
40
41        // Issue #1245 — record the connection acquisition after releasing
42        // the pool lock so the lock hold time is unchanged.
43        self.inner.node_load_telemetry.record_connect();
44
45        Ok(RuntimeConnection {
46            id,
47            inner: Arc::clone(&self.inner),
48        })
49    }
50
51    pub fn checkpoint(&self) -> RedDBResult<()> {
52        // Local fsync always allowed — losing the lease shouldn't
53        // prevent us from durably persisting what's already in memory.
54        // The remote upload is the side-effect that risks clobbering a
55        // peer's state, so it's behind the lease gate.
56        let started = std::time::Instant::now();
57        self.seal_hypertable_chunks_for_checkpoint(
58            self.inner.checkpoint_columnar_emission_budget_chunks,
59        )?;
60        self.persist_probabilistic_snapshots()?;
61        self.inner.db.flush_local_only().map_err(|err| {
62            // Issue #205 — local flush failure is a CheckpointFailed
63            // operator-grade event. The local-flush path also covers
64            // the WAL fsync we depend on, so a failure here doubles as
65            // the WalFsyncFailed signal for the runtime entry point.
66            let msg = err.to_string();
67            crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
68                lsn: 0,
69                error: msg.clone(),
70            }
71            .emit_global();
72            crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
73                path: "<flush_local_only>".to_string(),
74                error: msg.clone(),
75            }
76            .emit_global();
77            RedDBError::Engine(msg)
78        })?;
79        let checkpoint_lsn = self.cdc_current_lsn();
80        self.inner
81            .checkpoint_projection_stats
82            .record_checkpoint(checkpoint_lsn, started.elapsed().as_millis() as u64);
83        if let Err(err) = self.assert_remote_write_allowed("checkpoint") {
84            tracing::warn!(
85                target: "reddb::serverless::lease",
86                error = %err,
87                "checkpoint: skipping remote upload — lease not held"
88            );
89            return Ok(());
90        }
91        self.inner
92            .db
93            .upload_to_remote_backend()
94            .map_err(|err| RedDBError::Engine(err.to_string()))
95    }
96
97    /// Guard remote-mutating operations on the writer lease.
98    /// Returns `Ok(())` when no remote backend is configured (the
99    /// lease is irrelevant) or the lease state is `NotRequired` /
100    /// `Held`. Returns `RedDBError::ReadOnly` when the lease is
101    /// `NotHeld`, with an audit-friendly action label so the caller
102    /// can record the rejection.
103    pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
104        if self.inner.db.remote_backend.is_none() {
105            return Ok(());
106        }
107        match self.inner.write_gate.lease_state() {
108            crate::runtime::write_gate::LeaseGateState::NotHeld => {
109                self.inner.audit_log.record(
110                    action,
111                    "system",
112                    "remote_backend",
113                    "err: writer lease not held",
114                    crate::json::Value::Null,
115                );
116                Err(RedDBError::ReadOnly(format!(
117                    "writer lease not held — {action} blocked (serverless fence)"
118                )))
119            }
120            _ => Ok(()),
121        }
122    }
123
124    pub fn run_maintenance(&self) -> RedDBResult<()> {
125        self.inner
126            .db
127            .run_maintenance()
128            .map_err(|err| RedDBError::Internal(err.to_string()))
129    }
130
131    pub fn scan_collection(
132        &self,
133        collection: &str,
134        cursor: Option<ScanCursor>,
135        limit: usize,
136    ) -> RedDBResult<ScanPage> {
137        let store = self.inner.db.store();
138        let manager = store
139            .get_collection(collection)
140            .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
141
142        let mut entities = manager.query_all(|_| true);
143        entities.sort_by_key(|entity| entity.id.raw());
144
145        let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
146        let total = entities.len();
147        let end = total.min(offset.saturating_add(limit.max(1)));
148        let items = if offset >= total {
149            Vec::new()
150        } else {
151            entities[offset..end].to_vec()
152        };
153        let next = (end < total).then_some(ScanCursor { offset: end });
154
155        Ok(ScanPage {
156            collection: collection.to_string(),
157            items,
158            next,
159            total,
160        })
161    }
162
163    pub fn catalog(&self) -> CatalogModelSnapshot {
164        self.inner.db.catalog_model_snapshot()
165    }
166
167    pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
168        self.inner.db.catalog_consistency_report()
169    }
170
171    pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
172        crate::catalog::attention_summary(&self.catalog())
173    }
174
175    pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
176        crate::catalog::collection_attention(&self.catalog())
177    }
178
179    pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
180        crate::catalog::index_attention(&self.catalog())
181    }
182
183    pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
184        crate::catalog::graph_projection_attention(&self.catalog())
185    }
186
187    pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
188        crate::catalog::analytics_job_attention(&self.catalog())
189    }
190
191    pub fn stats(&self) -> RuntimeStats {
192        let pool = runtime_pool_lock(self);
193        RuntimeStats {
194            active_connections: pool.active,
195            idle_connections: pool.idle.len(),
196            total_checkouts: pool.total_checkouts,
197            paged_mode: self.inner.db.is_paged(),
198            started_at_unix_ms: self.inner.started_at_unix_ms,
199            store: self.inner.db.stats(),
200            system: SystemInfo::collect(),
201            result_blob_cache: self.inner.result_blob_cache.stats(),
202            kv: self.inner.kv_stats.snapshot(),
203            metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
204        }
205    }
206}