dynomite/seeds/dns.rs
1//! DNS-backed seeds provider.
2//!
3//! The reference engine issues a `T_TXT` query (or `T_A` when
4//! `DYNOMITE_DNS_TYPE=A`) against `_dynomite.<host>` and returns
5//! each TXT record's contents (or one synthesised seed per A
6//! record). The Rust port abstracts the resolver behind the
7//! [`Resolver`] trait so the unit test can drive a deterministic
8//! in-memory resolver. The caller is expected to wire
9//! `tokio::net::lookup_host` (or a similar resolver) when
10//! building the production provider.
11//!
12//! # Examples
13//!
14//! ```
15//! use dynomite::seeds::dns::{DnsSeedsProvider, Resolver, ResolvedSeeds};
16//! use dynomite::seeds::SeedsProvider;
17//!
18//! struct StaticResolver;
19//! impl Resolver for StaticResolver {
20//! fn resolve(&self, _name: &str)
21//! -> Result<ResolvedSeeds, dynomite::seeds::SeedsError>
22//! {
23//! Ok(ResolvedSeeds::Txt(vec![
24//! "h1:8101:rA:dc1:1".into(),
25//! "h2:8101:rA:dc1:2".into(),
26//! ]))
27//! }
28//! }
29//! let p = DnsSeedsProvider::new("_dynomite.example".into(), Box::new(StaticResolver));
30//! assert_eq!(p.get_seeds().unwrap().len(), 2);
31//! ```
32
33use std::sync::Arc;
34
35use crate::conf::ConfDynSeed;
36use crate::seeds::{SeedsError, SeedsProvider};
37
38/// Resolver result.
39#[derive(Debug, Clone)]
40pub enum ResolvedSeeds {
41 /// One TXT record per element. Each TXT body must be a
42 /// `host:port:rack:dc:tokens` seed (mirrors the reference
43 /// engine's `dns_get_seeds` TXT branch).
44 Txt(Vec<String>),
45 /// One A record per element, returned as `host:port` strings.
46 /// The provider attaches the supplied default rack/dc/tokens
47 /// when building the seed (mirrors the reference's `T_A`
48 /// branch where every result shares the same rack / dc).
49 A {
50 /// Resolved IP literals.
51 ips: Vec<String>,
52 /// Default port to attach.
53 port: u16,
54 /// Default rack name.
55 rack: String,
56 /// Default dc name.
57 dc: String,
58 /// Default token list.
59 tokens: String,
60 },
61}
62
63/// Trait used by [`DnsSeedsProvider`] to look up a name. Tests
64/// inject a deterministic implementation; the production binary
65/// wires `tokio::net::lookup_host` plus a TXT lookup helper.
66pub trait Resolver: Send + Sync {
67 /// Resolve `name` and return the [`ResolvedSeeds`].
68 fn resolve(&self, name: &str) -> Result<ResolvedSeeds, SeedsError>;
69}
70
71impl<T: Resolver + ?Sized> Resolver for Arc<T> {
72 fn resolve(&self, name: &str) -> Result<ResolvedSeeds, SeedsError> {
73 (**self).resolve(name)
74 }
75}
76
77/// DNS-backed provider.
78pub struct DnsSeedsProvider {
79 name: String,
80 resolver: Box<dyn Resolver>,
81}
82
83impl DnsSeedsProvider {
84 /// Build a provider that queries `name` via `resolver`.
85 ///
86 /// # Examples
87 ///
88 /// ```
89 /// use dynomite::seeds::dns::{DnsSeedsProvider, Resolver, ResolvedSeeds};
90 /// struct R;
91 /// impl Resolver for R {
92 /// fn resolve(&self, _: &str)
93 /// -> Result<ResolvedSeeds, dynomite::seeds::SeedsError>
94 /// {
95 /// Ok(ResolvedSeeds::Txt(Vec::new()))
96 /// }
97 /// }
98 /// let p = DnsSeedsProvider::new("n".into(), Box::new(R));
99 /// assert_eq!(p.name(), "n");
100 /// ```
101 #[must_use]
102 pub fn new(name: String, resolver: Box<dyn Resolver>) -> Self {
103 Self { name, resolver }
104 }
105
106 /// DNS query name.
107 #[must_use]
108 pub fn name(&self) -> &str {
109 &self.name
110 }
111}
112
113impl std::fmt::Debug for DnsSeedsProvider {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("DnsSeedsProvider")
116 .field("name", &self.name)
117 .finish_non_exhaustive()
118 }
119}
120
121impl SeedsProvider for DnsSeedsProvider {
122 fn get_seeds(&self) -> Result<Vec<ConfDynSeed>, SeedsError> {
123 let resolved = self.resolver.resolve(&self.name)?;
124 match resolved {
125 ResolvedSeeds::Txt(entries) => {
126 let mut out = Vec::with_capacity(entries.len());
127 for raw in entries {
128 let seed =
129 ConfDynSeed::parse(&raw).map_err(|e| SeedsError::Parse(e.to_string()))?;
130 out.push(seed);
131 }
132 Ok(out)
133 }
134 ResolvedSeeds::A {
135 ips,
136 port,
137 rack,
138 dc,
139 tokens,
140 } => {
141 let mut out = Vec::with_capacity(ips.len());
142 for ip in ips {
143 let raw = format!("{ip}:{port}:{rack}:{dc}:{tokens}");
144 let seed =
145 ConfDynSeed::parse(&raw).map_err(|e| SeedsError::Parse(e.to_string()))?;
146 out.push(seed);
147 }
148 Ok(out)
149 }
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 struct StaticResolver(ResolvedSeeds);
159 impl Resolver for StaticResolver {
160 fn resolve(&self, _: &str) -> Result<ResolvedSeeds, SeedsError> {
161 Ok(self.0.clone())
162 }
163 }
164
165 #[test]
166 fn txt_branch() {
167 let r = StaticResolver(ResolvedSeeds::Txt(vec![
168 "127.0.0.1:8101:rA:dc1:1".into(),
169 "127.0.0.2:8101:rA:dc1:2".into(),
170 ]));
171 let p = DnsSeedsProvider::new("n".into(), Box::new(r));
172 let v = p.get_seeds().unwrap();
173 assert_eq!(v.len(), 2);
174 assert_eq!(v[0].host(), "127.0.0.1");
175 }
176
177 #[test]
178 fn a_branch_synthesises_seed_format() {
179 let r = StaticResolver(ResolvedSeeds::A {
180 ips: vec!["10.0.0.1".into(), "10.0.0.2".into()],
181 port: 8101,
182 rack: "rA".into(),
183 dc: "dc1".into(),
184 tokens: "1".into(),
185 });
186 let p = DnsSeedsProvider::new("n".into(), Box::new(r));
187 let v = p.get_seeds().unwrap();
188 assert_eq!(v.len(), 2);
189 assert_eq!(v[0].port(), 8101);
190 assert_eq!(v[0].dc(), "dc1");
191 }
192
193 #[test]
194 fn parse_error_propagates() {
195 let r = StaticResolver(ResolvedSeeds::Txt(vec!["invalid-seed".into()]));
196 let p = DnsSeedsProvider::new("n".into(), Box::new(r));
197 assert!(matches!(p.get_seeds(), Err(SeedsError::Parse(_))));
198 }
199}