1use reth_prune_types::{PruneInterruptReason, PruneProgress};
2use std::{
3num::NonZeroUsize,
4 time::{Duration, Instant},
5};
67/// 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.
12deleted_entries_limit: Option<PruneDeletedEntriesLimit>,
13/// Maximum duration of one prune run.
14time_limit: Option<PruneTimeLimit>,
15}
1617#[derive(Debug, Clone)]
18struct PruneDeletedEntriesLimit {
19/// Maximum entries (rows in the database) to delete from the database.
20limit: usize,
21/// Current number of entries (rows in the database) that have been deleted.
22deleted: usize,
23}
2425impl PruneDeletedEntriesLimit {
26const fn new(limit: usize) -> Self {
27Self { limit, deleted: 0 }
28 }
2930const fn is_limit_reached(&self) -> bool {
31self.deleted >= self.limit
32 }
33}
3435#[derive(Debug, Clone)]
36struct PruneTimeLimit {
37/// Maximum duration of one prune run.
38limit: Duration,
39/// Time when the prune run has started.
40start: Instant,
41}
4243impl PruneTimeLimit {
44fn new(limit: Duration) -> Self {
45Self { limit, start: Instant::now() }
46 }
4748fn is_limit_reached(&self) -> bool {
49self.start.elapsed() > self.limit
50 }
51}
5253impl 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.
56pub const fn set_deleted_entries_limit(mut self, limit: usize) -> Self {
57if let Some(deleted_entries_limit) = self.deleted_entries_limit.as_mut() {
58deleted_entries_limit.limit = limit;
59 } else {
60self.deleted_entries_limit = Some(PruneDeletedEntriesLimit::new(limit));
61 }
6263self64}
6566/// 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.
70pub fn floor_deleted_entries_limit_to_multiple_of(mut self, denominator: NonZeroUsize) -> Self {
71if let Some(deleted_entries_limit) = self.deleted_entries_limit.as_mut() {
72deleted_entries_limit.limit =
73 (deleted_entries_limit.limit / denominator) * denominator.get();
74 }
7576self77}
7879/// Returns `true` if the limit on the number of deleted entries (rows in the database) is
80 /// reached.
81pub fn is_deleted_entries_limit_reached(&self) -> bool {
82self.deleted_entries_limit.as_ref().is_some_and(|limit| limit.is_limit_reached())
83 }
8485/// Increments the number of deleted entries by the given number.
86pub const fn increment_deleted_entries_count_by(&mut self, entries: usize) {
87if let Some(limit) = self.deleted_entries_limit.as_mut() {
88limit.deleted += entries;
89 }
90 }
9192/// Increments the number of deleted entries by one.
93pub const fn increment_deleted_entries_count(&mut self) {
94self.increment_deleted_entries_count_by(1)
95 }
9697/// Returns the number of deleted entries left before the limit is reached.
98pub fn deleted_entries_limit_left(&self) -> Option<usize> {
99self.deleted_entries_limit.as_ref().map(|limit| limit.limit - limit.deleted)
100 }
101102/// Returns the limit on the number of deleted entries (rows in the database).
103pub fn deleted_entries_limit(&self) -> Option<usize> {
104self.deleted_entries_limit.as_ref().map(|limit| limit.limit)
105 }
106107/// Sets the time limit.
108pub fn set_time_limit(mut self, limit: Duration) -> Self {
109self.time_limit = Some(PruneTimeLimit::new(limit));
110111self112}
113114/// Returns `true` if time limit is reached.
115pub fn is_time_limit_reached(&self) -> bool {
116self.time_limit.as_ref().is_some_and(|limit| limit.is_limit_reached())
117 }
118119/// Returns `true` if any limit is reached.
120pub fn is_limit_reached(&self) -> bool {
121self.is_deleted_entries_limit_reached() || self.is_time_limit_reached()
122 }
123124/// Creates new [`PruneInterruptReason`] based on the limiter's state.
125pub fn interrupt_reason(&self) -> PruneInterruptReason {
126if self.is_time_limit_reached() {
127PruneInterruptReason::Timeout128 } else if self.is_deleted_entries_limit_reached() {
129PruneInterruptReason::DeletedEntriesLimitReached130 } else {
131PruneInterruptReason::Unknown132 }
133 }
134135/// 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.
140pub fn progress(&self, done: bool) -> PruneProgress {
141if done {
142PruneProgress::Finished143 } else {
144PruneProgress::HasMoreData(self.interrupt_reason())
145 }
146 }
147}
148149#[cfg(test)]
150mod tests {
151use super::*;
152use std::thread::sleep;
153154#[test]
155fn test_prune_deleted_entries_limit_initial_state() {
156let limit_tracker = PruneDeletedEntriesLimit::new(10);
157// Limit should be set properly
158assert_eq!(limit_tracker.limit, 10);
159// No entries should be deleted
160assert_eq!(limit_tracker.deleted, 0);
161assert!(!limit_tracker.is_limit_reached());
162 }
163164#[test]
165fn test_prune_deleted_entries_limit_is_limit_reached() {
166// Test when the deleted entries are less than the limit
167let mut limit_tracker = PruneDeletedEntriesLimit::new(5);
168 limit_tracker.deleted = 3;
169assert!(!limit_tracker.is_limit_reached());
170171// Test when the deleted entries are equal to the limit
172limit_tracker.deleted = 5;
173assert!(limit_tracker.is_limit_reached());
174175// Test when the deleted entries exceed the limit
176limit_tracker.deleted = 6;
177assert!(limit_tracker.is_limit_reached());
178 }
179180#[test]
181fn test_prune_time_limit_initial_state() {
182let time_limit = PruneTimeLimit::new(Duration::from_secs(10));
183// The limit should be set correctly
184assert_eq!(time_limit.limit, Duration::from_secs(10));
185// The elapsed time should be very small right after creation
186assert!(time_limit.start.elapsed() < Duration::from_secs(1));
187// Limit should not be reached initially
188assert!(!time_limit.is_limit_reached());
189 }
190191#[test]
192fn test_prune_time_limit_is_limit_reached() {
193let time_limit = PruneTimeLimit::new(Duration::from_millis(50));
194195// Simulate waiting for some time (less than the limit)
196std::thread::sleep(Duration::from_millis(30));
197assert!(!time_limit.is_limit_reached());
198199// Simulate waiting for time greater than the limit
200std::thread::sleep(Duration::from_millis(30));
201assert!(time_limit.is_limit_reached());
202 }
203204#[test]
205fn test_set_deleted_entries_limit_initial_state() {
206let pruner = PruneLimiter::default().set_deleted_entries_limit(100);
207// The deleted_entries_limit should be set with the correct limit
208assert!(pruner.deleted_entries_limit.is_some());
209let deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
210assert_eq!(deleted_entries_limit.limit, 100);
211// The deleted count should be initially zero
212assert_eq!(deleted_entries_limit.deleted, 0);
213// The limit should not be reached initially
214assert!(!deleted_entries_limit.is_limit_reached());
215 }
216217#[test]
218fn test_set_deleted_entries_limit_overwrite_existing() {
219let mut pruner = PruneLimiter::default().set_deleted_entries_limit(50);
220// Overwrite the existing limit
221pruner = pruner.set_deleted_entries_limit(200);
222223assert!(pruner.deleted_entries_limit.is_some());
224let deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
225// Check that the limit has been overwritten correctly
226assert_eq!(deleted_entries_limit.limit, 200);
227// Deleted count should still be zero
228assert_eq!(deleted_entries_limit.deleted, 0);
229assert!(!deleted_entries_limit.is_limit_reached());
230 }
231232#[test]
233fn test_set_deleted_entries_limit_when_limit_is_reached() {
234let mut pruner = PruneLimiter::default().set_deleted_entries_limit(5);
235assert!(pruner.deleted_entries_limit.is_some());
236let mut deleted_entries_limit = pruner.deleted_entries_limit.clone().unwrap();
237238// Simulate deletion of entries
239deleted_entries_limit.deleted = 5;
240assert!(deleted_entries_limit.is_limit_reached());
241242// Overwrite the limit and check if it resets correctly
243pruner = pruner.set_deleted_entries_limit(10);
244 deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
245assert_eq!(deleted_entries_limit.limit, 10);
246// Deletion count should reset
247assert_eq!(deleted_entries_limit.deleted, 0);
248assert!(!deleted_entries_limit.is_limit_reached());
249 }
250251#[test]
252fn test_floor_deleted_entries_limit_to_multiple_of() {
253let limiter = PruneLimiter::default().set_deleted_entries_limit(15);
254let denominator = NonZeroUsize::new(4).unwrap();
255256// Floor limit to the largest multiple of 4 less than or equal to 15 (that is 12)
257let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
258assert_eq!(updated_limiter.deleted_entries_limit.unwrap().limit, 12);
259260// Test when the limit is already a multiple of the denominator
261let limiter = PruneLimiter::default().set_deleted_entries_limit(16);
262let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
263assert_eq!(updated_limiter.deleted_entries_limit.unwrap().limit, 16);
264265// Test when there's no limit set (should not panic)
266let limiter = PruneLimiter::default();
267let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
268assert!(updated_limiter.deleted_entries_limit.is_none());
269 }
270271#[test]
272fn test_is_deleted_entries_limit_reached() {
273// Limit is not set, should return false
274let limiter = PruneLimiter::default();
275assert!(!limiter.is_deleted_entries_limit_reached());
276277// Limit is set but not reached, should return false
278let 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
281assert!(!limiter.is_deleted_entries_limit_reached());
282283// Limit is reached, should return true
284limiter.deleted_entries_limit.as_mut().unwrap().deleted = 10;
285// 10 entries deleted out of 10
286assert!(limiter.is_deleted_entries_limit_reached());
287288// Deleted entries exceed the limit, should return true
289limiter.deleted_entries_limit.as_mut().unwrap().deleted = 12;
290// 12 entries deleted out of 10
291assert!(limiter.is_deleted_entries_limit_reached());
292 }
293294#[test]
295fn test_increment_deleted_entries_count_by() {
296// Increment when no limit is set
297let mut limiter = PruneLimiter::default();
298 limiter.increment_deleted_entries_count_by(5);
299assert_eq!(limiter.deleted_entries_limit.as_ref().map(|l| l.deleted), None); // Still None
300301 // Increment when limit is set
302let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
303 limiter.increment_deleted_entries_count_by(3);
304assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 3); // Now 3 deleted
305306 // Increment again
307limiter.increment_deleted_entries_count_by(2);
308assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 5); // Now 5 deleted
309}
310311#[test]
312fn test_increment_deleted_entries_count() {
313let mut limiter = PruneLimiter::default().set_deleted_entries_limit(5);
314assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 0); // Initially 0
315316limiter.increment_deleted_entries_count(); // Increment by 1
317assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 1); // Now 1
318}
319320#[test]
321fn test_deleted_entries_limit_left() {
322// Test when limit is set and some entries are deleted
323let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
324 limiter.increment_deleted_entries_count_by(3); // Simulate 3 deleted entries
325assert_eq!(limiter.deleted_entries_limit_left(), Some(7)); // 10 - 3 = 7
326327 // Test when no entries are deleted
328limiter = PruneLimiter::default().set_deleted_entries_limit(5);
329assert_eq!(limiter.deleted_entries_limit_left(), Some(5)); // 5 - 0 = 5
330331 // Test when limit is reached
332limiter.increment_deleted_entries_count_by(5); // Simulate deleting 5 entries
333assert_eq!(limiter.deleted_entries_limit_left(), Some(0)); // 5 - 5 = 0
334335 // Test when limit is not set
336limiter = PruneLimiter::default(); // No limit set
337assert_eq!(limiter.deleted_entries_limit_left(), None); // Should be None
338}
339340#[test]
341fn test_set_time_limit() {
342// Create a PruneLimiter instance with no time limit set
343let mut limiter = PruneLimiter::default();
344345// Set a time limit of 5 seconds
346limiter = limiter.set_time_limit(Duration::new(5, 0));
347348// Verify that the time limit is set correctly
349assert!(limiter.time_limit.is_some());
350let time_limit = limiter.time_limit.as_ref().unwrap();
351assert_eq!(time_limit.limit, Duration::new(5, 0));
352// Ensure the start time is recent
353assert!(time_limit.start.elapsed() < Duration::new(1, 0));
354 }
355356#[test]
357fn test_is_time_limit_reached() {
358// Create a PruneLimiter instance and set a time limit of 10 milliseconds
359let mut limiter = PruneLimiter::default();
360361// Time limit should not be reached initially
362assert!(!limiter.is_time_limit_reached(), "Time limit should not be reached yet");
363364 limiter = limiter.set_time_limit(Duration::new(0, 10_000_000)); // 10 milliseconds
365366 // Sleep for 5 milliseconds (less than the time limit)
367sleep(Duration::new(0, 5_000_000)); // 5 milliseconds
368assert!(!limiter.is_time_limit_reached(), "Time limit should not be reached yet");
369370// Sleep for an additional 10 milliseconds (totaling 15 milliseconds)
371sleep(Duration::new(0, 10_000_000)); // 10 milliseconds
372assert!(limiter.is_time_limit_reached(), "Time limit should be reached now");
373 }
374375#[test]
376fn test_is_limit_reached() {
377// Create a PruneLimiter instance
378let mut limiter = PruneLimiter::default();
379380// Test when no limits are set
381assert!(!limiter.is_limit_reached(), "Limit should not be reached with no limits set");
382383// Set a deleted entries limit
384limiter = limiter.set_deleted_entries_limit(5);
385assert!(
386 !limiter.is_limit_reached(),
387"Limit should not be reached when deleted entries are less than limit"
388);
389390// Increment deleted entries count to reach the limit
391limiter.increment_deleted_entries_count_by(5);
392assert!(
393 limiter.is_limit_reached(),
394"Limit should be reached when deleted entries equal the limit"
395);
396397// Reset the limiter
398limiter = PruneLimiter::default();
399400// Set a time limit and check
401limiter = limiter.set_time_limit(Duration::new(0, 10_000_000)); // 10 milliseconds
402403 // Sleep for 5 milliseconds (less than the time limit)
404sleep(Duration::new(0, 5_000_000)); // 5 milliseconds
405assert!(
406 !limiter.is_limit_reached(),
407"Limit should not be reached when time limit not reached"
408);
409410// Sleep for another 10 milliseconds (totaling 15 milliseconds)
411sleep(Duration::new(0, 10_000_000)); // 10 milliseconds
412assert!(limiter.is_limit_reached(), "Limit should be reached when time limit is reached");
413 }
414}