rpytest_daemon/
scheduler.rs1use crate::models::ScheduledTest;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct TestScheduler {
14 pub duration_history: HashMap<String, Vec<u64>>,
16 pub default_duration_ms: u64,
18}
19
20impl TestScheduler {
21 pub fn new() -> Self {
23 TestScheduler {
24 duration_history: HashMap::new(),
25 default_duration_ms: 1000, }
27 }
28
29 pub fn update_duration(&mut self, node_id: &str, duration_ms: u64) {
31 let history = self
32 .duration_history
33 .entry(node_id.to_string())
34 .or_default();
35 history.push(duration_ms);
36
37 if history.len() > 10 {
39 *history = history[history.len() - 10..].to_vec();
40 }
41 }
42
43 pub fn get_estimated_duration(&self, node_id: &str) -> u64 {
45 if let Some(durations) = self.duration_history.get(node_id) {
46 if durations.is_empty() {
47 return self.default_duration_ms;
48 }
49
50 if durations.len() == 1 {
51 return durations[0];
52 }
53
54 let len = durations.len();
56 let mut weighted_sum: f64 = 0.0;
57 let mut total_weight: f64 = 0.0;
58
59 for (i, &duration) in durations.iter().enumerate() {
60 let weight = 0.5_f64.powi((len - 1 - i) as i32);
62 weighted_sum += duration as f64 * weight;
63 total_weight += weight;
64 }
65
66 (weighted_sum / total_weight) as u64
67 } else {
68 self.default_duration_ms
69 }
70 }
71
72 pub fn schedule(
82 &self,
83 node_ids: &[String],
84 failed_first: bool,
85 recent_failures: &[String],
86 ) -> Vec<String> {
87 if node_ids.is_empty() {
88 return Vec::new();
89 }
90
91 let recent_failures_set: std::collections::HashSet<&str> =
92 recent_failures.iter().map(|s| s.as_str()).collect();
93
94 let mut scheduled: Vec<ScheduledTest> = Vec::with_capacity(node_ids.len());
96
97 for node_id in node_ids {
98 let est_duration = self.get_estimated_duration(node_id);
99
100 let mut priority = est_duration;
102
103 if failed_first && recent_failures_set.contains(node_id.as_str()) {
104 priority += 1_000_000;
106 }
107
108 scheduled.push(ScheduledTest {
109 node_id: node_id.clone(),
110 estimated_duration_ms: est_duration,
111 priority,
112 });
113 }
114
115 scheduled.sort_by(|a, b| b.priority.cmp(&a.priority));
117
118 scheduled.into_iter().map(|s| s.node_id).collect()
119 }
120
121 pub fn split_balanced(&self, node_ids: &[String], worker_count: usize) -> Vec<Vec<String>> {
129 if worker_count <= 1 || node_ids.is_empty() {
130 return vec![node_ids.to_vec()];
131 }
132
133 let mut sorted: Vec<_> = node_ids
135 .iter()
136 .map(|id| (id.clone(), self.get_estimated_duration(id)))
137 .collect();
138 sorted.sort_by(|a, b| b.1.cmp(&a.1));
139
140 let mut batches: Vec<Vec<String>> = vec![Vec::new(); worker_count];
142 let mut batch_durations = vec![0u64; worker_count];
143
144 for (test, duration) in sorted {
145 let min_idx = batch_durations
147 .iter()
148 .enumerate()
149 .min_by_key(|(_, d)| *d)
150 .map(|(i, _)| i)
151 .unwrap_or(0);
152
153 batches[min_idx].push(test);
154 batch_durations[min_idx] += duration;
155 }
156
157 batches.into_iter().filter(|b| !b.is_empty()).collect()
159 }
160
161 pub fn clear_history(&mut self) {
163 self.duration_history.clear();
164 }
165
166 pub fn get_history(&self, node_id: &str) -> Option<&Vec<u64>> {
168 self.duration_history.get(node_id)
169 }
170
171 pub fn tracked_count(&self) -> usize {
173 self.duration_history.len()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_schedule_ordering() {
183 let scheduler = TestScheduler::new();
184 let node_ids = vec![
185 "test_a".to_string(),
186 "test_b".to_string(),
187 "test_c".to_string(),
188 ];
189
190 let result = scheduler.schedule(&node_ids, false, &[]);
192 assert_eq!(result, node_ids);
193 }
194
195 #[test]
196 fn test_failed_first_priority() {
197 let scheduler = TestScheduler::new();
198 let node_ids = vec![
199 "test_a".to_string(),
200 "test_b".to_string(),
201 "test_c".to_string(),
202 ];
203 let recent_failures = vec!["test_b".to_string()];
204
205 let result = scheduler.schedule(&node_ids, true, &recent_failures);
206 assert_eq!(result[0], "test_b");
208 }
209
210 #[test]
211 fn test_duration_update() {
212 let mut scheduler = TestScheduler::new();
213 scheduler.update_duration("test_a", 100);
214 scheduler.update_duration("test_a", 200);
215
216 let duration = scheduler.get_estimated_duration("test_a");
217 assert!(duration >= 150 && duration <= 200);
219 }
220
221 #[test]
222 fn test_split_balanced_empty() {
223 let scheduler = TestScheduler::new();
224 let result = scheduler.split_balanced(&[], 4);
225 assert_eq!(result.len(), 1);
226 assert!(result[0].is_empty());
227 }
228
229 #[test]
230 fn test_split_balanced_single_worker() {
231 let scheduler = TestScheduler::new();
232 let node_ids = vec!["a".to_string(), "b".to_string(), "c".to_string()];
233 let result = scheduler.split_balanced(&node_ids, 1);
234 assert_eq!(result.len(), 1);
235 assert_eq!(result[0].len(), 3);
236 }
237
238 #[test]
239 fn test_split_balanced_even_distribution() {
240 let mut scheduler = TestScheduler::new();
241 scheduler.update_duration("slow", 1000);
243 scheduler.update_duration("medium1", 500);
244 scheduler.update_duration("medium2", 500);
245 scheduler.update_duration("fast1", 100);
246 scheduler.update_duration("fast2", 100);
247
248 let node_ids = vec![
249 "slow".to_string(),
250 "medium1".to_string(),
251 "medium2".to_string(),
252 "fast1".to_string(),
253 "fast2".to_string(),
254 ];
255
256 let batches = scheduler.split_balanced(&node_ids, 2);
257 assert_eq!(batches.len(), 2);
258
259 let batch1_duration: u64 = batches[0]
261 .iter()
262 .map(|id| scheduler.get_estimated_duration(id))
263 .sum();
264 let batch2_duration: u64 = batches[1]
265 .iter()
266 .map(|id| scheduler.get_estimated_duration(id))
267 .sum();
268
269 let max_dur = batch1_duration.max(batch2_duration);
271 let min_dur = batch1_duration.min(batch2_duration);
272 assert!(
273 max_dur <= min_dur * 2,
274 "Batches not balanced: {} vs {}",
275 batch1_duration,
276 batch2_duration
277 );
278 }
279}