Skip to main content

gosh_dl/
scheduler.rs

1//! Bandwidth Scheduling
2//!
3//! Provides time-based bandwidth limit scheduling. Allows setting different
4//! download/upload limits for different times of day or days of the week.
5
6use chrono::{Datelike, Local, Timelike};
7use parking_lot::RwLock;
8
9// Re-export protocol types for backward compatibility
10pub use crate::protocol::{BandwidthLimits, ScheduleRule};
11
12/// Bandwidth scheduler that manages time-based limits
13pub struct BandwidthScheduler {
14    /// Schedule rules (evaluated in order, first match wins)
15    rules: Vec<ScheduleRule>,
16    /// Default limits when no rule matches
17    default_limits: BandwidthLimits,
18    /// Current active limits (cached)
19    current_limits: RwLock<BandwidthLimits>,
20}
21
22impl BandwidthScheduler {
23    /// Create a new scheduler with the given rules and default limits
24    pub fn new(rules: Vec<ScheduleRule>, default_limits: BandwidthLimits) -> Self {
25        let current = Self::evaluate_rules(&rules, &default_limits);
26        Self {
27            rules,
28            default_limits,
29            current_limits: RwLock::new(current),
30        }
31    }
32
33    /// Create a scheduler with no rules (always use default limits)
34    pub fn with_defaults(download: Option<u64>, upload: Option<u64>) -> Self {
35        Self::new(Vec::new(), BandwidthLimits { download, upload })
36    }
37
38    /// Get the current bandwidth limits
39    pub fn get_limits(&self) -> BandwidthLimits {
40        *self.current_limits.read()
41    }
42
43    /// Update limits based on current time
44    ///
45    /// This should be called periodically (e.g., every minute) to update limits
46    /// when time-based rules are in effect.
47    pub fn update(&self) -> bool {
48        let new_limits = Self::evaluate_rules(&self.rules, &self.default_limits);
49        let mut current = self.current_limits.write();
50        if *current != new_limits {
51            tracing::info!(
52                "Bandwidth limits changed: download={:?} upload={:?}",
53                new_limits.download,
54                new_limits.upload
55            );
56            *current = new_limits;
57            true
58        } else {
59            false
60        }
61    }
62
63    /// Evaluate rules for the current time
64    fn evaluate_rules(rules: &[ScheduleRule], default: &BandwidthLimits) -> BandwidthLimits {
65        let now = Local::now();
66        let hour = now.hour() as u8;
67        let weekday = now.weekday();
68
69        for rule in rules {
70            if rule.matches(hour, weekday) {
71                return BandwidthLimits {
72                    download: rule.download_limit,
73                    upload: rule.upload_limit,
74                };
75            }
76        }
77
78        *default
79    }
80
81    /// Get the rules
82    pub fn rules(&self) -> &[ScheduleRule] {
83        &self.rules
84    }
85
86    /// Add a new rule (appended to end of list)
87    pub fn add_rule(&mut self, rule: ScheduleRule) {
88        self.rules.push(rule);
89        self.update();
90    }
91
92    /// Clear all rules
93    pub fn clear_rules(&mut self) {
94        self.rules.clear();
95        self.update();
96    }
97
98    /// Set new rules (replaces existing)
99    pub fn set_rules(&mut self, rules: Vec<ScheduleRule>) {
100        self.rules = rules;
101        self.update();
102    }
103
104    /// Set default limits
105    pub fn set_defaults(&mut self, limits: BandwidthLimits) {
106        self.default_limits = limits;
107        self.update();
108    }
109}
110
111impl Default for BandwidthScheduler {
112    fn default() -> Self {
113        Self::with_defaults(None, None)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use chrono::Weekday;
121
122    #[test]
123    fn test_schedule_rule_match_simple() {
124        let rule = ScheduleRule::all_days(9, 17, Some(1_000_000), None);
125
126        // During work hours
127        assert!(rule.matches(9, Weekday::Mon));
128        assert!(rule.matches(12, Weekday::Wed));
129        assert!(rule.matches(17, Weekday::Fri));
130
131        // Outside work hours
132        assert!(!rule.matches(8, Weekday::Mon));
133        assert!(!rule.matches(18, Weekday::Wed));
134        assert!(!rule.matches(0, Weekday::Fri));
135    }
136
137    #[test]
138    fn test_schedule_rule_match_overnight() {
139        // Rule: 22:00 to 06:00 (overnight)
140        let rule = ScheduleRule::all_days(22, 6, Some(10_000_000), None);
141
142        // During overnight hours
143        assert!(rule.matches(22, Weekday::Mon));
144        assert!(rule.matches(23, Weekday::Mon));
145        assert!(rule.matches(0, Weekday::Tue));
146        assert!(rule.matches(3, Weekday::Tue));
147        assert!(rule.matches(6, Weekday::Tue));
148
149        // Outside overnight hours
150        assert!(!rule.matches(7, Weekday::Mon));
151        assert!(!rule.matches(12, Weekday::Mon));
152        assert!(!rule.matches(21, Weekday::Mon));
153    }
154
155    #[test]
156    fn test_schedule_rule_weekdays() {
157        let rule = ScheduleRule::weekdays(9, 17, Some(500_000), None);
158
159        // Weekdays
160        assert!(rule.matches(12, Weekday::Mon));
161        assert!(rule.matches(12, Weekday::Tue));
162        assert!(rule.matches(12, Weekday::Wed));
163        assert!(rule.matches(12, Weekday::Thu));
164        assert!(rule.matches(12, Weekday::Fri));
165
166        // Weekends
167        assert!(!rule.matches(12, Weekday::Sat));
168        assert!(!rule.matches(12, Weekday::Sun));
169    }
170
171    #[test]
172    fn test_schedule_rule_weekends() {
173        let rule = ScheduleRule::weekends(0, 23, None, None);
174
175        // Weekends
176        assert!(rule.matches(12, Weekday::Sat));
177        assert!(rule.matches(12, Weekday::Sun));
178
179        // Weekdays
180        assert!(!rule.matches(12, Weekday::Mon));
181        assert!(!rule.matches(12, Weekday::Fri));
182    }
183
184    #[test]
185    fn test_scheduler_no_rules() {
186        let scheduler = BandwidthScheduler::with_defaults(Some(1_000_000), Some(500_000));
187        let limits = scheduler.get_limits();
188
189        assert_eq!(limits.download, Some(1_000_000));
190        assert_eq!(limits.upload, Some(500_000));
191    }
192
193    #[test]
194    fn test_scheduler_first_match_wins() {
195        // Rule 1: 0-23 all days, 1MB/s
196        // Rule 2: 0-23 all days, 2MB/s
197        // First rule should win
198        let rules = vec![
199            ScheduleRule::all_days(0, 23, Some(1_000_000), None),
200            ScheduleRule::all_days(0, 23, Some(2_000_000), None),
201        ];
202        let scheduler = BandwidthScheduler::new(rules, BandwidthLimits::default());
203        let limits = scheduler.get_limits();
204
205        assert_eq!(limits.download, Some(1_000_000));
206    }
207
208    #[test]
209    fn test_bandwidth_limits_default() {
210        let limits = BandwidthLimits::default();
211        assert_eq!(limits.download, None);
212        assert_eq!(limits.upload, None);
213    }
214}