use super::{Choice, CompactionStrategy};
use crate::{
HashMap, HashSet, KvPair, TableId, compaction::state::CompactionState, config::Config,
table::Table, time::now_on, version::Version, vlog::BlobFileId,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[doc(hidden)]
pub const NAME: &str = "FifoCompaction";
#[derive(Clone)]
pub struct Strategy {
pub limit: u64,
pub ttl_seconds: Option<u64>,
}
impl Strategy {
#[must_use]
pub fn new(limit: u64, ttl_seconds: Option<u64>) -> Self {
Self { limit, ttl_seconds }
}
}
impl CompactionStrategy for Strategy {
fn get_name(&self) -> &'static str {
NAME
}
fn get_config(&self) -> Vec<KvPair> {
vec![
(
crate::UserKey::from("fifo_limit"),
crate::UserValue::from(self.limit.to_le_bytes()),
),
(
crate::UserKey::from("fifo_ttl"),
crate::UserValue::from(if self.ttl_seconds.is_some() {
[1u8]
} else {
[0u8]
}),
),
(
crate::UserKey::from("fifo_ttl_seconds"),
crate::UserValue::from(self.ttl_seconds.map(u64::to_le_bytes).unwrap_or_default()),
),
]
}
fn choose(&self, version: &Version, config: &Config, state: &CompactionState) -> Choice {
if version.iter_tables().next().is_none() {
return Choice::DoNothing;
}
let db_size = version.iter_tables().map(Table::file_size).sum::<u64>()
+ version.blob_files.on_disk_size();
let hidden = state.hidden_set();
let mut ids_to_drop: HashSet<_> = HashSet::default();
let now = now_on(config.clock.as_ref());
let ttl_cutoff = match self.ttl_seconds {
Some(s) if s > 0 && !now.is_zero() => Some(
now.as_nanos()
.saturating_sub(u128::from(s) * 1_000_000_000u128),
),
_ => None,
};
let mut blob_credit = BlobCredit::new(version, state);
let mut ttl_dropped_bytes = 0u64;
let mut alive = Vec::new();
for table in version.iter_tables() {
let held = hidden.is_hidden(table.id());
let age = u128::from(table.metadata.created_at);
let expired = !held && age != 0 && ttl_cutoff.is_some_and(|cutoff| age <= cutoff);
if expired {
ids_to_drop.insert(table.id());
let Some(freed) = blob_credit.drop_table(table.id()) else {
return Choice::DoNothing;
};
ttl_dropped_bytes += table.file_size() + freed;
} else {
alive.push((table, held));
}
}
let size_after_ttl = db_size.saturating_sub(ttl_dropped_bytes);
if size_after_ttl > self.limit {
let overshoot = size_after_ttl - self.limit;
let mut collected_bytes = 0u64;
alive.sort_by_key(|(t, _)| (t.metadata.created_at, t.get_highest_seqno(), t.id()));
for (table, held) in alive {
if collected_bytes >= overshoot {
break;
}
if held {
break;
}
ids_to_drop.insert(table.id());
let Some(freed) = blob_credit.drop_table(table.id()) else {
return Choice::DoNothing;
};
collected_bytes += table.file_size() + freed;
}
}
if ids_to_drop.is_empty() {
Choice::DoNothing
} else {
Choice::Drop(ids_to_drop)
}
}
}
struct BlobCredit<'v> {
version: &'v Version,
state: &'v CompactionState,
refs: Option<BlobRefs>,
}
struct BlobRefs {
files_of: HashMap<TableId, Vec<BlobFileId>>,
refs: HashMap<BlobFileId, usize>,
}
impl BlobRefs {
fn read(version: &Version) -> Option<Self> {
let mut files_of = HashMap::default();
let mut refs: HashMap<BlobFileId, usize> = HashMap::default();
if version.blob_files.len() > 0 {
for table in version.iter_tables() {
let files: Vec<BlobFileId> = table
.blob_links()
.ok()?
.iter()
.map(|file| file.blob_file_id)
.collect();
for &file in &files {
*refs.entry(file).or_insert(0) += 1;
}
files_of.insert(table.id(), files);
}
}
Some(Self { files_of, refs })
}
}
impl<'v> BlobCredit<'v> {
fn new(version: &'v Version, state: &'v CompactionState) -> Self {
Self {
version,
state,
refs: None,
}
}
fn drop_table(&mut self, table: TableId) -> Option<u64> {
if self.refs.is_none() {
self.refs = Some(BlobRefs::read(self.version)?);
}
let refs = self.refs.as_mut()?;
let Some(files) = refs.files_of.remove(&table) else {
return Some(0);
};
let mut freed = 0;
for file in files {
let Some(count) = refs.refs.get_mut(&file) else {
continue;
};
debug_assert!(*count > 0, "blob file {file} released twice");
*count -= 1;
if *count == 0 && !self.state.memtable_references_blob_file(file) {
freed += self
.version
.blob_files
.get(file)
.map_or(0, |blob_file| blob_file.meta().total_compressed_bytes);
}
}
Some(freed)
}
}
#[cfg(test)]
mod tests;