ipfrs_storage/
compaction.rs1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::Arc;
20use std::time::{Duration, SystemTime, UNIX_EPOCH};
21
22#[derive(Debug, Clone)]
28pub struct CompactionConfig {
29 pub idle_threshold: Duration,
33
34 pub min_interval: Duration,
38
39 pub max_bytes_since_compact: u64,
44}
45
46impl Default for CompactionConfig {
47 fn default() -> Self {
48 Self {
49 idle_threshold: Duration::from_secs(5 * 60),
50 min_interval: Duration::from_secs(30 * 60),
51 max_bytes_since_compact: 100 * 1024 * 1024,
52 }
53 }
54}
55
56pub struct CompactionScheduler {
65 config: CompactionConfig,
66 last_operation_ms: AtomicU64,
68 last_compaction_ms: AtomicU64,
70 bytes_since_compaction: AtomicU64,
72 compaction_count: AtomicU64,
74 is_compacting: AtomicBool,
76}
77
78impl std::fmt::Debug for CompactionScheduler {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("CompactionScheduler")
81 .field("config", &self.config)
82 .field(
83 "last_operation_ms",
84 &self.last_operation_ms.load(Ordering::Relaxed),
85 )
86 .field(
87 "last_compaction_ms",
88 &self.last_compaction_ms.load(Ordering::Relaxed),
89 )
90 .field(
91 "bytes_since_compaction",
92 &self.bytes_since_compaction.load(Ordering::Relaxed),
93 )
94 .field(
95 "compaction_count",
96 &self.compaction_count.load(Ordering::Relaxed),
97 )
98 .field("is_compacting", &self.is_compacting.load(Ordering::Relaxed))
99 .finish()
100 }
101}
102
103fn now_ms() -> u64 {
105 SystemTime::now()
106 .duration_since(UNIX_EPOCH)
107 .unwrap_or(Duration::ZERO)
108 .as_millis() as u64
109}
110
111impl CompactionScheduler {
112 pub fn new(config: CompactionConfig) -> Arc<Self> {
114 let now = now_ms();
115 Arc::new(Self {
116 config,
117 last_operation_ms: AtomicU64::new(now),
120 last_compaction_ms: AtomicU64::new(now),
121 bytes_since_compaction: AtomicU64::new(0),
122 compaction_count: AtomicU64::new(0),
123 is_compacting: AtomicBool::new(false),
124 })
125 }
126
127 pub fn record_write(&self, bytes: usize) {
132 self.last_operation_ms.store(now_ms(), Ordering::Relaxed);
133 self.bytes_since_compaction
134 .fetch_add(bytes as u64, Ordering::Relaxed);
135 }
136
137 pub fn should_compact(&self) -> bool {
145 if self.is_compacting.load(Ordering::Acquire) {
146 return false;
147 }
148
149 let bytes = self.bytes_since_compaction.load(Ordering::Relaxed);
150 if bytes >= self.config.max_bytes_since_compact {
151 return true;
152 }
153
154 self.idle_duration() >= self.config.idle_threshold
155 && self.time_since_last_compaction() >= self.config.min_interval
156 }
157
158 pub fn mark_compaction_started(&self) -> bool {
165 self.is_compacting
166 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
167 .is_ok()
168 }
169
170 pub fn mark_compaction_done(&self) {
175 self.bytes_since_compaction.store(0, Ordering::Relaxed);
176 self.last_compaction_ms.store(now_ms(), Ordering::Relaxed);
177 self.compaction_count.fetch_add(1, Ordering::Relaxed);
178 self.is_compacting.store(false, Ordering::Release);
179 }
180
181 pub fn compaction_count(&self) -> u64 {
183 self.compaction_count.load(Ordering::Relaxed)
184 }
185
186 pub fn bytes_since_last_compaction(&self) -> u64 {
188 self.bytes_since_compaction.load(Ordering::Relaxed)
189 }
190
191 pub fn idle_duration(&self) -> Duration {
193 let last_ms = self.last_operation_ms.load(Ordering::Relaxed);
194 let now = now_ms();
195 Duration::from_millis(now.saturating_sub(last_ms))
196 }
197
198 pub fn time_since_last_compaction(&self) -> Duration {
200 let last_ms = self.last_compaction_ms.load(Ordering::Relaxed);
201 let now = now_ms();
202 Duration::from_millis(now.saturating_sub(last_ms))
203 }
204}
205
206#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::time::Duration;
214
215 fn make_scheduler_with_config(
216 idle_threshold_secs: u64,
217 min_interval_secs: u64,
218 max_bytes: u64,
219 ) -> Arc<CompactionScheduler> {
220 CompactionScheduler::new(CompactionConfig {
221 idle_threshold: Duration::from_secs(idle_threshold_secs),
222 min_interval: Duration::from_secs(min_interval_secs),
223 max_bytes_since_compact: max_bytes,
224 })
225 }
226
227 #[test]
228 fn test_default_config() {
229 let cfg = CompactionConfig::default();
230 assert_eq!(cfg.idle_threshold, Duration::from_secs(5 * 60));
231 assert_eq!(cfg.min_interval, Duration::from_secs(30 * 60));
232 assert_eq!(cfg.max_bytes_since_compact, 100 * 1024 * 1024);
233 }
234
235 #[test]
236 fn test_record_write_updates_bytes() {
237 let sched = make_scheduler_with_config(300, 1800, 100 * 1024 * 1024);
238 assert_eq!(sched.bytes_since_last_compaction(), 0);
239
240 sched.record_write(1024);
241 assert_eq!(sched.bytes_since_last_compaction(), 1024);
242
243 sched.record_write(512);
244 assert_eq!(sched.bytes_since_last_compaction(), 1536);
245 }
246
247 #[test]
248 fn test_should_compact_by_bytes() {
249 let sched = make_scheduler_with_config(300, 1800, 100);
251
252 sched.record_write(101);
254 assert!(
255 sched.should_compact(),
256 "should compact once bytes threshold is exceeded"
257 );
258 }
259
260 #[test]
261 fn test_should_compact_needs_min_interval() {
262 let sched = make_scheduler_with_config(0, 86400, 100 * 1024 * 1024);
264
265 assert!(
267 !sched.should_compact(),
268 "should NOT compact when min_interval has not elapsed"
269 );
270 }
271
272 #[test]
273 fn test_mark_compaction_lifecycle() {
274 let sched = make_scheduler_with_config(300, 1800, 100);
275
276 sched.record_write(200);
278 assert!(sched.should_compact());
279
280 let won = sched.mark_compaction_started();
282 assert!(won, "first caller must win the CAS");
283
284 assert!(
286 !sched.should_compact(),
287 "in-flight guard must block re-entry"
288 );
289
290 sched.mark_compaction_done();
292 assert_eq!(
293 sched.bytes_since_last_compaction(),
294 0,
295 "bytes counter must be reset after compaction"
296 );
297 assert_eq!(sched.compaction_count(), 1);
298 assert!(!sched.is_compacting.load(Ordering::Relaxed));
300 }
301
302 #[test]
303 fn test_concurrent_compaction_prevention() {
304 let sched = make_scheduler_with_config(300, 1800, 100 * 1024 * 1024);
305
306 let first = sched.mark_compaction_started();
308 assert!(first, "first caller must win");
309
310 let second = sched.mark_compaction_started();
312 assert!(
313 !second,
314 "second caller must be rejected while compaction is in-flight"
315 );
316
317 sched.mark_compaction_done();
319 }
320
321 #[test]
322 fn test_compaction_count_increments() {
323 let sched = make_scheduler_with_config(300, 1800, 100 * 1024 * 1024);
324
325 assert_eq!(sched.compaction_count(), 0);
326
327 for expected in 1..=5u64 {
328 let won = sched.mark_compaction_started();
329 assert!(won);
330 sched.mark_compaction_done();
331 assert_eq!(sched.compaction_count(), expected);
332 }
333 }
334}