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}