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