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