fix(cli): delete all static files when PruneModes::Full is configured (#21647)

This commit is contained in:
Dan Cline
2026-02-02 17:30:21 +00:00
committed by GitHub
parent f663d1d110
commit 6946f26d77
3 changed files with 102 additions and 6 deletions

View File

@@ -54,6 +54,38 @@ where
})
}
/// Deletes ALL static file jars for a given segment.
///
/// This is used for `PruneMode::Full` where all data should be removed, including the highest jar.
/// Unlike [`prune_static_files`], this does not preserve the most recent jar.
pub(crate) fn delete_static_files_segment<Provider>(
provider: &Provider,
input: PruneInput,
segment: StaticFileSegment,
) -> Result<SegmentOutput, PrunerError>
where
Provider: StaticFileProviderFactory,
{
let deleted_headers = provider.static_file_provider().delete_segment(segment)?;
if deleted_headers.is_empty() {
return Ok(SegmentOutput::done())
}
let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range());
let pruned = tx_ranges.clone().map(|range| range.len()).sum::<u64>() as usize;
Ok(SegmentOutput {
progress: PruneProgress::Finished,
pruned,
checkpoint: Some(SegmentOutputCheckpoint {
block_number: Some(input.to_block),
tx_number: tx_ranges.map(|range| range.end()).max(),
}),
})
}
/// A segment represents a pruning of some portion of the data.
///
/// Segments are called from [`Pruner`](crate::Pruner) with the following lifecycle:

View File

@@ -49,6 +49,16 @@ where
fn prune(&self, provider: &Provider, input: PruneInput) -> Result<SegmentOutput, PrunerError> {
if EitherWriterDestination::senders(provider).is_static_file() {
debug!(target: "pruner", "Pruning transaction senders from static files.");
if self.mode.is_full() {
debug!(target: "pruner", "PruneMode::Full: deleting all transaction senders static files.");
return segments::delete_static_files_segment(
provider,
input,
StaticFileSegment::TransactionSenders,
)
}
return segments::prune_static_files(
provider,
input,

View File

@@ -37,14 +37,15 @@ use reth_primitives_traits::{
AlloyBlockHeader as _, BlockBody as _, RecoveredBlock, SealedHeader, SignedTransaction,
StorageEntry,
};
use reth_prune_types::PruneSegment;
use reth_stages_types::PipelineTarget;
use reth_static_file_types::{
find_fixed_range, HighestStaticFiles, SegmentHeader, SegmentRangeInclusive, StaticFileMap,
StaticFileSegment, DEFAULT_BLOCKS_PER_STATIC_FILE,
};
use reth_storage_api::{
BlockBodyIndicesProvider, ChangeSetReader, DBProvider, StorageChangeSetReader,
StorageSettingsCache,
BlockBodyIndicesProvider, ChangeSetReader, DBProvider, PruneCheckpointReader,
StorageChangeSetReader, StorageSettingsCache,
};
use reth_storage_errors::provider::{ProviderError, ProviderResult, StaticFileWriterError};
use std::{
@@ -985,6 +986,34 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
Ok(header)
}
/// Deletes ALL static file jars for the given segment, including the highest one.
///
/// CAUTION: destructive. Deletes all files on disk for this segment.
///
/// This is used for `PruneMode::Full` where all data should be removed.
///
/// Returns a list of `SegmentHeader`s from the deleted jars.
pub fn delete_segment(&self, segment: StaticFileSegment) -> ProviderResult<Vec<SegmentHeader>> {
let mut deleted_headers = Vec::new();
while let Some(block_height) = self.get_highest_static_file_block(segment) {
debug!(
target: "provider::static_file",
?segment,
?block_height,
"Deleting static file jar"
);
let header = self.delete_jar(segment, block_height).inspect_err(|err| {
warn!(target: "provider::static_file", ?segment, %block_height, ?err, "Failed to delete static file jar")
})?;
deleted_headers.push(header);
}
Ok(deleted_headers)
}
/// Given a segment and block range it returns a cached
/// [`StaticFileJarProvider`]. TODO(joshie): we should check the size and pop N if there's too
/// many.
@@ -1293,6 +1322,7 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
Provider: DBProvider
+ BlockReader
+ StageCheckpointReader
+ PruneCheckpointReader
+ ChainSpecProvider
+ StorageSettingsCache,
N: NodePrimitives<Receipt: Value, BlockHeader: Value, SignedTx: Value>,
@@ -1452,7 +1482,7 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
/// database checkpoints or prune against them.
pub fn check_file_consistency<Provider>(&self, provider: &Provider) -> ProviderResult<()>
where
Provider: DBProvider + ChainSpecProvider + StorageSettingsCache,
Provider: DBProvider + ChainSpecProvider + StorageSettingsCache + PruneCheckpointReader,
{
info!(target: "reth::cli", "Healing static file inconsistencies.");
@@ -1469,7 +1499,7 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
provider: &'a Provider,
) -> impl Iterator<Item = StaticFileSegment> + 'a
where
Provider: DBProvider + ChainSpecProvider + StorageSettingsCache,
Provider: DBProvider + ChainSpecProvider + StorageSettingsCache + PruneCheckpointReader,
{
StaticFileSegment::iter()
.filter(move |segment| self.should_check_segment(provider, *segment))
@@ -1481,7 +1511,7 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
segment: StaticFileSegment,
) -> bool
where
Provider: DBProvider + ChainSpecProvider + StorageSettingsCache,
Provider: DBProvider + ChainSpecProvider + StorageSettingsCache + PruneCheckpointReader,
{
match segment {
StaticFileSegment::Headers | StaticFileSegment::Transactions => true,
@@ -1506,7 +1536,17 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
true
}
StaticFileSegment::TransactionSenders => {
!EitherWriterDestination::senders(provider).is_database()
if EitherWriterDestination::senders(provider).is_database() {
debug!(target: "reth::providers::static_file", ?segment, "Skipping senders segment: senders stored in database");
return false;
}
if Self::is_segment_fully_pruned(provider, PruneSegment::SenderRecovery) {
debug!(target: "reth::providers::static_file", ?segment, "Skipping senders segment: fully pruned");
return false;
}
true
}
StaticFileSegment::AccountChangeSets => {
if EitherWriter::account_changesets_destination(provider).is_database() {
@@ -1525,6 +1565,20 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
}
}
/// Returns `true` if the given prune segment has a checkpoint with
/// [`reth_prune_types::PruneMode::Full`], indicating all data for this segment has been
/// intentionally deleted.
fn is_segment_fully_pruned<Provider>(provider: &Provider, segment: PruneSegment) -> bool
where
Provider: PruneCheckpointReader,
{
provider
.get_prune_checkpoint(segment)
.ok()
.flatten()
.is_some_and(|checkpoint| checkpoint.prune_mode.is_full())
}
/// Checks consistency of the latest static file segment and throws an error if at fault.
/// Read-only.
pub fn check_segment_consistency(&self, segment: StaticFileSegment) -> ProviderResult<()> {