grafos_net/
dns.rs

1//! Fabric node name resolution.
2
3use std::collections::HashMap;
4use std::net::SocketAddr;
5use std::time::{Duration, Instant};
6
7/// Fabric DNS resolver for mapping node names to socket addresses.
8///
9/// Provides name registration and lookup with TTL-based cache expiry.
10/// Names follow the `<node_id>.fabric` or `<human_name>.fabric` convention.
11///
12/// This is an in-memory implementation suitable for testing and
13/// single-process use. A production implementation would query the
14/// fabricBIOS control plane.
15///
16/// # Example
17///
18/// ```rust
19/// use grafos_net::FabricDns;
20/// use std::net::SocketAddr;
21/// use std::time::Duration;
22///
23/// let mut dns = FabricDns::new(Duration::from_secs(300));
24/// let addr: SocketAddr = "10.10.0.11:5701".parse().unwrap();
25/// dns.register("node-b.fabric", addr);
26///
27/// let resolved = dns.resolve("node-b.fabric").unwrap();
28/// assert_eq!(resolved, vec![addr]);
29/// ```
30pub struct FabricDns {
31    entries: HashMap<String, DnsEntry>,
32    ttl: Duration,
33}
34
35struct DnsEntry {
36    addrs: Vec<SocketAddr>,
37    inserted: Instant,
38}
39
40impl FabricDns {
41    /// Create a new DNS resolver with the given TTL for cached entries.
42    pub fn new(ttl: Duration) -> Self {
43        FabricDns {
44            entries: HashMap::new(),
45            ttl,
46        }
47    }
48
49    /// Register a name-to-address mapping.
50    ///
51    /// If the name already exists, the entry is replaced with a fresh TTL.
52    pub fn register(&mut self, name: &str, addr: SocketAddr) {
53        let entry = self
54            .entries
55            .entry(name.to_string())
56            .or_insert_with(|| DnsEntry {
57                addrs: Vec::new(),
58                inserted: Instant::now(),
59            });
60        if !entry.addrs.contains(&addr) {
61            entry.addrs.push(addr);
62        }
63        entry.inserted = Instant::now();
64    }
65
66    /// Resolve a fabric node name to its registered addresses.
67    ///
68    /// Returns `None` if the name is not registered or has expired.
69    pub fn resolve(&self, name: &str) -> Option<Vec<SocketAddr>> {
70        let entry = self.entries.get(name)?;
71        if entry.inserted.elapsed() > self.ttl {
72            return None;
73        }
74        Some(entry.addrs.clone())
75    }
76
77    /// Remove expired entries from the cache.
78    pub fn evict_expired(&mut self) {
79        self.entries.retain(|_, e| e.inserted.elapsed() <= self.ttl);
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::net::{Ipv4Addr, SocketAddrV4};
87
88    fn addr(ip: [u8; 4], port: u16) -> SocketAddr {
89        SocketAddr::V4(SocketAddrV4::new(
90            Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
91            port,
92        ))
93    }
94
95    #[test]
96    fn register_and_resolve() {
97        let mut dns = FabricDns::new(Duration::from_secs(60));
98        let a = addr([10, 10, 0, 11], 5701);
99
100        dns.register("node-b.fabric", a);
101
102        let addrs = dns.resolve("node-b.fabric").expect("resolve");
103        assert_eq!(addrs, vec![a]);
104    }
105
106    #[test]
107    fn resolve_unknown_returns_none() {
108        let dns = FabricDns::new(Duration::from_secs(60));
109        assert!(dns.resolve("unknown.fabric").is_none());
110    }
111
112    #[test]
113    fn multiple_addrs_for_same_name() {
114        let mut dns = FabricDns::new(Duration::from_secs(60));
115        let a1 = addr([10, 10, 0, 11], 5701);
116        let a2 = addr([10, 10, 0, 11], 5702);
117
118        dns.register("node-b.fabric", a1);
119        dns.register("node-b.fabric", a2);
120
121        let addrs = dns.resolve("node-b.fabric").expect("resolve");
122        assert_eq!(addrs.len(), 2);
123        assert!(addrs.contains(&a1));
124        assert!(addrs.contains(&a2));
125    }
126
127    #[test]
128    fn duplicate_register_is_idempotent() {
129        let mut dns = FabricDns::new(Duration::from_secs(60));
130        let a = addr([10, 10, 0, 11], 5701);
131
132        dns.register("node-b.fabric", a);
133        dns.register("node-b.fabric", a);
134
135        let addrs = dns.resolve("node-b.fabric").expect("resolve");
136        assert_eq!(addrs.len(), 1);
137    }
138
139    #[test]
140    fn ttl_expiry() {
141        let mut dns = FabricDns::new(Duration::from_millis(0));
142        let a = addr([10, 10, 0, 11], 5701);
143
144        dns.register("node-b.fabric", a);
145        // Zero TTL means it expires immediately
146        std::thread::sleep(Duration::from_millis(1));
147
148        assert!(dns.resolve("node-b.fabric").is_none());
149    }
150
151    #[test]
152    fn evict_expired_removes_stale_entries() {
153        let mut dns = FabricDns::new(Duration::from_millis(0));
154        let a = addr([10, 10, 0, 11], 5701);
155
156        dns.register("node-b.fabric", a);
157        std::thread::sleep(Duration::from_millis(1));
158
159        dns.evict_expired();
160        assert!(dns.resolve("node-b.fabric").is_none());
161        assert!(dns.entries.is_empty());
162    }
163}