diff --git a/candidate-selection/src/num.rs b/candidate-selection/src/num.rs index 2c5481c..4acbf38 100644 --- a/candidate-selection/src/num.rs +++ b/candidate-selection/src/num.rs @@ -16,6 +16,10 @@ impl Normalized { Some(Self(value)) } + pub fn clamp(value: f64, min: f64, max: f64) -> Option { + Self::new(value.clamp(min, max)) + } + pub fn as_inner(&self) -> NotNan { self.0 } diff --git a/indexer-selection/src/lib.rs b/indexer-selection/src/lib.rs index d91d4b9..da53d3f 100644 --- a/indexer-selection/src/lib.rs +++ b/indexer-selection/src/lib.rs @@ -157,11 +157,11 @@ fn score_zero_allocation(zero_allocation: bool) -> Normalized { /// https://www.desmos.com/calculator/v2vrfktlpl pub fn score_latency(latency_ms: u16) -> Normalized { let s = |x: u16| 1.0 + E.powf(((x as f64) - 400.0) / 300.0); - Normalized::new(s(0) / s(latency_ms)).unwrap() + // Since high latency becomes bad success rate via timeouts, latency scores should have a floor. + Normalized::clamp(s(0) / s(latency_ms), 0.001, 1.0).unwrap() } /// https://www.desmos.com/calculator/df2keku3ad fn score_success_rate(success_rate: Normalized) -> Normalized { - let min_score = 1e-8; - Normalized::new(success_rate.as_f64().powi(7).max(min_score)).unwrap() + Normalized::clamp(success_rate.as_f64().powi(7), 1e-8, 1.0).unwrap() } diff --git a/indexer-selection/src/test.rs b/indexer-selection/src/test.rs index 41a1dfa..9693d99 100644 --- a/indexer-selection/src/test.rs +++ b/indexer-selection/src/test.rs @@ -162,6 +162,62 @@ fn sensitivity_seconds_behind() { ); } +#[test] +fn sensitivity_seconds_behind_vs_latency() { + let candidates = [ + Candidate { + indexer: hex!("0000000000000000000000000000000000000000").into(), + deployment: hex!("0000000000000000000000000000000000000000000000000000000000000000") + .into(), + url: "https://example.com".parse().unwrap(), + perf: ExpectedPerformance { + success_rate: Normalized::new(0.99).unwrap(), + latency_ms: 0, + }, + fee: Normalized::ZERO, + seconds_behind: 35_000_000, + slashable_grt: 1_600_000, + versions_behind: 0, + zero_allocation: false, + }, + Candidate { + indexer: hex!("0000000000000000000000000000000000000001").into(), + deployment: hex!("0000000000000000000000000000000000000000000000000000000000000000") + .into(), + url: "https://example.com".parse().unwrap(), + perf: ExpectedPerformance { + success_rate: Normalized::new(0.99).unwrap(), + latency_ms: 10_000, + }, + fee: Normalized::ZERO, + seconds_behind: 120, + slashable_grt: 100_000, + versions_behind: 0, + zero_allocation: true, + }, + ]; + + println!( + "score {} {:?}", + candidates[0].indexer, + candidates[0].score(), + ); + println!( + "score {} {:?}", + candidates[1].indexer, + candidates[1].score(), + ); + assert!(candidates[0].score() <= candidates[1].score()); + + let selections: ArrayVec<&Candidate, 3> = crate::select(&candidates); + assert_eq!(1, selections.len(), "select exatly one candidate"); + assert_eq!( + Some(candidates[1].indexer), + selections.first().map(|s| s.indexer), + "select candidate closer to chain head", + ); +} + #[test] fn multi_selection_preference() { let candidates = [