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}