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