Skip to main content

brz_memcache/
cacheservice.rs

1//! Application-supplied cache topology and native configuration parsing.
2
3use std::collections::HashMap;
4
5use serde::Deserialize;
6
7/// Parsed cache-service configuration: a map from namespace name to its
8/// configuration block. Unknown YAML fields are ignored.
9#[derive(Clone, Debug, Deserialize)]
10pub struct CacheServiceConfig(HashMap<String, CacheNamespaceConf>);
11
12impl CacheServiceConfig {
13    /// Parse native cache configuration without legacy format conversions.
14    pub fn from_yaml_str(yaml: &str) -> Result<Self, CacheServiceError> {
15        serde_yaml::from_str(yaml).map_err(|e| CacheServiceError::Yaml(e.to_string()))
16    }
17
18    /// Returns the master list for the given namespace, if present.
19    pub fn masters_of(&self, namespace: &str) -> Option<&[String]> {
20        self.0.get(namespace).map(|c| c.master.as_slice())
21    }
22
23    /// Returns the configuration block for the given namespace, if present.
24    pub fn namespace(&self, namespace: &str) -> Option<&CacheNamespaceConf> {
25        self.0.get(namespace)
26    }
27}
28
29/// One namespace block within a [`CacheServiceConfig`].
30#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
31pub struct CacheNamespaceConf {
32    #[serde(default)]
33    pub hash: Option<String>,
34    #[serde(default)]
35    pub distribution: Option<String>,
36    #[serde(default)]
37    pub master: Vec<String>,
38    #[serde(default)]
39    pub slave: Vec<String>,
40    #[serde(default, rename = "master_l1")]
41    pub master_l1: Vec<Vec<String>>,
42    #[serde(default, rename = "slave_l1")]
43    pub slave_l1: Vec<Vec<String>>,
44    #[serde(default)]
45    pub exptime: i64,
46    #[serde(default)]
47    pub timeout_ms_master: u32,
48    #[serde(default)]
49    pub timeout_ms_slave: u32,
50    #[serde(default = "default_true")]
51    pub update_slave_l1: bool,
52    #[serde(default)]
53    pub local_affinity: bool,
54    #[serde(default)]
55    pub backend_no_storage: bool,
56}
57
58impl CacheNamespaceConf {
59    pub fn single_master(endpoint: String) -> Self {
60        Self {
61            hash: None,
62            distribution: None,
63            master: vec![endpoint],
64            slave: Vec::new(),
65            master_l1: Vec::new(),
66            slave_l1: Vec::new(),
67            exptime: 0,
68            timeout_ms_master: 0,
69            timeout_ms_slave: 0,
70            update_slave_l1: true,
71            local_affinity: false,
72            backend_no_storage: false,
73        }
74    }
75
76    /// Hashing algorithm (e.g. `crc32`).
77    pub fn hash(&self) -> Option<&str> {
78        self.hash.as_deref()
79    }
80
81    /// Distribution strategy (e.g. `modula`, `ketama`).
82    pub fn distribution(&self) -> Option<&str> {
83        self.distribution.as_deref()
84    }
85
86    /// Master endpoints in configured order (`host:port`).
87    pub fn masters(&self) -> &[String] {
88        &self.master
89    }
90
91    /// Slave endpoints in configured order (`host:port`); empty when the
92    /// namespace has no slave pool.
93    pub fn slaves(&self) -> &[String] {
94        &self.slave
95    }
96
97    /// L1 master endpoint groups; each inner list is one group of `host:port`.
98    pub fn master_l1(&self) -> &[Vec<String>] {
99        &self.master_l1
100    }
101
102    /// L1 slave endpoint groups; each inner list is one group of `host:port`.
103    pub fn slave_l1(&self) -> &[Vec<String>] {
104        &self.slave_l1
105    }
106
107    /// Expiration in milliseconds used when a read hit is written back into
108    /// the first missed replica group.
109    pub fn writeback_expiration_ms(&self) -> i64 {
110        self.exptime
111    }
112
113    pub fn timeout_ms_master(&self) -> u32 {
114        self.timeout_ms_master
115    }
116
117    pub fn timeout_ms_slave(&self) -> u32 {
118        self.timeout_ms_slave
119    }
120
121    pub fn update_slave_l1(&self) -> bool {
122        self.update_slave_l1
123    }
124
125    pub fn local_affinity(&self) -> bool {
126        self.local_affinity
127    }
128
129    pub fn backend_no_storage(&self) -> bool {
130        self.backend_no_storage
131    }
132}
133
134fn default_true() -> bool {
135    true
136}
137
138/// Errors returned by cache-service YAML parsing.
139#[derive(Debug, thiserror::Error)]
140pub enum CacheServiceError {
141    /// The YAML document could not be parsed.
142    #[error("cache-service yaml parse error: {0}")]
143    Yaml(String),
144
145    /// The namespace identifier was empty or contained control characters.
146    #[error("invalid cache-service namespace: {0:?}")]
147    InvalidNamespace(String),
148
149    /// The namespace was not present in the parsed YAML document.
150    #[error("cache-service namespace not found: {0}")]
151    MissingNamespace(String),
152}
153
154/// Looks up the master list for `namespace` in a cache-service YAML document.
155///
156/// Convenience wrapper around [`CacheServiceConfig::from_yaml_str`] +
157/// [`CacheServiceConfig::masters_of`]: parses `yaml` and returns the master
158/// endpoints for `namespace`, or an error if the namespace is absent.
159///
160/// `namespace` must be non-empty (after trimming) and contain no control
161/// characters; otherwise an [`CacheServiceError::InvalidNamespace`] is returned
162/// before parsing.
163pub fn masters_from_yaml(yaml: &str, namespace: &str) -> Result<Vec<String>, CacheServiceError> {
164    if namespace.trim().is_empty() || namespace.chars().any(char::is_control) {
165        return Err(CacheServiceError::InvalidNamespace(namespace.to_string()));
166    }
167    let cfg = CacheServiceConfig::from_yaml_str(yaml)?;
168    cfg.masters_of(namespace)
169        .map(|m| m.to_vec())
170        .ok_or_else(|| CacheServiceError::MissingNamespace(namespace.to_string()))
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    const SAMPLE_YAML: &str = "example-abtest:\n  hash: crc32\n  distribution: modula\n  hash_tag: example-abtest\n  master:\n  - 192.0.4.103:15138\n  - 192.0.0.95:15138\n  - 192.0.0.131:15138\n  - 192.0.0.209:15138\n  slave_l1:\n  - - 192.0.2.203:15138\n    - 192.0.2.204:15138\n";
178
179    #[test]
180    fn parses_cache_service_yaml_namespace() {
181        let cfg = CacheServiceConfig::from_yaml_str(SAMPLE_YAML).unwrap();
182        let ns = cfg.namespace("example-abtest").unwrap();
183        // Native configuration preserves the explicitly supplied hash name.
184        assert_eq!(ns.hash(), Some("crc32"));
185        assert_eq!(ns.distribution(), Some(crate::sharding::DIST_MODULA));
186        assert_eq!(
187            ns.masters(),
188            &[
189                "192.0.4.103:15138",
190                "192.0.0.95:15138",
191                "192.0.0.131:15138",
192                "192.0.0.209:15138",
193            ]
194        );
195        assert_eq!(ns.slave_l1().len(), 1);
196        assert_eq!(
197            ns.slave_l1()[0],
198            &["192.0.2.203:15138", "192.0.2.204:15138"]
199        );
200    }
201
202    #[test]
203    fn masters_of_returns_none_for_missing_namespace() {
204        let cfg = CacheServiceConfig::from_yaml_str(SAMPLE_YAML).unwrap();
205        assert!(cfg.masters_of("nope").is_none());
206    }
207
208    #[test]
209    fn masters_from_yaml_returns_master_list() {
210        let masters = masters_from_yaml(SAMPLE_YAML, "example-abtest").unwrap();
211        assert_eq!(
212            masters,
213            vec![
214                "192.0.4.103:15138",
215                "192.0.0.95:15138",
216                "192.0.0.131:15138",
217                "192.0.0.209:15138",
218            ]
219        );
220    }
221
222    #[test]
223    fn masters_from_yaml_errors_on_missing_namespace() {
224        assert!(matches!(
225            masters_from_yaml(SAMPLE_YAML, "missing-namespace"),
226            Err(CacheServiceError::MissingNamespace(n)) if n == "missing-namespace"
227        ));
228    }
229
230    #[test]
231    fn masters_from_yaml_rejects_empty_namespace() {
232        assert!(matches!(
233            masters_from_yaml(SAMPLE_YAML, " \t"),
234            Err(CacheServiceError::InvalidNamespace(_))
235        ));
236    }
237
238    #[test]
239    fn ignores_unknown_yaml_fields() {
240        // Extra top-level keys and extra fields within a namespace must be
241        // silently ignored, not cause a parse failure.
242        let yaml = "other-namespace:\n  hash: bkdr\nexample-abtest:\n  master:\n  - 1.2.3.4:15138\n  custom_field: whatever\n  nested:\n    a: 1\n";
243        let masters = masters_from_yaml(yaml, "example-abtest").unwrap();
244        assert_eq!(masters, vec!["1.2.3.4:15138"]);
245    }
246
247    #[test]
248    fn masters_defaults_to_empty_when_absent() {
249        // A namespace with no `master:` key parses to an empty master list
250        // (serde default), not an error.
251        let yaml = "example-abtest:\n  hash: crc32\n";
252        let cfg = CacheServiceConfig::from_yaml_str(yaml).unwrap();
253        assert_eq!(cfg.masters_of("example-abtest").unwrap(), &[] as &[String]);
254    }
255
256    #[test]
257    fn malformed_yaml_is_an_error() {
258        assert!(matches!(
259            CacheServiceConfig::from_yaml_str("not: [valid: yaml"),
260            Err(CacheServiceError::Yaml(_))
261        ));
262    }
263}