spider_browser/retry/
browser_selector.rs1use super::failure_tracker::{FailureTracker, ROTATE_AFTER_FAILURES};
9
10pub const PRIMARY_ROTATION: &[&str] = &["chrome-h", "chrome-new"];
15
16pub const EXTENDED_ROTATION: &[&str] = &["firefox", "lightpanda", "servo"];
18
19pub const BROWSER_ROTATION: &[&str] = &[
21 "chrome-h",
22 "chrome-new",
23 "firefox",
24 "lightpanda",
25 "servo",
26];
27
28pub struct BrowserSelector {
35 tracker: FailureTracker,
36}
37
38impl BrowserSelector {
39 pub fn new(tracker: FailureTracker) -> Self {
41 Self { tracker }
42 }
43
44 pub fn failure_tracker(&self) -> &FailureTracker {
46 &self.tracker
47 }
48
49 pub fn should_rotate(&self, domain: &str, current_browser: &str) -> bool {
54 self.tracker.failure_count(domain, current_browser) >= ROTATE_AFTER_FAILURES
55 }
56
57 pub fn next_browser(&self, domain: &str, current_browser: &str) -> Option<&'static str> {
65 let current_idx = BROWSER_ROTATION
66 .iter()
67 .position(|&b| b == current_browser)
68 .unwrap_or(0);
69
70 for offset in 1..BROWSER_ROTATION.len() {
71 let idx = (current_idx + offset) % BROWSER_ROTATION.len();
72 let candidate = BROWSER_ROTATION[idx];
73 if self.tracker.failure_count(domain, candidate) < ROTATE_AFTER_FAILURES {
74 return Some(candidate);
75 }
76 }
77
78 None
79 }
80
81 pub fn choose_browser<'a>(&self, domain: &str, fallback: &'a str) -> &'a str
87 where
88 'static: 'a,
89 {
90 for &browser in BROWSER_ROTATION {
91 if self.tracker.failure_count(domain, browser) < ROTATE_AFTER_FAILURES {
92 return browser;
93 }
94 }
95 fallback
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn make_selector() -> BrowserSelector {
104 BrowserSelector::new(FailureTracker::new())
105 }
106
107 #[test]
108 fn rotation_constants_are_consistent() {
109 let mut expected: Vec<&str> = PRIMARY_ROTATION.to_vec();
111 expected.extend_from_slice(EXTENDED_ROTATION);
112 assert_eq!(BROWSER_ROTATION, expected.as_slice());
113 }
114
115 #[test]
116 fn should_rotate_after_threshold() {
117 let sel = make_selector();
118 assert!(!sel.should_rotate("example.com", "chrome-h"));
119
120 sel.failure_tracker().record_failure("example.com", "chrome-h");
121 assert!(!sel.should_rotate("example.com", "chrome-h"));
122
123 sel.failure_tracker().record_failure("example.com", "chrome-h");
124 assert!(sel.should_rotate("example.com", "chrome-h"));
125 }
126
127 #[test]
128 fn next_browser_skips_exhausted() {
129 let sel = make_selector();
130
131 sel.failure_tracker().record_failure("d.com", "chrome-h");
133 sel.failure_tracker().record_failure("d.com", "chrome-h");
134
135 assert_eq!(sel.next_browser("d.com", "chrome-h"), Some("chrome-new"));
137
138 sel.failure_tracker().record_failure("d.com", "chrome-new");
140 sel.failure_tracker().record_failure("d.com", "chrome-new");
141
142 assert_eq!(sel.next_browser("d.com", "chrome-h"), Some("firefox"));
143 }
144
145 #[test]
146 fn next_browser_returns_none_when_all_exhausted() {
147 let sel = make_selector();
148 for &browser in BROWSER_ROTATION {
149 sel.failure_tracker().record_failure("d.com", browser);
150 sel.failure_tracker().record_failure("d.com", browser);
151 }
152 assert_eq!(sel.next_browser("d.com", "chrome-h"), None);
153 }
154
155 #[test]
156 fn choose_browser_picks_first_available() {
157 let sel = make_selector();
158
159 assert_eq!(sel.choose_browser("d.com", "fallback"), "chrome-h");
161
162 sel.failure_tracker().record_failure("d.com", "chrome-h");
164 sel.failure_tracker().record_failure("d.com", "chrome-h");
165 assert_eq!(sel.choose_browser("d.com", "fallback"), "chrome-new");
166 }
167
168 #[test]
169 fn choose_browser_falls_back_when_all_exhausted() {
170 let sel = make_selector();
171 for &browser in BROWSER_ROTATION {
172 sel.failure_tracker().record_failure("d.com", browser);
173 sel.failure_tracker().record_failure("d.com", browser);
174 }
175 assert_eq!(sel.choose_browser("d.com", "fallback"), "fallback");
176 }
177
178 #[test]
179 fn next_browser_wraps_around() {
180 let sel = make_selector();
181 let last = *BROWSER_ROTATION.last().unwrap();
183 assert_eq!(sel.next_browser("d.com", last), Some("chrome-h"));
184 }
185}