Skip to main content

reddb_server/runtime/
impl_telemetry_accessors.rs

1//! Runtime telemetry / gates / limits / shutdown accessors.
2//!
3//! Extracted verbatim from `impl_core.rs` (impl_core slice 8/10, issue #1629).
4//! Houses the mutation-engine handle, the write-gate + resource-limit checks,
5//! the queue / claim / query-latency / occupancy / node-load /
6//! vector-introspection telemetry readers, the slow-query store, lease-lifecycle
7//! accessors, the batch / db size guards, graceful shutdown, quota bucket,
8//! encryption-at-rest status, replica-apply health, and the `record_metrics_*`
9//! family.
10use super::*;
11
12impl RedDBRuntime {
13    /// Emit a CDC change event and replicate to WAL buffer.
14    /// Create a `MutationEngine` bound to this runtime.
15    ///
16    /// The engine is cheap to construct (no allocation) and should be
17    /// dropped after `apply` returns. Use this from application-layer
18    /// `create_row` / `create_rows_batch` instead of calling
19    /// `bulk_insert` + `index_entity_insert` + `cdc_emit` separately.
20    pub(crate) fn mutation_engine(&self) -> crate::runtime::mutation::MutationEngine<'_> {
21        crate::runtime::mutation::MutationEngine::new(self)
22    }
23
24    /// Public-mutation gate snapshot (PLAN.md W1).
25    ///
26    /// Surfaces that accept untrusted client requests (SQL DML/DDL,
27    /// gRPC mutating RPCs, HTTP/native wire mutations, admin
28    /// maintenance, serverless lifecycle) call `check_write` before
29    /// dispatching to storage. Returns `RedDBError::ReadOnly` on any
30    /// instance running as a replica or with `options.read_only =
31    /// true`. The replica internal logical-WAL apply path reaches into
32    /// the store directly and never calls this method, so legitimate
33    /// replica catch-up still works.
34    pub fn check_write(&self, kind: crate::runtime::write_gate::WriteKind) -> RedDBResult<()> {
35        self.inner.write_gate.check(kind)
36    }
37
38    /// Read-only handle to the gate, useful for transports that want
39    /// to surface the policy in health/status output without taking on
40    /// a dependency on the concrete enum.
41    pub fn write_gate(&self) -> &crate::runtime::write_gate::WriteGate {
42        &self.inner.write_gate
43    }
44
45    /// Process lifecycle handle (PLAN.md Phase 1). Health probes,
46    /// admin/shutdown, and signal handlers consult this single
47    /// state machine.
48    pub fn lifecycle(&self) -> &crate::runtime::lifecycle::Lifecycle {
49        &self.inner.lifecycle
50    }
51
52    /// Operator-imposed resource limits (PLAN.md Phase 4.1).
53    pub fn resource_limits(&self) -> &crate::runtime::resource_limits::ResourceLimits {
54        &self.inner.resource_limits
55    }
56
57    /// Append-only audit log for admin mutations (PLAN.md Phase 6.5).
58    pub fn audit_log(&self) -> &crate::runtime::audit_log::AuditLogger {
59        &self.inner.audit_log
60    }
61
62    /// Shared `Arc` to the audit logger — used by collaborators (the
63    /// lease lifecycle, future request-context plumbing) that need to
64    /// keep the logger alive past the runtime's stack frame.
65    pub fn audit_log_arc(&self) -> Arc<crate::runtime::audit_log::AuditLogger> {
66        Arc::clone(&self.inner.audit_log)
67    }
68
69    /// Shared queue telemetry counters (delivered/acked/nacked).
70    pub(crate) fn queue_telemetry(
71        &self,
72    ) -> &crate::runtime::queue_telemetry::QueueTelemetryCounters {
73        &self.inner.queue_telemetry
74    }
75
76    /// Snapshots of the queue telemetry counters in label-deterministic
77    /// order for `/metrics` rendering and the integration test.
78    pub fn queue_telemetry_snapshot(
79        &self,
80    ) -> crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
81        crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
82            delivered: self.inner.queue_telemetry.delivered_snapshot(),
83            acked: self.inner.queue_telemetry.acked_snapshot(),
84            nacked: self.inner.queue_telemetry.nacked_snapshot(),
85            wait_started: self.inner.queue_telemetry.wait_started_snapshot(),
86            wait_woken: self.inner.queue_telemetry.wait_woken_snapshot(),
87            wait_timed_out: self.inner.queue_telemetry.wait_timed_out_snapshot(),
88            wait_cancelled: self.inner.queue_telemetry.wait_cancelled_snapshot(),
89            wait_duration: self.inner.queue_telemetry.wait_duration_snapshot(),
90        }
91    }
92
93    /// Snapshots of Concurrent claim counters in label-deterministic order.
94    pub fn claim_telemetry_snapshot(&self) -> crate::runtime::ClaimTelemetrySnapshot {
95        self.inner.claim_telemetry.snapshot()
96    }
97
98    /// Per-`kind` query latency histograms for `/metrics` (only kinds with
99    /// a real sample are present — empty kinds are absent, not zero-filled).
100    pub fn query_latency_snapshot(
101        &self,
102    ) -> Vec<crate::runtime::query_latency_telemetry::QueryLatencyHistogram> {
103        self.inner.query_latency_telemetry.snapshot()
104    }
105
106    /// Cross-kind query latency rollup for `/cluster/status` and the
107    /// red-ui percentile panels. `count == 0` until a real sample exists.
108    pub fn query_latency_rollup(
109        &self,
110    ) -> crate::runtime::query_latency_telemetry::QueryLatencyHistogram {
111        self.inner.query_latency_telemetry.rollup()
112    }
113
114    /// Issue #1244 — take a fresh node CPU/RAM occupancy reading for
115    /// `/cluster/status`. CPU utilisation is measured over the interval
116    /// since the previous call (the first call only establishes a baseline
117    /// and reports `NotSampled`). See `occupancy_sampler` for overhead.
118    pub fn sample_occupancy(&self) -> crate::runtime::occupancy_sampler::OccupancySample {
119        self.inner.occupancy_sampler.sample()
120    }
121
122    /// Issue #1245 — point-in-time node load snapshot (active queries +
123    /// connect/disconnect churn). Feeds `/metrics`, `/cluster/status`, and
124    /// the red-ui load panels.
125    pub fn node_load_snapshot(&self) -> crate::runtime::node_load_telemetry::NodeLoadSnapshot {
126        self.inner.node_load_telemetry.snapshot()
127    }
128
129    /// Issue #742 — consumer presence registry. Heartbeats land here
130    /// from `QUEUE READ` (and, in a follow-up slice, an explicit
131    /// `QUEUE HEARTBEAT` command); Red UI and `red.queue_consumers`
132    /// read snapshots through `queue_consumer_presence_snapshot`.
133    pub(crate) fn queue_presence(
134        &self,
135    ) -> &std::sync::Arc<crate::storage::queue::presence::ConsumerPresenceRegistry> {
136        &self.inner.queue_presence
137    }
138
139    /// Issue #742 — point-in-time presence snapshot, classifying each
140    /// `(queue, group, consumer)` as active/stale/expired against the
141    /// supplied TTL. Wall-clock is read once here so the lifecycle
142    /// flags inside the snapshot are internally consistent.
143    pub fn queue_consumer_presence_snapshot(
144        &self,
145        ttl_ms: u64,
146    ) -> Vec<crate::storage::queue::presence::ConsumerPresence> {
147        let now_ns = std::time::SystemTime::now()
148            .duration_since(std::time::UNIX_EPOCH)
149            .map(|d| d.as_nanos() as u64)
150            .unwrap_or(0);
151        self.inner.queue_presence.snapshot(now_ns, ttl_ms)
152    }
153
154    /// Issue #742 — active-consumer count per `(queue, group)` for the
155    /// queue-metadata surface. Stale/expired entries are excluded by
156    /// definition; they are still visible in the per-row snapshot.
157    pub fn queue_active_consumer_counts(
158        &self,
159        ttl_ms: u64,
160    ) -> std::collections::HashMap<(String, String), u32> {
161        let now_ns = std::time::SystemTime::now()
162            .duration_since(std::time::UNIX_EPOCH)
163            .map(|d| d.as_nanos() as u64)
164            .unwrap_or(0);
165        self.inner
166            .queue_presence
167            .count_active_by_group(now_ns, ttl_ms)
168    }
169
170    /// Issue #743 — vector + TurboQuant introspection registry. Engine
171    /// publish points (collection create, artifact build start /
172    /// finish, fallback toggle, drop) update this; Red UI and
173    /// `red.*` vector virtual tables read snapshots through
174    /// `vector_introspection_snapshot` / `vector_introspection_get`.
175    pub(crate) fn vector_introspection_registry(
176        &self,
177    ) -> &std::sync::Arc<crate::storage::vector::introspection::VectorIntrospectionRegistry> {
178        &self.inner.vector_introspection
179    }
180
181    /// Issue #743 — full snapshot of every tracked vector collection's
182    /// `(VectorMetadata, ArtifactMetadata)`. Deterministically ordered
183    /// by collection name so Red UI tables and tests both see a
184    /// stable shape.
185    pub fn vector_introspection_snapshot(
186        &self,
187    ) -> Vec<crate::storage::vector::introspection::VectorIntrospection> {
188        self.inner.vector_introspection.snapshot()
189    }
190
191    /// Issue #743 — single-collection lookup, for the per-collection
192    /// metadata endpoint Red UI hits when an operator opens one
193    /// vector's toolbar.
194    pub fn vector_introspection_get(
195        &self,
196        collection: &str,
197    ) -> Option<crate::storage::vector::introspection::VectorIntrospection> {
198        self.inner.vector_introspection.get(collection)
199    }
200
201    /// Issue #1238 — ADR 0060 read-model accessor for slow-query telemetry.
202    ///
203    /// Returns a reference to the bounded ring store so HTTP handlers and
204    /// the red-ui read model can call `store.read(filter)` without
205    /// touching `red-slow.log` directly.
206    pub fn slow_query_store(&self) -> &Arc<crate::telemetry::slow_query_store::SlowQueryStore> {
207        &self.inner.slow_query_store
208    }
209
210    /// Slice 10 of issue #527 — render-time scan of pending entries
211    /// per (queue, group) for the `queue_pending_gauge` exposition.
212    /// Walks `red_queue_meta` live so the gauge cannot drift from
213    /// the source of truth.
214    pub fn queue_pending_counts(&self) -> Vec<((String, String), u64)> {
215        let store = self.inner.db.store();
216        crate::runtime::impl_queue::pending_counts_by_group(store.as_ref())
217            .into_iter()
218            .collect()
219    }
220
221    /// Shared `Arc` to the write gate. Same rationale as
222    /// `audit_log_arc`: collaborators (lease lifecycle, refresh
223    /// thread) need a clone-cheap handle they can move into a
224    /// background thread.
225    pub fn write_gate_arc(&self) -> Arc<crate::runtime::write_gate::WriteGate> {
226        Arc::clone(&self.inner.write_gate)
227    }
228
229    /// Serverless writer-lease state machine. `None` when the operator
230    /// did not opt into lease fencing (`RED_LEASE_REQUIRED` unset).
231    pub fn lease_lifecycle(&self) -> Option<&Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
232        self.inner.lease_lifecycle.get()
233    }
234
235    /// Install the lease lifecycle. Idempotent; subsequent calls
236    /// return the previously stored value untouched.
237    pub fn set_lease_lifecycle(
238        &self,
239        lifecycle: Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>,
240    ) -> Result<(), Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
241        self.inner.lease_lifecycle.set(lifecycle)
242    }
243
244    /// Reject the call when the requested batch size exceeds
245    /// `RED_MAX_BATCH_SIZE`. Returns `RedDBError::QuotaExceeded`
246    /// shaped so the HTTP layer can map it to 413 Payload Too
247    /// Large (PLAN.md Phase 4.1).
248    pub fn check_batch_size(&self, requested: usize) -> RedDBResult<()> {
249        if self.inner.resource_limits.batch_size_exceeded(requested) {
250            let max = self.inner.resource_limits.max_batch_size.unwrap_or(0);
251            return Err(RedDBError::QuotaExceeded(format!(
252                "max_batch_size:{requested}:{max}"
253            )));
254        }
255        Ok(())
256    }
257
258    /// Reject the call when the local DB file exceeds
259    /// `RED_MAX_DB_SIZE_BYTES`. Reads file metadata once per call —
260    /// the cost is a single `stat()` syscall, negligible against the
261    /// I/O the caller is about to do. Returns `QuotaExceeded` shaped
262    /// for HTTP 507 Insufficient Storage.
263    pub fn check_db_size(&self) -> RedDBResult<()> {
264        let Some(limit) = self.inner.resource_limits.max_db_size_bytes else {
265            return Ok(());
266        };
267        if limit == 0 {
268            return Ok(());
269        }
270        let Some(path) = self.inner.db.path() else {
271            return Ok(());
272        };
273        let current = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
274        if current > limit {
275            return Err(RedDBError::QuotaExceeded(format!(
276                "max_db_size_bytes:{current}:{limit}"
277            )));
278        }
279        Ok(())
280    }
281
282    /// Graceful shutdown coordinator (PLAN.md Phase 1.1).
283    ///
284    /// Steps, in order, all idempotent across re-entrant calls:
285    ///   1. Move lifecycle into `ShuttingDown` (concurrent callers
286    ///      observe `Stopped` after first finishes).
287    ///   2. Flush WAL + run final checkpoint via `db.flush()` so
288    ///      every acked write is durable on disk.
289    ///   3. If `backup_on_shutdown == true` and a remote backend is
290    ///      configured, run a synchronous `trigger_backup()` so the
291    ///      remote head reflects the final state.
292    ///   4. Stamp the report and move to `Stopped`. Subsequent calls
293    ///      return the cached report without re-running anything.
294    ///
295    /// On any error, the runtime is still marked `Stopped` so the
296    /// process can exit; the caller logs the error context but does
297    /// not retry the same shutdown — the operator can inspect the
298    /// report fields to see which step failed.
299    pub fn graceful_shutdown(
300        &self,
301        backup_on_shutdown: bool,
302    ) -> RedDBResult<crate::runtime::lifecycle::ShutdownReport> {
303        if !self.inner.lifecycle.begin_shutdown() {
304            // Someone else already shut down (or is in flight). Return
305            // the cached report so the HTTP caller and SIGTERM handler
306            // get the same idempotent answer.
307            return Ok(self.inner.lifecycle.shutdown_report().unwrap_or_default());
308        }
309
310        let started_ms = std::time::SystemTime::now()
311            .duration_since(std::time::UNIX_EPOCH)
312            .map(|d| d.as_millis() as u64)
313            .unwrap_or(0);
314        let mut report = crate::runtime::lifecycle::ShutdownReport {
315            started_at_ms: started_ms,
316            ..Default::default()
317        };
318
319        // Flush WAL + run any pending checkpoint. Local fsync is
320        // unconditional — even a lease-lost replica needs its WAL on
321        // disk before exit so a future restore has the latest tail.
322        // The remote upload is gated separately so a lost-lease writer
323        // doesn't clobber the new holder's state on its way out.
324        let flush_res = self.inner.db.flush_local_only();
325        report.flushed_wal = flush_res.is_ok();
326        report.final_checkpoint = flush_res.is_ok();
327        if let Err(err) = &flush_res {
328            tracing::error!(
329                target: "reddb::lifecycle",
330                error = %err,
331                "graceful_shutdown: local flush failed"
332            );
333        } else if let Err(lease_err) =
334            self.assert_remote_write_allowed("shutdown/checkpoint_upload")
335        {
336            tracing::warn!(
337                target: "reddb::serverless::lease",
338                error = %lease_err,
339                "graceful_shutdown: remote upload skipped — lease not held"
340            );
341        } else if let Err(err) = self.inner.db.upload_to_remote_backend() {
342            tracing::error!(
343                target: "reddb::lifecycle",
344                error = %err,
345                "graceful_shutdown: remote upload failed"
346            );
347        }
348
349        // Optional final backup. Skipped silently when no remote
350        // backend is configured — `trigger_backup()` returns Err
351        // anyway in that case, but logging it as a shutdown failure
352        // would be misleading on a standalone (no-backend) runtime.
353        if backup_on_shutdown && self.inner.db.remote_backend.is_some() {
354            // The trigger_backup gate now reads `WriteKind::Backup`,
355            // which a replica/read_only instance refuses. That's
356            // intentional — replicas don't drive backups; only the
357            // primary does. We still want shutdown to flush its WAL
358            // even if the backup branch is gated off.
359            match self.trigger_backup() {
360                Ok(result) => {
361                    report.backup_uploaded = result.uploaded;
362                }
363                Err(err) => {
364                    tracing::warn!(
365                        target: "reddb::lifecycle",
366                        error = %err,
367                        "graceful_shutdown: final backup skipped"
368                    );
369                }
370            }
371        }
372
373        let completed_ms = std::time::SystemTime::now()
374            .duration_since(std::time::UNIX_EPOCH)
375            .map(|d| d.as_millis() as u64)
376            .unwrap_or(started_ms);
377        report.completed_at_ms = completed_ms;
378        report.duration_ms = completed_ms.saturating_sub(started_ms);
379
380        self.inner.lifecycle.finish_shutdown(report.clone());
381        Ok(report)
382    }
383
384    /// PLAN.md Phase 4.4 — per-caller quota bucket. Always
385    /// returned; `is_configured()` lets callers short-circuit.
386    pub fn quota_bucket(&self) -> &crate::runtime::quota_bucket::QuotaBucket {
387        &self.inner.quota_bucket
388    }
389
390    /// PLAN.md Phase 6.3 — whether at-rest encryption is configured.
391    /// Reads `RED_ENCRYPTION_KEY` / `RED_ENCRYPTION_KEY_FILE` lazily;
392    /// returns `("enabled", None)` when a key is loadable, `("error", Some(msg))`
393    /// when the operator set the env but it doesn't parse, and
394    /// `("disabled", None)` when no key is configured. The pager
395    /// hookup is deferred — this accessor surfaces the operator's
396    /// intent for /admin/status without yet using the key in writes.
397    pub fn encryption_at_rest_status(&self) -> (&'static str, Option<String>) {
398        match crate::crypto::page_encryption::key_from_env() {
399            Ok(Some(_)) => ("enabled", None),
400            Ok(None) => ("disabled", None),
401            Err(err) => ("error", Some(err)),
402        }
403    }
404
405    /// PLAN.md Phase 11.5 — current replica apply health label
406    /// (`ok`, `gap`, `divergence`, `apply_error`, `connecting`,
407    /// `stalled_gap`). Read from the persisted `red.replication.state`
408    /// config key updated by the replica loop. Returns `None` on
409    /// non-replica instances or when no apply has run yet.
410    pub fn replica_apply_health(&self) -> Option<String> {
411        let state = self.config_string("red.replication.state", "");
412        if state.is_empty() {
413            None
414        } else {
415            Some(state)
416        }
417    }
418
419    pub(crate) fn record_metrics_ingest(
420        &self,
421        accepted_samples: u64,
422        accepted_series: u64,
423        rejected_samples: u64,
424        rejected_series: u64,
425    ) {
426        self.inner.metrics_ingest_stats.record(
427            accepted_samples,
428            accepted_series,
429            rejected_samples,
430            rejected_series,
431        );
432    }
433
434    pub(crate) fn record_metrics_cardinality_budget_rejections(&self, rejected_series: u64) {
435        self.inner
436            .metrics_ingest_stats
437            .record_cardinality_budget_rejections(rejected_series);
438    }
439
440    pub(crate) fn record_metrics_tenant_activity(
441        &self,
442        tenant: &str,
443        namespace: &str,
444        operation: &str,
445    ) {
446        self.inner
447            .metrics_tenant_activity_stats
448            .record(tenant, namespace, operation);
449    }
450
451    pub(crate) fn metrics_tenant_activity_snapshot(
452        &self,
453    ) -> Vec<crate::runtime::MetricsTenantActivityStats> {
454        self.inner.metrics_tenant_activity_stats.snapshot()
455    }
456}