hopr_ct_full_network/
config.rs1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use validator::{Validate, ValidationError, ValidationErrors};
4
5#[derive(Debug, Clone, Copy, PartialEq, smart_default::SmartDefault)]
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(deny_unknown_fields))]
8pub struct ProberConfig {
9 #[cfg_attr(
11 feature = "serde",
12 serde(default = "default_probing_interval", with = "humantime_serde")
13 )]
14 #[default(default_probing_interval())]
15 pub interval: std::time::Duration,
16
17 #[cfg_attr(feature = "serde", serde(default = "default_staleness_weight"))]
23 #[default(default_staleness_weight())]
24 pub staleness_weight: f64,
25
26 #[cfg_attr(feature = "serde", serde(default = "default_quality_weight"))]
32 #[default(default_quality_weight())]
33 pub quality_weight: f64,
34
35 #[cfg_attr(feature = "serde", serde(default = "default_base_priority"))]
42 #[default(default_base_priority())]
43 pub base_priority: f64,
44
45 #[cfg_attr(feature = "serde", serde(default = "default_shuffle_ttl", with = "humantime_serde"))]
50 #[default(default_shuffle_ttl())]
51 pub shuffle_ttl: std::time::Duration,
52
53 #[cfg_attr(feature = "serde", serde(default = "just_true"))]
60 #[default(just_true())]
61 pub probe_connected_only: bool,
62}
63
64impl ProberConfig {
65 pub fn validate_against_probe_timeout(&self, probe_timeout: std::time::Duration) -> anyhow::Result<()> {
70 self.validate()
71 .map_err(|e| anyhow::anyhow!("invalid ProberConfig: {e}"))?;
72 anyhow::ensure!(
73 self.interval >= probe_timeout,
74 "ProberConfig.interval ({:?}) must be >= probe_timeout ({:?})",
75 self.interval,
76 probe_timeout,
77 );
78 Ok(())
79 }
80}
81
82impl Validate for ProberConfig {
83 fn validate(&self) -> Result<(), ValidationErrors> {
84 let mut errors = ValidationErrors::new();
85
86 if !(0.0..=1.0).contains(&self.staleness_weight) {
87 errors.add(
88 "staleness_weight",
89 ValidationError::new("staleness_weight must be between 0.0 and 1.0"),
90 );
91 }
92 if !(0.0..=1.0).contains(&self.quality_weight) {
93 errors.add(
94 "quality_weight",
95 ValidationError::new("quality_weight must be between 0.0 and 1.0"),
96 );
97 }
98 if !(0.0..=1.0).contains(&self.base_priority) {
99 errors.add(
100 "base_priority",
101 ValidationError::new("base_priority must be between 0.0 and 1.0"),
102 );
103 }
104
105 if self.staleness_weight + self.quality_weight + self.base_priority <= 0.0 {
106 errors.add(
107 "weights",
108 ValidationError::new("at least one priority weight must be positive"),
109 );
110 }
111
112 if errors.is_empty() { Ok(()) } else { Err(errors) }
113 }
114}
115
116#[inline]
117const fn default_staleness_weight() -> f64 {
118 0.4
119}
120
121#[inline]
122const fn default_quality_weight() -> f64 {
123 0.3
124}
125
126#[inline]
127const fn default_base_priority() -> f64 {
128 0.3
129}
130
131#[inline]
132const fn default_shuffle_ttl() -> std::time::Duration {
133 std::time::Duration::from_secs(default_probing_interval().as_secs() * 2)
134}
135
136#[inline]
137const fn default_probing_interval() -> std::time::Duration {
138 std::time::Duration::from_secs(30)
139}
140
141#[inline]
142const fn just_true() -> bool {
143 true
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn interval_less_than_probe_timeout_is_invalid() {
152 let cfg = ProberConfig::default(); let timeout = std::time::Duration::from_secs(60);
154 assert!(cfg.validate_against_probe_timeout(timeout).is_err());
155 }
156
157 #[test]
158 fn interval_equal_to_probe_timeout_is_valid() {
159 let cfg = ProberConfig::default(); let timeout = std::time::Duration::from_secs(30);
161 assert!(cfg.validate_against_probe_timeout(timeout).is_ok());
162 }
163
164 #[test]
165 fn default_config_is_valid() {
166 let cfg = ProberConfig::default();
167 assert!(cfg.validate().is_ok());
168 assert!(cfg.probe_connected_only, "probe_connected_only should default to true");
169 }
170
171 #[test]
172 fn all_zero_weights_are_invalid() {
173 let cfg = ProberConfig {
174 staleness_weight: 0.0,
175 quality_weight: 0.0,
176 base_priority: 0.0,
177 ..Default::default()
178 };
179 let err = cfg.validate().unwrap_err();
180 assert!(err.field_errors().contains_key("weights"));
181 }
182
183 #[test]
184 fn zero_staleness_weight_alone_is_valid() {
185 let cfg = ProberConfig {
186 staleness_weight: 0.0,
187 ..Default::default()
188 };
189 assert!(cfg.validate().is_ok());
190 }
191
192 #[test]
193 fn zero_quality_weight_alone_is_valid() {
194 let cfg = ProberConfig {
195 quality_weight: 0.0,
196 ..Default::default()
197 };
198 assert!(cfg.validate().is_ok());
199 }
200
201 #[test]
202 fn zero_base_priority_alone_is_valid() {
203 let cfg = ProberConfig {
204 base_priority: 0.0,
205 ..Default::default()
206 };
207 assert!(cfg.validate().is_ok());
208 }
209
210 #[test]
211 fn staleness_weight_above_one_is_invalid() {
212 let cfg = ProberConfig {
213 staleness_weight: 1.1,
214 ..Default::default()
215 };
216 let err = cfg.validate().unwrap_err();
217 assert!(err.field_errors().contains_key("staleness_weight"));
218 }
219
220 #[test]
221 fn quality_weight_above_one_is_invalid() {
222 let cfg = ProberConfig {
223 quality_weight: 1.1,
224 ..Default::default()
225 };
226 let err = cfg.validate().unwrap_err();
227 assert!(err.field_errors().contains_key("quality_weight"));
228 }
229
230 #[test]
231 fn base_priority_above_one_is_invalid() {
232 let cfg = ProberConfig {
233 base_priority: 1.1,
234 ..Default::default()
235 };
236 let err = cfg.validate().unwrap_err();
237 assert!(err.field_errors().contains_key("base_priority"));
238 }
239
240 #[test]
241 fn negative_staleness_weight_is_invalid() {
242 let cfg = ProberConfig {
243 staleness_weight: -0.1,
244 quality_weight: 0.5,
245 base_priority: 0.5,
246 ..Default::default()
247 };
248 let err = cfg.validate().unwrap_err();
249 assert!(err.field_errors().contains_key("staleness_weight"));
250 }
251
252 #[test]
253 fn negative_quality_weight_is_invalid() {
254 let cfg = ProberConfig {
255 staleness_weight: 0.5,
256 quality_weight: -0.1,
257 base_priority: 0.5,
258 ..Default::default()
259 };
260 let err = cfg.validate().unwrap_err();
261 assert!(err.field_errors().contains_key("quality_weight"));
262 }
263
264 #[test]
265 fn negative_base_priority_is_invalid() {
266 let cfg = ProberConfig {
267 staleness_weight: 0.5,
268 quality_weight: 0.5,
269 base_priority: -0.1,
270 ..Default::default()
271 };
272 let err = cfg.validate().unwrap_err();
273 assert!(err.field_errors().contains_key("base_priority"));
274 }
275}