Files
numa/src/stats.rs
Razvan Dimescu 30e46e549c feat: in-flight query coalescing with COALESCED path (#20)
* feat: in-flight query coalescing for recursive resolver

When multiple queries for the same (domain, qtype) arrive concurrently
and all miss the cache, only the first triggers recursive resolution.
Subsequent queries wait on a broadcast channel for the result.

Prevents thundering herd where N concurrent cache misses each
independently walk the full NS chain, compounding timeouts.

Uses InflightGuard (Drop impl) to guarantee map cleanup on
panic/cancellation — prevents permanent SERVFAIL poisoning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: add InflightMap type alias for clippy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add COALESCED query path and coalescing tests

Followers in the inflight coalescing path now log as COALESCED instead
of RECURSIVE, making it visible in the dashboard when queries were
deduplicated vs independently resolved. Adds 10 tests covering
InflightGuard cleanup, broadcast mechanics, and concurrent handle_query
coalescing through a mock TCP DNS server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: cargo fmt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract acquire_inflight, rewrite tests against real code

Move Disposition enum and inflight acquisition logic into a standalone
acquire_inflight() function. Rewrite 4 tests that were exercising tokio
primitives to call the real coalescing code path instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:36:02 +03:00

159 lines
4.6 KiB
Rust

use std::time::Instant;
pub struct ServerStats {
queries_total: u64,
queries_forwarded: u64,
queries_recursive: u64,
queries_coalesced: u64,
queries_cached: u64,
queries_blocked: u64,
queries_local: u64,
queries_overridden: u64,
upstream_errors: u64,
started_at: Instant,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum QueryPath {
Local,
Cached,
Forwarded,
Recursive,
Coalesced,
Blocked,
Overridden,
UpstreamError,
}
impl QueryPath {
pub fn as_str(&self) -> &'static str {
match self {
QueryPath::Local => "LOCAL",
QueryPath::Cached => "CACHED",
QueryPath::Forwarded => "FORWARD",
QueryPath::Recursive => "RECURSIVE",
QueryPath::Coalesced => "COALESCED",
QueryPath::Blocked => "BLOCKED",
QueryPath::Overridden => "OVERRIDE",
QueryPath::UpstreamError => "SERVFAIL",
}
}
pub fn parse_str(s: &str) -> Option<QueryPath> {
if s.eq_ignore_ascii_case("LOCAL") {
Some(QueryPath::Local)
} else if s.eq_ignore_ascii_case("CACHED") {
Some(QueryPath::Cached)
} else if s.eq_ignore_ascii_case("FORWARD") {
Some(QueryPath::Forwarded)
} else if s.eq_ignore_ascii_case("RECURSIVE") {
Some(QueryPath::Recursive)
} else if s.eq_ignore_ascii_case("COALESCED") {
Some(QueryPath::Coalesced)
} else if s.eq_ignore_ascii_case("BLOCKED") {
Some(QueryPath::Blocked)
} else if s.eq_ignore_ascii_case("OVERRIDE") {
Some(QueryPath::Overridden)
} else if s.eq_ignore_ascii_case("SERVFAIL") {
Some(QueryPath::UpstreamError)
} else {
None
}
}
}
impl Default for ServerStats {
fn default() -> Self {
Self::new()
}
}
impl ServerStats {
pub fn new() -> Self {
ServerStats {
queries_total: 0,
queries_forwarded: 0,
queries_recursive: 0,
queries_coalesced: 0,
queries_cached: 0,
queries_blocked: 0,
queries_local: 0,
queries_overridden: 0,
upstream_errors: 0,
started_at: Instant::now(),
}
}
pub fn record(&mut self, path: QueryPath) -> u64 {
self.queries_total += 1;
match path {
QueryPath::Local => self.queries_local += 1,
QueryPath::Cached => self.queries_cached += 1,
QueryPath::Forwarded => self.queries_forwarded += 1,
QueryPath::Recursive => self.queries_recursive += 1,
QueryPath::Coalesced => self.queries_coalesced += 1,
QueryPath::Blocked => self.queries_blocked += 1,
QueryPath::Overridden => self.queries_overridden += 1,
QueryPath::UpstreamError => self.upstream_errors += 1,
}
self.queries_total
}
pub fn total(&self) -> u64 {
self.queries_total
}
pub fn uptime_secs(&self) -> u64 {
self.started_at.elapsed().as_secs()
}
pub fn snapshot(&self) -> StatsSnapshot {
StatsSnapshot {
uptime_secs: self.uptime_secs(),
total: self.queries_total,
forwarded: self.queries_forwarded,
recursive: self.queries_recursive,
coalesced: self.queries_coalesced,
cached: self.queries_cached,
local: self.queries_local,
overridden: self.queries_overridden,
blocked: self.queries_blocked,
errors: self.upstream_errors,
}
}
pub fn log_summary(&self) {
let uptime = self.started_at.elapsed();
let hours = uptime.as_secs() / 3600;
let mins = (uptime.as_secs() % 3600) / 60;
let secs = uptime.as_secs() % 60;
log::info!(
"STATS | uptime {}h{}m{}s | total {} | fwd {} | recursive {} | coalesced {} | cached {} | local {} | override {} | blocked {} | errors {}",
hours, mins, secs,
self.queries_total,
self.queries_forwarded,
self.queries_recursive,
self.queries_coalesced,
self.queries_cached,
self.queries_local,
self.queries_overridden,
self.queries_blocked,
self.upstream_errors,
);
}
}
pub struct StatsSnapshot {
pub uptime_secs: u64,
pub total: u64,
pub forwarded: u64,
pub recursive: u64,
pub coalesced: u64,
pub cached: u64,
pub local: u64,
pub overridden: u64,
pub blocked: u64,
pub errors: u64,
}