mithril_aggregator/tools/
vacuum_tracker.rs1use 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#[derive(Debug, Clone)]
18pub struct VacuumTracker {
19 tracker_file: PathBuf,
20 min_interval: TimeDelta,
21 logger: Logger,
22}
23
24impl VacuumTracker {
25 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 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 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}