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}