reth_prune/
limiter.rs

1use reth_prune_types::{PruneInterruptReason, PruneProgress};
2use std::{
3    num::NonZeroUsize,
4    time::{Duration, Instant},
5};
6
7/// Limits a pruner run by either the number of entries (rows in the database) that can be deleted
8/// or the time it can run.
9#[derive(Debug, Clone, Default)]
10pub struct PruneLimiter {
11    /// Maximum entries (rows in the database) to delete from the database per run.
12    deleted_entries_limit: Option<PruneDeletedEntriesLimit>,
13    /// Maximum duration of one prune run.
14    time_limit: Option<PruneTimeLimit>,
15}
16
17#[derive(Debug, Clone)]
18struct PruneDeletedEntriesLimit {
19    /// Maximum entries (rows in the database) to delete from the database.
20    limit: usize,
21    /// Current number of entries (rows in the database) that have been deleted.
22    deleted: usize,
23}
24
25impl PruneDeletedEntriesLimit {
26    const fn new(limit: usize) -> Self {
27        Self { limit, deleted: 0 }
28    }
29
30    const fn is_limit_reached(&self) -> bool {
31        self.deleted >= self.limit
32    }
33}
34
35#[derive(Debug, Clone)]
36struct PruneTimeLimit {
37    /// Maximum duration of one prune run.
38    limit: Duration,
39    /// Time when the prune run has started.
40    start: Instant,
41}
42
43impl PruneTimeLimit {
44    fn new(limit: Duration) -> Self {
45        Self { limit, start: Instant::now() }
46    }
47
48    fn is_limit_reached(&self) -> bool {
49        self.start.elapsed() > self.limit
50    }
51}
52
53impl PruneLimiter {
54    /// Sets the limit on the number of deleted entries (rows in the database).
55    /// If the limit was already set, it will be overwritten.
56    pub const fn set_deleted_entries_limit(mut self, limit: usize) -> Self {
57        if let Some(deleted_entries_limit) = self.deleted_entries_limit.as_mut() {
58            deleted_entries_limit.limit = limit;
59        } else {
60            self.deleted_entries_limit = Some(PruneDeletedEntriesLimit::new(limit));
61        }
62
63        self
64    }
65
66    /// Sets the limit on the number of deleted entries (rows in the database) to a biggest
67    /// multiple of the given denominator that is smaller than the existing limit.
68    ///
69    /// If the limit wasn't set, does nothing.
70    pub fn floor_deleted_entries_limit_to_multiple_of(mut self, denominator: NonZeroUsize) -> Self {
71        if let Some(deleted_entries_limit) = self.deleted_entries_limit.as_mut() {
72            deleted_entries_limit.limit =
73                (deleted_entries_limit.limit / denominator) * denominator.get();
74        }
75
76        self
77    }
78
79    /// Returns `true` if the limit on the number of deleted entries (rows in the database) is
80    /// reached.
81    pub fn is_deleted_entries_limit_reached(&self) -> bool {
82        self.deleted_entries_limit.as_ref().is_some_and(|limit| limit.is_limit_reached())
83    }
84
85    /// Increments the number of deleted entries by the given number.
86    pub const fn increment_deleted_entries_count_by(&mut self, entries: usize) {
87        if let Some(limit) = self.deleted_entries_limit.as_mut() {
88            limit.deleted += entries;
89        }
90    }
91
92    /// Increments the number of deleted entries by one.
93    pub const fn increment_deleted_entries_count(&mut self) {
94        self.increment_deleted_entries_count_by(1)
95    }
96
97    /// Returns the number of deleted entries left before the limit is reached.
98    pub fn deleted_entries_limit_left(&self) -> Option<usize> {
99        self.deleted_entries_limit.as_ref().map(|limit| limit.limit - limit.deleted)
100    }
101
102    /// Returns the limit on the number of deleted entries (rows in the database).
103    pub fn deleted_entries_limit(&self) -> Option<usize> {
104        self.deleted_entries_limit.as_ref().map(|limit| limit.limit)
105    }
106
107    /// Sets the time limit.
108    pub fn set_time_limit(mut self, limit: Duration) -> Self {
109        self.time_limit = Some(PruneTimeLimit::new(limit));
110
111        self
112    }
113
114    /// Returns `true` if time limit is reached.
115    pub fn is_time_limit_reached(&self) -> bool {
116        self.time_limit.as_ref().is_some_and(|limit| limit.is_limit_reached())
117    }
118
119    /// Returns `true` if any limit is reached.
120    pub fn is_limit_reached(&self) -> bool {
121        self.is_deleted_entries_limit_reached() || self.is_time_limit_reached()
122    }
123
124    /// Creates new [`PruneInterruptReason`] based on the limiter's state.
125    pub fn interrupt_reason(&self) -> PruneInterruptReason {
126        if self.is_time_limit_reached() {
127            PruneInterruptReason::Timeout
128        } else if self.is_deleted_entries_limit_reached() {
129            PruneInterruptReason::DeletedEntriesLimitReached
130        } else {
131            PruneInterruptReason::Unknown
132        }
133    }
134
135    /// Creates new [`PruneProgress`].
136    ///
137    /// If `done == true`, returns [`PruneProgress::Finished`], otherwise
138    /// [`PruneProgress::HasMoreData`] is returned with [`PruneInterruptReason`] according to the
139    /// limiter's state.
140    pub fn progress(&self, done: bool) -> PruneProgress {
141        if done {
142            PruneProgress::Finished
143        } else {
144            PruneProgress::HasMoreData(self.interrupt_reason())
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::thread::sleep;
153
154    #[test]
155    fn test_prune_deleted_entries_limit_initial_state() {
156        let limit_tracker = PruneDeletedEntriesLimit::new(10);
157        // Limit should be set properly
158        assert_eq!(limit_tracker.limit, 10);
159        // No entries should be deleted
160        assert_eq!(limit_tracker.deleted, 0);
161        assert!(!limit_tracker.is_limit_reached());
162    }
163
164    #[test]
165    fn test_prune_deleted_entries_limit_is_limit_reached() {
166        // Test when the deleted entries are less than the limit
167        let mut limit_tracker = PruneDeletedEntriesLimit::new(5);
168        limit_tracker.deleted = 3;
169        assert!(!limit_tracker.is_limit_reached());
170
171        // Test when the deleted entries are equal to the limit
172        limit_tracker.deleted = 5;
173        assert!(limit_tracker.is_limit_reached());
174
175        // Test when the deleted entries exceed the limit
176        limit_tracker.deleted = 6;
177        assert!(limit_tracker.is_limit_reached());
178    }
179
180    #[test]
181    fn test_prune_time_limit_initial_state() {
182        let time_limit = PruneTimeLimit::new(Duration::from_secs(10));
183        // The limit should be set correctly
184        assert_eq!(time_limit.limit, Duration::from_secs(10));
185        // The elapsed time should be very small right after creation
186        assert!(time_limit.start.elapsed() < Duration::from_secs(1));
187        // Limit should not be reached initially
188        assert!(!time_limit.is_limit_reached());
189    }
190
191    #[test]
192    fn test_prune_time_limit_is_limit_reached() {
193        let time_limit = PruneTimeLimit::new(Duration::from_millis(50));
194
195        // Simulate waiting for some time (less than the limit)
196        std::thread::sleep(Duration::from_millis(30));
197        assert!(!time_limit.is_limit_reached());
198
199        // Simulate waiting for time greater than the limit
200        std::thread::sleep(Duration::from_millis(30));
201        assert!(time_limit.is_limit_reached());
202    }
203
204    #[test]
205    fn test_set_deleted_entries_limit_initial_state() {
206        let pruner = PruneLimiter::default().set_deleted_entries_limit(100);
207        // The deleted_entries_limit should be set with the correct limit
208        assert!(pruner.deleted_entries_limit.is_some());
209        let deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
210        assert_eq!(deleted_entries_limit.limit, 100);
211        // The deleted count should be initially zero
212        assert_eq!(deleted_entries_limit.deleted, 0);
213        // The limit should not be reached initially
214        assert!(!deleted_entries_limit.is_limit_reached());
215    }
216
217    #[test]
218    fn test_set_deleted_entries_limit_overwrite_existing() {
219        let mut pruner = PruneLimiter::default().set_deleted_entries_limit(50);
220        // Overwrite the existing limit
221        pruner = pruner.set_deleted_entries_limit(200);
222
223        assert!(pruner.deleted_entries_limit.is_some());
224        let deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
225        // Check that the limit has been overwritten correctly
226        assert_eq!(deleted_entries_limit.limit, 200);
227        // Deleted count should still be zero
228        assert_eq!(deleted_entries_limit.deleted, 0);
229        assert!(!deleted_entries_limit.is_limit_reached());
230    }
231
232    #[test]
233    fn test_set_deleted_entries_limit_when_limit_is_reached() {
234        let mut pruner = PruneLimiter::default().set_deleted_entries_limit(5);
235        assert!(pruner.deleted_entries_limit.is_some());
236        let mut deleted_entries_limit = pruner.deleted_entries_limit.clone().unwrap();
237
238        // Simulate deletion of entries
239        deleted_entries_limit.deleted = 5;
240        assert!(deleted_entries_limit.is_limit_reached());
241
242        // Overwrite the limit and check if it resets correctly
243        pruner = pruner.set_deleted_entries_limit(10);
244        deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
245        assert_eq!(deleted_entries_limit.limit, 10);
246        // Deletion count should reset
247        assert_eq!(deleted_entries_limit.deleted, 0);
248        assert!(!deleted_entries_limit.is_limit_reached());
249    }
250
251    #[test]
252    fn test_floor_deleted_entries_limit_to_multiple_of() {
253        let limiter = PruneLimiter::default().set_deleted_entries_limit(15);
254        let denominator = NonZeroUsize::new(4).unwrap();
255
256        // Floor limit to the largest multiple of 4 less than or equal to 15 (that is 12)
257        let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
258        assert_eq!(updated_limiter.deleted_entries_limit.unwrap().limit, 12);
259
260        // Test when the limit is already a multiple of the denominator
261        let limiter = PruneLimiter::default().set_deleted_entries_limit(16);
262        let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
263        assert_eq!(updated_limiter.deleted_entries_limit.unwrap().limit, 16);
264
265        // Test when there's no limit set (should not panic)
266        let limiter = PruneLimiter::default();
267        let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
268        assert!(updated_limiter.deleted_entries_limit.is_none());
269    }
270
271    #[test]
272    fn test_is_deleted_entries_limit_reached() {
273        // Limit is not set, should return false
274        let limiter = PruneLimiter::default();
275        assert!(!limiter.is_deleted_entries_limit_reached());
276
277        // Limit is set but not reached, should return false
278        let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
279        limiter.deleted_entries_limit.as_mut().unwrap().deleted = 5;
280        // 5 entries deleted out of 10
281        assert!(!limiter.is_deleted_entries_limit_reached());
282
283        // Limit is reached, should return true
284        limiter.deleted_entries_limit.as_mut().unwrap().deleted = 10;
285        // 10 entries deleted out of 10
286        assert!(limiter.is_deleted_entries_limit_reached());
287
288        // Deleted entries exceed the limit, should return true
289        limiter.deleted_entries_limit.as_mut().unwrap().deleted = 12;
290        // 12 entries deleted out of 10
291        assert!(limiter.is_deleted_entries_limit_reached());
292    }
293
294    #[test]
295    fn test_increment_deleted_entries_count_by() {
296        // Increment when no limit is set
297        let mut limiter = PruneLimiter::default();
298        limiter.increment_deleted_entries_count_by(5);
299        assert_eq!(limiter.deleted_entries_limit.as_ref().map(|l| l.deleted), None); // Still None
300
301        // Increment when limit is set
302        let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
303        limiter.increment_deleted_entries_count_by(3);
304        assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 3); // Now 3 deleted
305
306        // Increment again
307        limiter.increment_deleted_entries_count_by(2);
308        assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 5); // Now 5 deleted
309    }
310
311    #[test]
312    fn test_increment_deleted_entries_count() {
313        let mut limiter = PruneLimiter::default().set_deleted_entries_limit(5);
314        assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 0); // Initially 0
315
316        limiter.increment_deleted_entries_count(); // Increment by 1
317        assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 1); // Now 1
318    }
319
320    #[test]
321    fn test_deleted_entries_limit_left() {
322        // Test when limit is set and some entries are deleted
323        let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
324        limiter.increment_deleted_entries_count_by(3); // Simulate 3 deleted entries
325        assert_eq!(limiter.deleted_entries_limit_left(), Some(7)); // 10 - 3 = 7
326
327        // Test when no entries are deleted
328        limiter = PruneLimiter::default().set_deleted_entries_limit(5);
329        assert_eq!(limiter.deleted_entries_limit_left(), Some(5)); // 5 - 0 = 5
330
331        // Test when limit is reached
332        limiter.increment_deleted_entries_count_by(5); // Simulate deleting 5 entries
333        assert_eq!(limiter.deleted_entries_limit_left(), Some(0)); // 5 - 5 = 0
334
335        // Test when limit is not set
336        limiter = PruneLimiter::default(); // No limit set
337        assert_eq!(limiter.deleted_entries_limit_left(), None); // Should be None
338    }
339
340    #[test]
341    fn test_set_time_limit() {
342        // Create a PruneLimiter instance with no time limit set
343        let mut limiter = PruneLimiter::default();
344
345        // Set a time limit of 5 seconds
346        limiter = limiter.set_time_limit(Duration::new(5, 0));
347
348        // Verify that the time limit is set correctly
349        assert!(limiter.time_limit.is_some());
350        let time_limit = limiter.time_limit.as_ref().unwrap();
351        assert_eq!(time_limit.limit, Duration::new(5, 0));
352        // Ensure the start time is recent
353        assert!(time_limit.start.elapsed() < Duration::new(1, 0));
354    }
355
356    #[test]
357    fn test_is_time_limit_reached() {
358        // Create a PruneLimiter instance and set a time limit of 10 milliseconds
359        let mut limiter = PruneLimiter::default();
360
361        // Time limit should not be reached initially
362        assert!(!limiter.is_time_limit_reached(), "Time limit should not be reached yet");
363
364        limiter = limiter.set_time_limit(Duration::new(0, 10_000_000)); // 10 milliseconds
365
366        // Sleep for 5 milliseconds (less than the time limit)
367        sleep(Duration::new(0, 5_000_000)); // 5 milliseconds
368        assert!(!limiter.is_time_limit_reached(), "Time limit should not be reached yet");
369
370        // Sleep for an additional 10 milliseconds (totaling 15 milliseconds)
371        sleep(Duration::new(0, 10_000_000)); // 10 milliseconds
372        assert!(limiter.is_time_limit_reached(), "Time limit should be reached now");
373    }
374
375    #[test]
376    fn test_is_limit_reached() {
377        // Create a PruneLimiter instance
378        let mut limiter = PruneLimiter::default();
379
380        // Test when no limits are set
381        assert!(!limiter.is_limit_reached(), "Limit should not be reached with no limits set");
382
383        // Set a deleted entries limit
384        limiter = limiter.set_deleted_entries_limit(5);
385        assert!(
386            !limiter.is_limit_reached(),
387            "Limit should not be reached when deleted entries are less than limit"
388        );
389
390        // Increment deleted entries count to reach the limit
391        limiter.increment_deleted_entries_count_by(5);
392        assert!(
393            limiter.is_limit_reached(),
394            "Limit should be reached when deleted entries equal the limit"
395        );
396
397        // Reset the limiter
398        limiter = PruneLimiter::default();
399
400        // Set a time limit and check
401        limiter = limiter.set_time_limit(Duration::new(0, 10_000_000)); // 10 milliseconds
402
403        // Sleep for 5 milliseconds (less than the time limit)
404        sleep(Duration::new(0, 5_000_000)); // 5 milliseconds
405        assert!(
406            !limiter.is_limit_reached(),
407            "Limit should not be reached when time limit not reached"
408        );
409
410        // Sleep for another 10 milliseconds (totaling 15 milliseconds)
411        sleep(Duration::new(0, 10_000_000)); // 10 milliseconds
412        assert!(limiter.is_limit_reached(), "Limit should be reached when time limit is reached");
413    }
414}