Skip to main content

dynomite/conf/
server.rs

1//! Datastore server and dynomite peer seed parsing.
2//!
3//! `servers:` entries take the colon-delimited form
4//! `host:port:weight [name]` (or `/path/sock:weight [name]` for a Unix
5//! domain socket). `dyn_seeds:` entries take the longer form
6//! `host:port:rack:dc:tokens [name]`. Both forms allow an optional
7//! trailing space-delimited friendly name.
8
9use std::fmt;
10
11use serde::de::{self, Deserializer, Visitor};
12use serde::{Deserialize, Serialize};
13
14use super::error::ConfError;
15use super::tokens::TokenList;
16
17/// Default ketama port: when a server omits an explicit name and runs
18/// on this port, the port is dropped from the consistent-hash key for
19/// libmemcached compatibility. Mirrors `CONF_DEFAULT_KETAMA_PORT`.
20const KETAMA_DEFAULT_PORT: u16 = 11_211;
21
22/// A `servers:` entry: a single backing datastore endpoint.
23///
24/// # Examples
25///
26/// ```
27/// use dynomite::conf::ConfServer;
28/// let s = ConfServer::parse("127.0.0.1:6379:1 redis_a").unwrap();
29/// assert_eq!(s.host(), "127.0.0.1");
30/// assert_eq!(s.port(), 6379);
31/// assert_eq!(s.weight(), 1);
32/// assert_eq!(s.name(), "redis_a");
33/// ```
34#[derive(Debug, Clone, Eq, PartialEq)]
35pub struct ConfServer {
36    pname: String,
37    name: String,
38    host: String,
39    port: u16,
40    weight: u32,
41    is_unix: bool,
42}
43
44impl ConfServer {
45    /// Parse a `host:port:weight [name]` (or `/path:weight [name]`) string.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use dynomite::conf::ConfServer;
51    /// let unix = ConfServer::parse("/tmp/redis.sock:1").unwrap();
52    /// assert!(unix.is_unix());
53    /// assert_eq!(unix.port(), 0);
54    /// assert!(ConfServer::parse("").is_err());
55    /// ```
56    pub fn parse(raw: &str) -> Result<Self, ConfError> {
57        let bad = |reason: &str| ConfError::BadServer {
58            field: "servers",
59            value: raw.to_string(),
60            reason: reason.to_string(),
61        };
62
63        if raw.is_empty() {
64            return Err(bad("empty value"));
65        }
66
67        let (head, friendly_name) = split_optional_friendly_name(raw);
68        let head = head.trim_end();
69
70        if let Some(rest) = head.strip_prefix('/') {
71            // /path/socket:weight
72            let (path_no_prefix, weight) =
73                split_last_colon(rest).ok_or_else(|| bad("unix path requires ':weight' suffix"))?;
74            let weight = parse_weight(weight).ok_or_else(|| bad("invalid weight"))?;
75            let path = format!("/{path_no_prefix}");
76            let name = friendly_name.map_or_else(|| path.clone(), str::to_string);
77            let pname = head.to_string();
78            return Ok(Self {
79                pname,
80                name,
81                host: path,
82                port: 0,
83                weight,
84                is_unix: true,
85            });
86        }
87
88        // host:port:weight
89        let (head_no_weight, weight_str) =
90            split_last_colon(head).ok_or_else(|| bad("expected 'host:port:weight'"))?;
91        let weight = parse_weight(weight_str).ok_or_else(|| bad("invalid weight"))?;
92
93        let (host, port_str) =
94            split_last_colon(head_no_weight).ok_or_else(|| bad("expected 'host:port:weight'"))?;
95        let port = parse_port(port_str).ok_or_else(|| bad("port must be in 1..=65535"))?;
96        if host.is_empty() {
97            return Err(bad("empty host"));
98        }
99
100        let name = match friendly_name {
101            Some(n) => n.to_string(),
102            None => {
103                if port == KETAMA_DEFAULT_PORT {
104                    host.to_string()
105                } else {
106                    format!("{host}:{port_str}")
107                }
108            }
109        };
110
111        Ok(Self {
112            pname: head.to_string(),
113            name,
114            host: host.to_string(),
115            port,
116            weight,
117            is_unix: false,
118        })
119    }
120
121    /// The original `host:port:weight` portion (without any friendly name).
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use dynomite::conf::ConfServer;
127    /// let s = ConfServer::parse("127.0.0.1:6379:1 redis_a").unwrap();
128    /// assert_eq!(s.pname(), "127.0.0.1:6379:1");
129    /// ```
130    pub fn pname(&self) -> &str {
131        &self.pname
132    }
133    /// The hashing-key name for this server.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use dynomite::conf::ConfServer;
139    /// // Port 11211 is treated as a default and dropped from the name.
140    /// assert_eq!(ConfServer::parse("10.0.0.1:11211:1").unwrap().name(), "10.0.0.1");
141    /// assert_eq!(ConfServer::parse("10.0.0.1:6379:1").unwrap().name(), "10.0.0.1:6379");
142    /// ```
143    pub fn name(&self) -> &str {
144        &self.name
145    }
146    /// Hostname or IP address for an inet entry, or the Unix socket path.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use dynomite::conf::ConfServer;
152    /// assert_eq!(ConfServer::parse("127.0.0.1:6379:1").unwrap().host(), "127.0.0.1");
153    /// ```
154    pub fn host(&self) -> &str {
155        &self.host
156    }
157    /// TCP port; `0` for Unix socket entries.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use dynomite::conf::ConfServer;
163    /// assert_eq!(ConfServer::parse("127.0.0.1:6379:1").unwrap().port(), 6379);
164    /// assert_eq!(ConfServer::parse("/var/run/r.sock:1").unwrap().port(), 0);
165    /// ```
166    pub fn port(&self) -> u16 {
167        self.port
168    }
169    /// Configured weight (parsed for backward compatibility; the engine
170    /// ignores it once parsed).
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use dynomite::conf::ConfServer;
176    /// assert_eq!(ConfServer::parse("127.0.0.1:6379:42").unwrap().weight(), 42);
177    /// ```
178    pub fn weight(&self) -> u32 {
179        self.weight
180    }
181    /// Whether this entry refers to a Unix domain socket.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use dynomite::conf::ConfServer;
187    /// assert!(ConfServer::parse("/var/run/r.sock:1").unwrap().is_unix());
188    /// assert!(!ConfServer::parse("127.0.0.1:6379:1").unwrap().is_unix());
189    /// ```
190    pub fn is_unix(&self) -> bool {
191        self.is_unix
192    }
193}
194
195impl fmt::Display for ConfServer {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.write_str(&self.pname)
198    }
199}
200
201impl Serialize for ConfServer {
202    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
203        ser.serialize_str(&self.pname)
204    }
205}
206
207impl<'de> Deserialize<'de> for ConfServer {
208    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
209        struct V;
210        impl Visitor<'_> for V {
211            type Value = ConfServer;
212            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213                f.write_str("a 'host:port:weight' or '/path:weight' server entry")
214            }
215            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
216                ConfServer::parse(v).map_err(|e| E::custom(e.to_string()))
217            }
218        }
219        de.deserialize_str(V)
220    }
221}
222
223/// A `dyn_seeds:` entry: a peer dynomite node with rack / dc / tokens.
224///
225/// # Examples
226///
227/// ```
228/// use dynomite::conf::ConfDynSeed;
229/// let s = ConfDynSeed::parse("127.0.0.2:8101:rack2:dc2:1383429731").unwrap();
230/// assert_eq!(s.rack(), "rack2");
231/// assert_eq!(s.dc(), "dc2");
232/// ```
233#[derive(Debug, Clone, Eq, PartialEq)]
234pub struct ConfDynSeed {
235    pname: String,
236    name: String,
237    host: String,
238    port: u16,
239    rack: String,
240    dc: String,
241    tokens: TokenList,
242}
243
244impl ConfDynSeed {
245    /// Parse a `host:port:rack:dc:tokens [name]` entry.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use dynomite::conf::ConfDynSeed;
251    /// let s = ConfDynSeed::parse("h:1:r:d:1,2,3 friendly").unwrap();
252    /// assert_eq!(s.tokens().len(), 3);
253    /// assert_eq!(s.name(), "friendly");
254    /// assert!(ConfDynSeed::parse("a:b:c:d").is_err());
255    /// ```
256    pub fn parse(raw: &str) -> Result<Self, ConfError> {
257        let bad = |reason: &str| ConfError::BadServer {
258            field: "dyn_seeds",
259            value: raw.to_string(),
260            reason: reason.to_string(),
261        };
262
263        if raw.is_empty() {
264            return Err(bad("empty value"));
265        }
266
267        let (head, friendly_name) = split_optional_friendly_name(raw);
268        let head = head.trim_end();
269
270        // tokens
271        let (head, tokens_str) =
272            split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
273        // dc
274        let (head, dc) =
275            split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
276        // rack
277        let (head, rack) =
278            split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
279        // port
280        let (host, port_str) =
281            split_last_colon(head).ok_or_else(|| bad("expected 'host:port:rack:dc:tokens'"))?;
282
283        if host.is_empty() {
284            return Err(bad("empty host"));
285        }
286        if rack.is_empty() {
287            return Err(bad("empty rack"));
288        }
289        if dc.is_empty() {
290            return Err(bad("empty dc"));
291        }
292
293        let port = parse_port(port_str).ok_or_else(|| bad("port must be in 1..=65535"))?;
294        let tokens = TokenList::parse(tokens_str).map_err(|e| ConfError::BadServer {
295            field: "dyn_seeds",
296            value: raw.to_string(),
297            reason: e.to_string(),
298        })?;
299
300        let name = match friendly_name {
301            Some(n) => n.to_string(),
302            None => {
303                if port == KETAMA_DEFAULT_PORT {
304                    host.to_string()
305                } else {
306                    format!("{host}:{port_str}")
307                }
308            }
309        };
310
311        Ok(Self {
312            pname: head.to_string(),
313            name,
314            host: host.to_string(),
315            port,
316            rack: rack.to_string(),
317            dc: dc.to_string(),
318            tokens,
319        })
320    }
321
322    /// The colon-joined `host:port` portion (rack, dc and tokens are
323    /// stripped from the input during parsing).
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use dynomite::conf::ConfDynSeed;
329    /// let s = ConfDynSeed::parse("h:1:r:d:1 friendly").unwrap();
330    /// assert_eq!(s.pname(), "h:1");
331    /// ```
332    pub fn pname(&self) -> &str {
333        &self.pname
334    }
335    /// Hashing-key name.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use dynomite::conf::ConfDynSeed;
341    /// assert_eq!(
342    ///     ConfDynSeed::parse("h:1:r:d:1 friendly").unwrap().name(),
343    ///     "friendly",
344    /// );
345    /// ```
346    pub fn name(&self) -> &str {
347        &self.name
348    }
349    /// Hostname or IP.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use dynomite::conf::ConfDynSeed;
355    /// assert_eq!(ConfDynSeed::parse("node-a:1:r:d:1").unwrap().host(), "node-a");
356    /// ```
357    pub fn host(&self) -> &str {
358        &self.host
359    }
360    /// TCP port.
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// use dynomite::conf::ConfDynSeed;
366    /// assert_eq!(ConfDynSeed::parse("h:8101:r:d:1").unwrap().port(), 8101);
367    /// ```
368    pub fn port(&self) -> u16 {
369        self.port
370    }
371    /// Logical rack.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use dynomite::conf::ConfDynSeed;
377    /// assert_eq!(ConfDynSeed::parse("h:1:rack-x:d:1").unwrap().rack(), "rack-x");
378    /// ```
379    pub fn rack(&self) -> &str {
380        &self.rack
381    }
382    /// Logical datacenter.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use dynomite::conf::ConfDynSeed;
388    /// assert_eq!(ConfDynSeed::parse("h:1:r:dc-x:1").unwrap().dc(), "dc-x");
389    /// ```
390    pub fn dc(&self) -> &str {
391        &self.dc
392    }
393    /// Token list owned by this seed.
394    ///
395    /// # Examples
396    ///
397    /// ```
398    /// use dynomite::conf::ConfDynSeed;
399    /// assert_eq!(ConfDynSeed::parse("h:1:r:d:1,2,3").unwrap().tokens().len(), 3);
400    /// ```
401    pub fn tokens(&self) -> &TokenList {
402        &self.tokens
403    }
404}
405
406impl fmt::Display for ConfDynSeed {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        f.write_str(&self.pname)
409    }
410}
411
412impl Serialize for ConfDynSeed {
413    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
414        ser.serialize_str(&self.pname)
415    }
416}
417
418impl<'de> Deserialize<'de> for ConfDynSeed {
419    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
420        struct V;
421        impl Visitor<'_> for V {
422            type Value = ConfDynSeed;
423            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424                f.write_str("a 'host:port:rack:dc:tokens' dyn_seeds entry")
425            }
426            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
427                ConfDynSeed::parse(v).map_err(|e| E::custom(e.to_string()))
428            }
429        }
430        de.deserialize_str(V)
431    }
432}
433
434/// Strip the optional space-separated friendly name suffix from a
435/// colon-delimited entry. Returns `(head, name)`.
436fn split_optional_friendly_name(raw: &str) -> (&str, Option<&str>) {
437    if let Some(idx) = raw.rfind(' ') {
438        // Anything after the last space is the friendly name when the
439        // head still contains a colon; the C parser is greedy on the
440        // rightmost space.
441        let (head, tail) = raw.split_at(idx);
442        let tail = &tail[1..];
443        if !tail.is_empty() && head.contains(':') {
444            return (head, Some(tail));
445        }
446    }
447    (raw, None)
448}
449
450fn split_last_colon(s: &str) -> Option<(&str, &str)> {
451    let idx = s.rfind(':')?;
452    Some((&s[..idx], &s[idx + 1..]))
453}
454
455fn parse_port(s: &str) -> Option<u16> {
456    let n: u16 = s.parse().ok()?;
457    if n > 0 {
458        Some(n)
459    } else {
460        None
461    }
462}
463
464fn parse_weight(s: &str) -> Option<u32> {
465    s.parse().ok()
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn server_basic() {
474        let s = ConfServer::parse("127.0.0.1:22122:1").unwrap();
475        assert_eq!(s.host(), "127.0.0.1");
476        assert_eq!(s.port(), 22122);
477        assert_eq!(s.weight(), 1);
478        assert_eq!(s.name(), "127.0.0.1:22122");
479        assert!(!s.is_unix());
480    }
481
482    #[test]
483    fn server_with_friendly_name() {
484        let s = ConfServer::parse("127.0.0.1:6379:1 redis_a").unwrap();
485        assert_eq!(s.host(), "127.0.0.1");
486        assert_eq!(s.port(), 6379);
487        assert_eq!(s.name(), "redis_a");
488        assert_eq!(s.pname(), "127.0.0.1:6379:1");
489    }
490
491    #[test]
492    fn server_default_ketama_port_drops_port_from_name() {
493        let s = ConfServer::parse("10.0.0.1:11211:1").unwrap();
494        assert_eq!(s.name(), "10.0.0.1");
495    }
496
497    #[test]
498    fn server_unix_socket() {
499        let s = ConfServer::parse("/tmp/redis.sock:1").unwrap();
500        assert!(s.is_unix());
501        assert_eq!(s.host(), "/tmp/redis.sock");
502        assert_eq!(s.port(), 0);
503    }
504
505    #[test]
506    fn server_bad_format() {
507        assert!(ConfServer::parse("just-a-host").is_err());
508        assert!(ConfServer::parse("a:b:c").is_err());
509        assert!(ConfServer::parse("").is_err());
510    }
511
512    #[test]
513    fn dyn_seed_basic() {
514        let s = ConfDynSeed::parse("127.0.0.2:8101:rack2:dc2:1383429731").unwrap();
515        assert_eq!(s.host(), "127.0.0.2");
516        assert_eq!(s.port(), 8101);
517        assert_eq!(s.rack(), "rack2");
518        assert_eq!(s.dc(), "dc2");
519        assert_eq!(s.tokens().to_string(), "1383429731");
520    }
521
522    #[test]
523    fn dyn_seed_multi_tokens() {
524        let s = ConfDynSeed::parse("h:1:r:d:1,2,3 friendly").unwrap();
525        assert_eq!(s.tokens().len(), 3);
526        assert_eq!(s.name(), "friendly");
527    }
528
529    #[test]
530    fn dyn_seed_bad() {
531        assert!(ConfDynSeed::parse("a:b:c:d").is_err());
532        assert!(ConfDynSeed::parse("h:1:r::1").is_err());
533    }
534}