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