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