mithril_aggregator/tools/
vacuum_tracker.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4};
5
6use anyhow::Context;
7use chrono::{DateTime, TimeDelta, Utc};
8use slog::{debug, info, Logger};
9
10use mithril_common::StdResult;
11
12const LAST_VACUUM_TIME_FILENAME: &str = "last_vacuum_time";
13
14type LastVacuumTime = DateTime<Utc>;
15
16/// Helper to track when vacuum was last performed
17#[derive(Debug, Clone)]
18pub struct VacuumTracker {
19    tracker_file: PathBuf,
20    min_interval: TimeDelta,
21    logger: Logger,
22}
23
24impl VacuumTracker {
25    /// Create a new [VacuumTracker] for the given store directory
26    pub fn new(store_dir: &Path, interval: TimeDelta, logger: Logger) -> Self {
27        let last_vacuum_file = store_dir.join(LAST_VACUUM_TIME_FILENAME);
28
29        Self {
30            tracker_file: last_vacuum_file,
31            min_interval: interval,
32            logger,
33        }
34    }
35
36    /// Check if enough time has passed since last vacuum (returning the last vacuum timestamp)
37    pub fn check_vacuum_needed(&self) -> StdResult<(bool, Option<LastVacuumTime>)> {
38        if !self.tracker_file.exists() {
39            debug!(
40                self.logger,
41                "No previous vacuum timestamp found, vacuum can be performed"
42            );
43            return Ok((true, None));
44        }
45
46        let last_vacuum = fs::read_to_string(&self.tracker_file).with_context(|| {
47            format!(
48                "Failed to read vacuum timestamp file: {:?}",
49                self.tracker_file
50            )
51        })?;
52        let last_vacuum = DateTime::parse_from_rfc3339(&last_vacuum)?.with_timezone(&Utc);
53
54        let duration_since_last = Utc::now() - (last_vacuum);
55
56        let should_vacuum = duration_since_last >= self.min_interval;
57        let info_message = if should_vacuum {
58            "Sufficient time has passed since last vacuum"
59        } else {
60            "Not enough time elapsed since last vacuum"
61        };
62
63        info!(
64            self.logger,
65            "{}", info_message;
66            "last_vacuum" => last_vacuum.to_string(),
67            "elapsed_days" => duration_since_last.num_days(),
68            "min_interval_days" => self.min_interval.num_days()
69        );
70
71        Ok((should_vacuum, Some(last_vacuum)))
72    }
73
74    /// Update the last vacuum time to now
75    pub fn update_last_vacuum_time(&self) -> StdResult<LastVacuumTime> {
76        let timestamp = Utc::now();
77
78        fs::write(&self.tracker_file, timestamp.to_rfc3339()).with_context(|| {
79            format!(
80                "Failed to write to last vacuum time file: {:?}",
81                self.tracker_file
82            )
83        })?;
84
85        Ok(timestamp)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use mithril_common::temp_dir_create;
92
93    use crate::test_tools::TestLogger;
94
95    use super::*;
96
97    const DUMMY_INTERVAL: TimeDelta = TimeDelta::milliseconds(99);
98
99    #[test]
100    fn update_last_vacuum_time_creates_file_with_current_timestamp() {
101        let tracker = VacuumTracker::new(&temp_dir_create!(), DUMMY_INTERVAL, TestLogger::stdout());
102
103        assert!(!tracker.tracker_file.exists());
104
105        let saved_timestamp = tracker.update_last_vacuum_time().unwrap();
106        let approximative_expected_saved_timestamp = Utc::now();
107
108        let vacuum_file_content = fs::read_to_string(tracker.tracker_file).unwrap();
109        let timestamp_retrieved = DateTime::parse_from_rfc3339(&vacuum_file_content).unwrap();
110        let diff = timestamp_retrieved
111            .signed_duration_since(approximative_expected_saved_timestamp)
112            .num_milliseconds();
113        assert!(diff < 1);
114        assert_eq!(timestamp_retrieved, saved_timestamp);
115    }
116
117    #[test]
118    fn update_last_vacuum_time_overwrites_previous_timestamp() {
119        let tracker = VacuumTracker::new(&temp_dir_create!(), DUMMY_INTERVAL, TestLogger::stdout());
120
121        let initial_saved_timestamp = tracker.update_last_vacuum_time().unwrap();
122        let last_saved_timestamp = tracker.update_last_vacuum_time().unwrap();
123
124        let vacuum_file_content = fs::read_to_string(tracker.tracker_file).unwrap();
125        let timestamp_retrieved = DateTime::parse_from_rfc3339(&vacuum_file_content).unwrap();
126        assert!(last_saved_timestamp > initial_saved_timestamp);
127        assert_eq!(timestamp_retrieved, last_saved_timestamp);
128    }
129
130    #[test]
131    fn update_last_vacuum_time_fails_on_write_error() {
132        let dir_not_exist = Path::new("path-does-not-exist");
133        let tracker = VacuumTracker::new(dir_not_exist, DUMMY_INTERVAL, TestLogger::stdout());
134
135        tracker
136            .update_last_vacuum_time()
137            .expect_err("Update last vacuum time should fail when error while writing to file");
138    }
139
140    #[test]
141    fn check_vacuum_needed_returns_true_when_no_previous_record() {
142        let tracker = VacuumTracker::new(&temp_dir_create!(), DUMMY_INTERVAL, TestLogger::stdout());
143
144        let (is_vacuum_needed, last_timestamp) = tracker.check_vacuum_needed().unwrap();
145
146        assert!(is_vacuum_needed);
147        assert!(last_timestamp.is_none());
148    }
149
150    #[test]
151    fn check_vacuum_needed_returns_true_after_interval_elapsed() {
152        let min_interval = TimeDelta::milliseconds(10);
153        let tracker = VacuumTracker::new(&temp_dir_create!(), min_interval, TestLogger::stdout());
154
155        let saved_timestamp = Utc::now() - TimeDelta::milliseconds(10);
156        fs::write(tracker.clone().tracker_file, saved_timestamp.to_rfc3339()).unwrap();
157
158        let (is_vacuum_needed, last_timestamp) = tracker.check_vacuum_needed().unwrap();
159
160        assert!(is_vacuum_needed);
161        assert_eq!(last_timestamp, Some(saved_timestamp));
162    }
163
164    #[test]
165    fn check_vacuum_needed_returns_false_within_interval() {
166        let min_interval = TimeDelta::minutes(2);
167        let tracker = VacuumTracker::new(&temp_dir_create!(), min_interval, TestLogger::stdout());
168
169        let saved_timestamp = tracker.update_last_vacuum_time().unwrap();
170
171        let (is_vacuum_needed, last_timestamp) = tracker.check_vacuum_needed().unwrap();
172
173        assert!(!is_vacuum_needed);
174        assert_eq!(last_timestamp, Some(saved_timestamp));
175    }
176}