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