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