use {
crate::{
blockstore::{
Blockstore, PurgeType,
column::{ColumnName, columns},
},
blockstore_options::BlockstoreCleanupStrategy,
},
crossbeam_channel::{Receiver, Sender, TrySendError, bounded},
solana_clock::Slot,
solana_measure::measure::Measure,
std::{
string::ToString,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread::{self, Builder, JoinHandle},
time::{Duration, Instant},
},
};
pub const DEFAULT_MAX_BLOCKSTORE_SHREDS: u64 = 400_000_000;
pub const DEFAULT_MIN_MAX_BLOCKSTORE_SHREDS: u64 = 100_000_000;
pub const LEGACY_DEFAULT_MAX_LEDGER_SHREDS: u64 = 200_000_000;
pub const LEGACY_DEFAULT_MIN_MAX_LEDGER_SHREDS: u64 = 50_000_000;
const DEFAULT_CLEANUP_SLOT_INTERVAL: u64 = 512;
const CHECK_FOR_CLEANUP_INTERVAL: Duration = Duration::from_secs(10);
pub struct BlockstoreCleanupService {
t_cleanup: JoinHandle<()>,
}
impl BlockstoreCleanupService {
pub fn new(
blockstore: Arc<Blockstore>,
cleanup_strategy: BlockstoreCleanupStrategy,
exit: Arc<AtomicBool>,
) -> Self {
let mut last_purge_slot = 0;
let mut last_check_time = Instant::now();
let t_cleanup = Builder::new()
.name("solBstoreClean".to_string())
.spawn(move || {
let (cleanup_request_sender, cleanup_request_receiver) = bounded(1);
blockstore.register_manual_purge_request_sender(cleanup_request_sender.clone());
info!(
"BlockstoreCleanupService has started with automatic cleanup strategy \
{cleanup_strategy:?}",
);
loop {
if exit.load(Ordering::Relaxed) {
break;
}
if last_check_time.elapsed() > CHECK_FOR_CLEANUP_INTERVAL {
Self::cleanup_ledger(
&blockstore,
&cleanup_request_sender,
&cleanup_request_receiver,
cleanup_strategy,
&mut last_purge_slot,
DEFAULT_CLEANUP_SLOT_INTERVAL,
);
last_check_time = Instant::now();
}
thread::sleep(Duration::from_secs(1));
}
info!("BlockstoreCleanupService has stopped");
})
.unwrap();
Self { t_cleanup }
}
fn maybe_generate_automatic_cleanup_request(
blockstore: &Blockstore,
cleanup_request_sender: &Sender<Slot>,
cleanup_strategy: BlockstoreCleanupStrategy,
last_purge_slot: &mut u64,
purge_interval: u64,
) {
if cleanup_request_sender.is_full() {
return;
}
let root = blockstore.max_root();
if root - *last_purge_slot <= purge_interval {
return;
}
*last_purge_slot = root;
info!("Looking for Blockstore data to cleanup, latest root: {root}");
let (num_data_shreds, num_coding_shreds) = {
let live_files = blockstore
.live_files_metadata()
.expect("Blockstore::live_files_metadata()");
let mut num_data_shreds = 0;
let mut num_coding_shreds = 0;
live_files
.iter()
.for_each(|file_meta| match file_meta.column_family_name.as_str() {
columns::ShredData::NAME | columns::AlternateShredData::NAME => {
num_data_shreds += file_meta.num_entries
}
columns::ShredCode::NAME => num_coding_shreds += file_meta.num_entries,
_ => {}
});
(num_data_shreds, num_coding_shreds)
};
let lowest_slot = blockstore.lowest_slot();
let highest_slot = blockstore
.highest_slot()
.expect("Blockstore::highest_slot()")
.unwrap_or(lowest_slot);
if highest_slot < lowest_slot {
error!(
"Skipping Blockstore cleanup: highest slot {highest_slot} < lowest slot \
{lowest_slot}",
);
return;
}
info!(
"Blockstore has {} total shreds in slots [{lowest_slot}, {highest_slot}]; \
{num_data_shreds} data shreds, {num_coding_shreds} coding shreds",
num_data_shreds + num_coding_shreds
);
let (num_shreds, max_num_shreds) = match cleanup_strategy {
BlockstoreCleanupStrategy::None => {
return;
}
BlockstoreCleanupStrategy::CountDataShreds(limit) => (num_data_shreds, limit),
BlockstoreCleanupStrategy::CountDataAndCodingShreds(limit) => {
(num_data_shreds + num_coding_shreds, limit)
}
};
if num_shreds <= max_num_shreds {
return;
}
let num_slots = highest_slot - lowest_slot + 1;
let mean_shreds_per_slot = num_shreds / num_slots;
let num_slots_to_clean = (num_shreds - max_num_shreds + mean_shreds_per_slot - 1)
.checked_div(mean_shreds_per_slot);
let Some(num_slots_to_clean) = num_slots_to_clean else {
error!("Skipping Blockstore automatic cleanup: calculated mean of 0 shreds per slot");
return;
};
let lowest_cleanup_slot =
std::cmp::min(lowest_slot + num_slots_to_clean, root).saturating_sub(1);
match cleanup_request_sender.try_send(lowest_cleanup_slot) {
Ok(()) => {}
Err(TrySendError::Full(_)) => {
info!("Dropping Blockstore automatic cleanup request: a pending request exists");
}
Err(TrySendError::Disconnected(_)) => {
unreachable!(
"Channel disconnected while this thread holds both ends of the channel"
);
}
};
}
pub fn cleanup_ledger(
blockstore: &Blockstore,
cleanup_request_sender: &Sender<Slot>,
cleanup_request_receiver: &Receiver<Slot>,
cleanup_strategy: BlockstoreCleanupStrategy,
last_purge_slot: &mut u64,
purge_interval: u64,
) {
Self::maybe_generate_automatic_cleanup_request(
blockstore,
cleanup_request_sender,
cleanup_strategy,
last_purge_slot,
purge_interval,
);
let lowest_cleanup_slot = cleanup_request_receiver.try_recv().ok();
if let Some(lowest_cleanup_slot) = lowest_cleanup_slot {
*blockstore.lowest_cleanup_slot.write().unwrap() = lowest_cleanup_slot;
let mut purge_time = Measure::start("purge_slots()");
let _ = blockstore
.purge_slots(0, lowest_cleanup_slot, PurgeType::CompactionFilter)
.inspect_err(|e| {
error!("Purge failed when cleaning ledger to {lowest_cleanup_slot}: {e:?}")
});
blockstore.set_max_expired_slot(lowest_cleanup_slot);
purge_time.stop();
info!("Cleaned up Blockstore data older than slot {lowest_cleanup_slot}. {purge_time}");
}
}
pub fn join(self) -> thread::Result<()> {
self.t_cleanup.join()
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
blockstore::make_many_slot_entries, blockstore_meta::BlockLocation,
get_tmp_ledger_path_auto_delete,
},
solana_hash::Hash,
};
fn flush_blockstore_contents_to_disk(blockstore: Blockstore) -> Blockstore {
let ledger_path = blockstore.ledger_path().clone();
drop(blockstore);
Blockstore::open(&ledger_path).unwrap()
}
#[test]
fn test_maybe_generate_automatic_cleanup_request() {
agave_logger::setup();
let ledger_path = get_tmp_ledger_path_auto_delete!();
let blockstore = Blockstore::open(ledger_path.path()).unwrap();
let (sender, receiver) = bounded(1);
let num_slots: u64 = 10;
let num_entries = 200;
let (shreds, _) = make_many_slot_entries(1, num_slots, num_entries);
shreds.iter().for_each(|shred| assert!(shred.is_data()));
let total_num_shreds = shreds.len() as u64;
let shreds_per_slot = (shreds.len() / num_slots as usize) as u64;
assert!(shreds_per_slot > 1);
blockstore.insert_shreds(shreds, false).unwrap();
let blockstore = Arc::new(flush_blockstore_contents_to_disk(blockstore));
let mut last_purge_slot = 0;
let purge_interval = 0;
let mut latest_root = 1;
blockstore.set_roots(std::iter::once(&latest_root)).unwrap();
let cleanup_strategy = BlockstoreCleanupStrategy::CountDataAndCodingShreds(1);
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert_eq!(receiver.try_recv().unwrap(), latest_root - 1);
assert_eq!(last_purge_slot, 1);
last_purge_slot = 0;
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert_eq!(receiver.try_recv().unwrap(), latest_root - 1);
assert_eq!(last_purge_slot, 1);
last_purge_slot = 0;
sender.try_send(100).unwrap();
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert_eq!(receiver.try_recv().unwrap(), 100);
assert!(receiver.is_empty());
let cleanup_strategy = BlockstoreCleanupStrategy::None;
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert!(receiver.is_empty());
let cleanup_strategy =
BlockstoreCleanupStrategy::CountDataAndCodingShreds(total_num_shreds + 1);
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert!(receiver.is_empty());
assert_eq!(last_purge_slot, 1);
last_purge_slot = 0;
let cleanup_strategy =
BlockstoreCleanupStrategy::CountDataAndCodingShreds(total_num_shreds - 1);
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert_eq!(receiver.try_recv().unwrap(), latest_root - 1);
assert_eq!(last_purge_slot, 1);
last_purge_slot = 0;
for slot in 1..=num_slots {
latest_root = slot;
blockstore.set_roots(std::iter::once(&latest_root)).unwrap();
let cleanup_strategy = BlockstoreCleanupStrategy::CountDataAndCodingShreds(0);
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert_eq!(receiver.try_recv().unwrap(), latest_root - 1);
}
}
impl Blockstore {
fn insert_raw_coding_shred(&self, slot: Slot, index: u64, shred: &[u8]) {
self.code_shred_cf.put_bytes((slot, index), shred).unwrap();
}
fn insert_raw_data_shred_at_location(
&self,
slot: Slot,
location: BlockLocation,
index: u64,
shred: &[u8],
) {
match location {
BlockLocation::Original => {
self.data_shred_cf.put_bytes((slot, index), shred).unwrap();
}
BlockLocation::Alternate { block_id } => {
self.alt_data_shred_cf
.put_bytes((slot, block_id, index), shred)
.unwrap();
}
}
}
}
#[test]
fn test_cleanup_counts_all_shred_types() {
let ledger_path = get_tmp_ledger_path_auto_delete!();
let blockstore = Blockstore::open(ledger_path.path()).unwrap();
let (sender, receiver) = bounded(1);
let shred = vec![7; 10];
let num_shreds_per_slot = 10;
let orig_location = BlockLocation::Original;
let alt_location = BlockLocation::Alternate {
block_id: Hash::new_unique(),
};
for index in 0..num_shreds_per_slot {
blockstore.insert_raw_data_shred_at_location(2, orig_location, index, &shred);
blockstore.insert_raw_coding_shred(4, index, &shred);
blockstore.insert_raw_data_shred_at_location(6, alt_location, index, &shred);
}
let blockstore = flush_blockstore_contents_to_disk(blockstore);
let mut last_purge_slot = 0;
let purge_interval = 0;
let latest_root = 10;
blockstore.set_roots(std::iter::once(&latest_root)).unwrap();
let limit = (num_shreds_per_slot * 3) - 1;
let cleanup_strategy = BlockstoreCleanupStrategy::CountDataAndCodingShreds(limit);
BlockstoreCleanupService::maybe_generate_automatic_cleanup_request(
&blockstore,
&sender,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert!(receiver.try_recv().is_ok());
}
#[test]
fn test_cleanup() {
agave_logger::setup();
let ledger_path = get_tmp_ledger_path_auto_delete!();
let blockstore = Blockstore::open(ledger_path.path()).unwrap();
let (sender, receiver) = bounded(1);
let (shreds, _) = make_many_slot_entries(0, 50, 5);
blockstore.insert_shreds(shreds, false).unwrap();
let blockstore = Arc::new(flush_blockstore_contents_to_disk(blockstore));
let root = 40;
blockstore.set_roots(std::iter::once(&root)).unwrap();
let mut last_purge_slot = 0;
let cleanup_strategy = BlockstoreCleanupStrategy::CountDataAndCodingShreds(5);
let purge_interval = 10;
BlockstoreCleanupService::cleanup_ledger(
&blockstore,
&sender,
&receiver,
cleanup_strategy,
&mut last_purge_slot,
purge_interval,
);
assert_eq!(last_purge_slot, root);
assert!(receiver.is_empty());
blockstore
.slot_meta_iterator(0)
.unwrap()
.for_each(|(slot, _)| assert!(slot >= 40));
}
}