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 mut entities = manager.query_all(|_| true);
158        entities.sort_by_key(|entity| entity.id.raw());
159
160        let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
161        let total = entities.len();
162        let end = total.min(offset.saturating_add(limit.max(1)));
163        let items = if offset >= total {
164            Vec::new()
165        } else {
166            entities[offset..end].to_vec()
167        };
168        let next = (end < total).then_some(ScanCursor { offset: end });
169
170        Ok(ScanPage {
171            collection: collection.to_string(),
172            items,
173            next,
174            total,
175        })
176    }
177
178    pub fn catalog(&self) -> CatalogModelSnapshot {
179        self.inner.db.catalog_model_snapshot()
180    }
181
182    pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
183        self.inner.db.catalog_consistency_report()
184    }
185
186    pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
187        crate::catalog::attention_summary(&self.catalog())
188    }
189
190    pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
191        crate::catalog::collection_attention(&self.catalog())
192    }
193
194    pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
195        crate::catalog::index_attention(&self.catalog())
196    }
197
198    pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
199        crate::catalog::graph_projection_attention(&self.catalog())
200    }
201
202    pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
203        crate::catalog::analytics_job_attention(&self.catalog())
204    }
205
206    pub fn stats(&self) -> RuntimeStats {
207        let pool = runtime_pool_lock(self);
208        RuntimeStats {
209            active_connections: pool.active,
210            idle_connections: pool.idle.len(),
211            total_checkouts: pool.total_checkouts,
212            paged_mode: self.inner.db.is_paged(),
213            started_at_unix_ms: self.inner.started_at_unix_ms,
214            store: self.inner.db.stats(),
215            system: SystemInfo::collect(),
216            result_blob_cache: self.inner.result_blob_cache.stats(),
217            kv: self.inner.kv_stats.snapshot(),
218            metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
219        }
220    }
221}