Skip to main content

reth_prune_types/
pruner.rs

1use crate::{PruneCheckpoint, PruneMode, PruneSegment};
2use alloc::{format, string::ToString, vec::Vec};
3use alloy_primitives::{BlockNumber, TxNumber};
4use core::time::Duration;
5use derive_more::Display;
6use tracing::debug;
7
8/// Pruner run output.
9#[derive(Debug)]
10pub struct PrunerOutput {
11    /// Pruning progress.
12    pub progress: PruneProgress,
13    /// Pruning output for each segment.
14    pub segments: Vec<(PruneSegment, SegmentOutput)>,
15}
16
17impl From<PruneProgress> for PrunerOutput {
18    fn from(progress: PruneProgress) -> Self {
19        Self { progress, segments: Vec::new() }
20    }
21}
22
23impl PrunerOutput {
24    /// Logs a human-readable summary of the pruner run at DEBUG level.
25    ///
26    /// Format: `"Pruner finished tip=24328929 deleted=10886 elapsed=148ms
27    /// segments=AccountHistory[24318865, done] ..."`
28    #[inline]
29    pub fn debug_log(
30        &self,
31        tip_block_number: BlockNumber,
32        deleted_entries: usize,
33        elapsed: Duration,
34    ) {
35        let message = match self.progress {
36            PruneProgress::HasMoreData(_) => "Pruner interrupted, has more data",
37            PruneProgress::Finished => "Pruner finished",
38        };
39
40        let segments: Vec<_> = self
41            .segments
42            .iter()
43            .filter(|(_, seg)| seg.pruned > 0)
44            .map(|(segment, seg)| {
45                let block = seg
46                    .checkpoint
47                    .and_then(|c| c.block_number)
48                    .map(|b| b.to_string())
49                    .unwrap_or_else(|| "?".to_string());
50                let status = if seg.progress.is_finished() { "done" } else { "in_progress" };
51                format!("{segment}[{block}, {status}]")
52            })
53            .collect();
54
55        debug!(
56            target: "pruner",
57            %tip_block_number,
58            deleted_entries,
59            ?elapsed,
60            segments = %segments.join(" "),
61            "{message}",
62        );
63    }
64}
65
66/// Represents information of a pruner run for a segment.
67#[derive(Debug, Clone, PartialEq, Eq, Display)]
68#[display("(table={segment}, pruned={pruned}, status={progress})")]
69pub struct PrunedSegmentInfo {
70    /// The pruned segment
71    pub segment: PruneSegment,
72    /// Number of pruned entries
73    pub pruned: usize,
74    /// Prune progress
75    pub progress: PruneProgress,
76}
77
78/// Segment pruning output.
79#[derive(Debug, Clone, Copy, Eq, PartialEq)]
80pub struct SegmentOutput {
81    /// Segment pruning progress.
82    pub progress: PruneProgress,
83    /// Number of entries pruned, i.e. deleted from the database.
84    pub pruned: usize,
85    /// Pruning checkpoint to save to database, if any.
86    pub checkpoint: Option<SegmentOutputCheckpoint>,
87}
88
89impl SegmentOutput {
90    /// Returns a [`SegmentOutput`] with `done = true`, `pruned = 0` and `checkpoint = None`.
91    /// Use when no pruning is needed.
92    pub const fn done() -> Self {
93        Self { progress: PruneProgress::Finished, pruned: 0, checkpoint: None }
94    }
95
96    /// Returns a [`SegmentOutput`] with `done = false`, `pruned = 0` and the given checkpoint.
97    /// Use when pruning is needed but cannot be done.
98    pub const fn not_done(
99        reason: PruneInterruptReason,
100        checkpoint: Option<SegmentOutputCheckpoint>,
101    ) -> Self {
102        Self { progress: PruneProgress::HasMoreData(reason), pruned: 0, checkpoint }
103    }
104}
105
106/// Segment pruning checkpoint.
107#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
108pub struct SegmentOutputCheckpoint {
109    /// Highest pruned block number. If it's [None], the pruning for block `0` is not finished yet.
110    pub block_number: Option<BlockNumber>,
111    /// Highest pruned transaction number, if applicable.
112    pub tx_number: Option<TxNumber>,
113}
114
115impl SegmentOutputCheckpoint {
116    /// Converts [`PruneCheckpoint`] to [`SegmentOutputCheckpoint`].
117    pub const fn from_prune_checkpoint(checkpoint: PruneCheckpoint) -> Self {
118        Self { block_number: checkpoint.block_number, tx_number: checkpoint.tx_number }
119    }
120
121    /// Converts [`SegmentOutputCheckpoint`] to [`PruneCheckpoint`] with the provided [`PruneMode`]
122    pub const fn as_prune_checkpoint(&self, prune_mode: PruneMode) -> PruneCheckpoint {
123        PruneCheckpoint { block_number: self.block_number, tx_number: self.tx_number, prune_mode }
124    }
125}
126
127/// Progress of pruning.
128#[derive(Debug, PartialEq, Eq, Clone, Copy, Display)]
129pub enum PruneProgress {
130    /// There is more data to prune.
131    #[display("HasMoreData({_0})")]
132    HasMoreData(PruneInterruptReason),
133    /// Pruning has been finished.
134    #[display("Finished")]
135    Finished,
136}
137
138/// Reason for interrupting a prune run.
139#[derive(Debug, PartialEq, Eq, Clone, Copy, Display)]
140pub enum PruneInterruptReason {
141    /// Prune run timed out.
142    Timeout,
143    /// Limit on the number of deleted entries (rows in the database) per prune run was reached.
144    DeletedEntriesLimitReached,
145    /// Waiting for another segment to finish pruning before this segment can proceed.
146    WaitingOnSegment(PruneSegment),
147    /// Unknown reason for stopping prune run.
148    Unknown,
149}
150
151impl PruneInterruptReason {
152    /// Returns `true` if the reason is timeout.
153    pub const fn is_timeout(&self) -> bool {
154        matches!(self, Self::Timeout)
155    }
156
157    /// Returns `true` if the reason is reaching the limit on deleted entries.
158    pub const fn is_entries_limit_reached(&self) -> bool {
159        matches!(self, Self::DeletedEntriesLimitReached)
160    }
161}
162
163impl PruneProgress {
164    /// Returns `true` if prune run is finished.
165    pub const fn is_finished(&self) -> bool {
166        matches!(self, Self::Finished)
167    }
168
169    /// Combines two progress values, keeping `HasMoreData` if either has it.
170    ///
171    /// Once any segment reports `HasMoreData`, the combined progress remains
172    /// `HasMoreData`. Only returns `Finished` if both are `Finished`.
173    #[must_use]
174    pub const fn combine(self, other: Self) -> Self {
175        match (self, other) {
176            (Self::HasMoreData(reason), _) => Self::HasMoreData(reason),
177            (Self::Finished, other) => other,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_prune_progress_combine() {
188        use PruneInterruptReason::*;
189        use PruneProgress::*;
190
191        // HasMoreData dominates Finished
192        assert!(matches!(HasMoreData(Timeout).combine(Finished), HasMoreData(Timeout)));
193
194        // First HasMoreData reason is preserved
195        assert!(matches!(
196            HasMoreData(Timeout).combine(HasMoreData(DeletedEntriesLimitReached)),
197            HasMoreData(Timeout)
198        ));
199
200        // Finished adopts new progress
201        assert!(matches!(Finished.combine(Finished), Finished));
202        assert!(matches!(
203            Finished.combine(HasMoreData(DeletedEntriesLimitReached)),
204            HasMoreData(DeletedEntriesLimitReached)
205        ));
206    }
207}