use reth_prune_types::{PruneInterruptReason, PruneProgress};
use std::{
num::NonZeroUsize,
time::{Duration, Instant},
};
#[derive(Debug, Clone, Default)]
pub struct PruneLimiter {
deleted_entries_limit: Option<PruneDeletedEntriesLimit>,
time_limit: Option<PruneTimeLimit>,
}
#[derive(Debug, Clone)]
struct PruneDeletedEntriesLimit {
limit: usize,
deleted: usize,
}
impl PruneDeletedEntriesLimit {
const fn new(limit: usize) -> Self {
Self { limit, deleted: 0 }
}
const fn is_limit_reached(&self) -> bool {
self.deleted >= self.limit
}
}
#[derive(Debug, Clone)]
struct PruneTimeLimit {
limit: Duration,
start: Instant,
}
impl PruneTimeLimit {
fn new(limit: Duration) -> Self {
Self { limit, start: Instant::now() }
}
fn is_limit_reached(&self) -> bool {
self.start.elapsed() > self.limit
}
}
impl PruneLimiter {
pub fn set_deleted_entries_limit(mut self, limit: usize) -> Self {
if let Some(deleted_entries_limit) = self.deleted_entries_limit.as_mut() {
deleted_entries_limit.limit = limit;
} else {
self.deleted_entries_limit = Some(PruneDeletedEntriesLimit::new(limit));
}
self
}
pub fn floor_deleted_entries_limit_to_multiple_of(mut self, denominator: NonZeroUsize) -> Self {
if let Some(deleted_entries_limit) = self.deleted_entries_limit.as_mut() {
deleted_entries_limit.limit =
(deleted_entries_limit.limit / denominator) * denominator.get();
}
self
}
pub fn is_deleted_entries_limit_reached(&self) -> bool {
self.deleted_entries_limit.as_ref().is_some_and(|limit| limit.is_limit_reached())
}
pub fn increment_deleted_entries_count_by(&mut self, entries: usize) {
if let Some(limit) = self.deleted_entries_limit.as_mut() {
limit.deleted += entries;
}
}
pub fn increment_deleted_entries_count(&mut self) {
self.increment_deleted_entries_count_by(1)
}
pub fn deleted_entries_limit_left(&self) -> Option<usize> {
self.deleted_entries_limit.as_ref().map(|limit| limit.limit - limit.deleted)
}
pub fn deleted_entries_limit(&self) -> Option<usize> {
self.deleted_entries_limit.as_ref().map(|limit| limit.limit)
}
pub fn set_time_limit(mut self, limit: Duration) -> Self {
self.time_limit = Some(PruneTimeLimit::new(limit));
self
}
pub fn is_time_limit_reached(&self) -> bool {
self.time_limit.as_ref().is_some_and(|limit| limit.is_limit_reached())
}
pub fn is_limit_reached(&self) -> bool {
self.is_deleted_entries_limit_reached() || self.is_time_limit_reached()
}
pub fn interrupt_reason(&self) -> PruneInterruptReason {
if self.is_time_limit_reached() {
PruneInterruptReason::Timeout
} else if self.is_deleted_entries_limit_reached() {
PruneInterruptReason::DeletedEntriesLimitReached
} else {
PruneInterruptReason::Unknown
}
}
pub fn progress(&self, done: bool) -> PruneProgress {
if done {
PruneProgress::Finished
} else {
PruneProgress::HasMoreData(self.interrupt_reason())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_prune_deleted_entries_limit_initial_state() {
let limit_tracker = PruneDeletedEntriesLimit::new(10);
assert_eq!(limit_tracker.limit, 10);
assert_eq!(limit_tracker.deleted, 0);
assert!(!limit_tracker.is_limit_reached());
}
#[test]
fn test_prune_deleted_entries_limit_is_limit_reached() {
let mut limit_tracker = PruneDeletedEntriesLimit::new(5);
limit_tracker.deleted = 3;
assert!(!limit_tracker.is_limit_reached());
limit_tracker.deleted = 5;
assert!(limit_tracker.is_limit_reached());
limit_tracker.deleted = 6;
assert!(limit_tracker.is_limit_reached());
}
#[test]
fn test_prune_time_limit_initial_state() {
let time_limit = PruneTimeLimit::new(Duration::from_secs(10));
assert_eq!(time_limit.limit, Duration::from_secs(10));
assert!(time_limit.start.elapsed() < Duration::from_secs(1));
assert!(!time_limit.is_limit_reached());
}
#[test]
fn test_prune_time_limit_is_limit_reached() {
let time_limit = PruneTimeLimit::new(Duration::from_millis(50));
std::thread::sleep(Duration::from_millis(30));
assert!(!time_limit.is_limit_reached());
std::thread::sleep(Duration::from_millis(30));
assert!(time_limit.is_limit_reached());
}
#[test]
fn test_set_deleted_entries_limit_initial_state() {
let pruner = PruneLimiter::default().set_deleted_entries_limit(100);
assert!(pruner.deleted_entries_limit.is_some());
let deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
assert_eq!(deleted_entries_limit.limit, 100);
assert_eq!(deleted_entries_limit.deleted, 0);
assert!(!deleted_entries_limit.is_limit_reached());
}
#[test]
fn test_set_deleted_entries_limit_overwrite_existing() {
let mut pruner = PruneLimiter::default().set_deleted_entries_limit(50);
pruner = pruner.set_deleted_entries_limit(200);
assert!(pruner.deleted_entries_limit.is_some());
let deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
assert_eq!(deleted_entries_limit.limit, 200);
assert_eq!(deleted_entries_limit.deleted, 0);
assert!(!deleted_entries_limit.is_limit_reached());
}
#[test]
fn test_set_deleted_entries_limit_when_limit_is_reached() {
let mut pruner = PruneLimiter::default().set_deleted_entries_limit(5);
assert!(pruner.deleted_entries_limit.is_some());
let mut deleted_entries_limit = pruner.deleted_entries_limit.clone().unwrap();
deleted_entries_limit.deleted = 5;
assert!(deleted_entries_limit.is_limit_reached());
pruner = pruner.set_deleted_entries_limit(10);
deleted_entries_limit = pruner.deleted_entries_limit.unwrap();
assert_eq!(deleted_entries_limit.limit, 10);
assert_eq!(deleted_entries_limit.deleted, 0);
assert!(!deleted_entries_limit.is_limit_reached());
}
#[test]
fn test_floor_deleted_entries_limit_to_multiple_of() {
let limiter = PruneLimiter::default().set_deleted_entries_limit(15);
let denominator = NonZeroUsize::new(4).unwrap();
let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
assert_eq!(updated_limiter.deleted_entries_limit.unwrap().limit, 12);
let limiter = PruneLimiter::default().set_deleted_entries_limit(16);
let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
assert_eq!(updated_limiter.deleted_entries_limit.unwrap().limit, 16);
let limiter = PruneLimiter::default();
let updated_limiter = limiter.floor_deleted_entries_limit_to_multiple_of(denominator);
assert!(updated_limiter.deleted_entries_limit.is_none());
}
#[test]
fn test_is_deleted_entries_limit_reached() {
let limiter = PruneLimiter::default();
assert!(!limiter.is_deleted_entries_limit_reached());
let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
limiter.deleted_entries_limit.as_mut().unwrap().deleted = 5;
assert!(!limiter.is_deleted_entries_limit_reached());
limiter.deleted_entries_limit.as_mut().unwrap().deleted = 10;
assert!(limiter.is_deleted_entries_limit_reached());
limiter.deleted_entries_limit.as_mut().unwrap().deleted = 12;
assert!(limiter.is_deleted_entries_limit_reached());
}
#[test]
fn test_increment_deleted_entries_count_by() {
let mut limiter = PruneLimiter::default();
limiter.increment_deleted_entries_count_by(5);
assert_eq!(limiter.deleted_entries_limit.as_ref().map(|l| l.deleted), None); let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
limiter.increment_deleted_entries_count_by(3);
assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 3); limiter.increment_deleted_entries_count_by(2);
assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 5); }
#[test]
fn test_increment_deleted_entries_count() {
let mut limiter = PruneLimiter::default().set_deleted_entries_limit(5);
assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 0); limiter.increment_deleted_entries_count(); assert_eq!(limiter.deleted_entries_limit.as_ref().unwrap().deleted, 1); }
#[test]
fn test_deleted_entries_limit_left() {
let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
limiter.increment_deleted_entries_count_by(3); assert_eq!(limiter.deleted_entries_limit_left(), Some(7)); limiter = PruneLimiter::default().set_deleted_entries_limit(5);
assert_eq!(limiter.deleted_entries_limit_left(), Some(5)); limiter.increment_deleted_entries_count_by(5); assert_eq!(limiter.deleted_entries_limit_left(), Some(0)); limiter = PruneLimiter::default(); assert_eq!(limiter.deleted_entries_limit_left(), None); }
#[test]
fn test_set_time_limit() {
let mut limiter = PruneLimiter::default();
limiter = limiter.set_time_limit(Duration::new(5, 0));
assert!(limiter.time_limit.is_some());
let time_limit = limiter.time_limit.as_ref().unwrap();
assert_eq!(time_limit.limit, Duration::new(5, 0));
assert!(time_limit.start.elapsed() < Duration::new(1, 0));
}
#[test]
fn test_is_time_limit_reached() {
let mut limiter = PruneLimiter::default();
assert!(!limiter.is_time_limit_reached(), "Time limit should not be reached yet");
limiter = limiter.set_time_limit(Duration::new(0, 10_000_000)); sleep(Duration::new(0, 5_000_000)); assert!(!limiter.is_time_limit_reached(), "Time limit should not be reached yet");
sleep(Duration::new(0, 10_000_000)); assert!(limiter.is_time_limit_reached(), "Time limit should be reached now");
}
#[test]
fn test_is_limit_reached() {
let mut limiter = PruneLimiter::default();
assert!(!limiter.is_limit_reached(), "Limit should not be reached with no limits set");
limiter = limiter.set_deleted_entries_limit(5);
assert!(
!limiter.is_limit_reached(),
"Limit should not be reached when deleted entries are less than limit"
);
limiter.increment_deleted_entries_count_by(5);
assert!(
limiter.is_limit_reached(),
"Limit should be reached when deleted entries equal the limit"
);
limiter = PruneLimiter::default();
limiter = limiter.set_time_limit(Duration::new(0, 10_000_000)); sleep(Duration::new(0, 5_000_000)); assert!(
!limiter.is_limit_reached(),
"Limit should not be reached when time limit not reached"
);
sleep(Duration::new(0, 10_000_000)); assert!(limiter.is_limit_reached(), "Limit should be reached when time limit is reached");
}
}