pub mod merge;
pub mod meta;
pub mod multi_writer;
pub mod reader;
pub mod scanner;
pub mod writer;
use crate::{
Checksum, GlobalTableId, TreeId, blob_tree::FragmentationMap, deletion_pause::DeletionPause,
file_accessor::FileAccessor, fs::Fs, vlog::BlobFileId,
};
pub use meta::Metadata;
use std::{
path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool},
};
pub struct Inner {
pub id: BlobFileId,
pub tree_id: TreeId,
pub path: PathBuf,
pub meta: Metadata,
pub is_deleted: AtomicBool,
pub checksum: Checksum,
pub(crate) file_accessor: FileAccessor,
pub(crate) fs: Arc<dyn Fs>,
pub(crate) deletion_pause: once_cell::race::OnceBox<Arc<DeletionPause>>,
#[cfg(feature = "std")]
pub(crate) background_deleter: once_cell::race::OnceBox<Arc<crate::BackgroundDeleter>>,
}
impl Inner {
fn global_id(&self) -> GlobalTableId {
GlobalTableId::from((self.tree_id, self.id))
}
}
impl core::fmt::Debug for Inner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("blob_file::Inner")
.field("id", &self.id)
.field("tree_id", &self.tree_id)
.field("path", &self.path)
.field(
"is_deleted",
&self.is_deleted.load(std::sync::atomic::Ordering::Relaxed),
)
.field("meta", &self.meta)
.finish_non_exhaustive()
}
}
impl Drop for Inner {
fn drop(&mut self) {
if self.is_deleted.load(std::sync::atomic::Ordering::Acquire) {
log::trace!(
"Cleanup deleted blob file {:?} at {}",
self.id,
self.path.display(),
);
let global_id = self.global_id();
let file_accessor = std::mem::replace(&mut self.file_accessor, FileAccessor::Closed);
file_accessor
.as_descriptor_table()
.inspect(|d| d.remove_for_blob_file(&global_id));
drop(file_accessor);
let deferred = match self.deletion_pause.get() {
Some(pause) if pause.is_active() => {
pause.try_enqueue(Arc::clone(&self.fs), self.path.clone())
}
_ => false,
};
if deferred {
log::trace!(
"Deferred deletion of blob file {:?} at {} (checkpoint active)",
self.id,
self.path.display(),
);
return;
}
#[cfg(feature = "std")]
if let Some(deleter) = self.background_deleter.get() {
if self.fs.hard_link_count(&self.path).is_ok_and(|n| n <= 1)
&& let Err(e) = self.fs.truncate_file(&self.path)
{
log::warn!(
"Failed to truncate deleted blob file {:?} at {}: {e:?}",
self.id,
self.path.display(),
);
}
deleter.enqueue(Arc::clone(&self.fs), self.path.clone());
return;
}
if let Err(e) = self.fs.remove_file(&self.path) {
log::warn!(
"Failed to cleanup deleted blob file {:?} at {}: {e:?}",
self.id,
self.path.display(),
);
}
}
}
}
#[derive(Clone)]
pub struct BlobFile(pub(crate) Arc<Inner>);
impl Eq for BlobFile {}
impl PartialEq for BlobFile {
fn eq(&self, other: &Self) -> bool {
self.id().eq(&other.id())
}
}
impl std::hash::Hash for BlobFile {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id().hash(state);
}
}
impl BlobFile {
pub(crate) fn mark_as_deleted(&self) {
self.0
.is_deleted
.store(true, std::sync::atomic::Ordering::Release);
}
pub(crate) fn install_deletion_pause(&self, pause: Arc<DeletionPause>) {
let _ = self.0.deletion_pause.set(Box::new(pause));
}
#[cfg(feature = "std")]
pub(crate) fn install_background_deleter(&self, deleter: Arc<crate::BackgroundDeleter>) {
let _ = self.0.background_deleter.set(Box::new(deleter));
}
#[must_use]
pub fn id(&self) -> BlobFileId {
self.0.id
}
#[must_use]
pub fn checksum(&self) -> Checksum {
self.0.checksum
}
#[must_use]
pub fn path(&self) -> &Path {
&self.0.path
}
#[must_use]
pub(crate) fn file_accessor(&self) -> &FileAccessor {
&self.0.file_accessor
}
#[must_use]
#[expect(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 {
self.0.meta.item_count
}
pub(crate) fn is_stale(&self, frag_map: &FragmentationMap, threshold: f32) -> bool {
frag_map.get(&self.id()).is_some_and(|x| {
#[expect(
clippy::cast_precision_loss,
reason = "ok to lose precision as this is an approximate calculation"
)]
let stale_bytes = x.bytes as f32;
#[expect(
clippy::cast_precision_loss,
reason = "ok to lose precision as this is an approximate calculation"
)]
let all_bytes = self.0.meta.total_uncompressed_bytes as f32;
let ratio = stale_bytes / all_bytes;
ratio >= threshold
})
}
pub(crate) fn is_dead(&self, frag_map: &FragmentationMap) -> bool {
frag_map.get(&self.id()).is_some_and(|x| {
let stale_bytes = x.bytes;
let all_bytes = self.0.meta.total_uncompressed_bytes;
stale_bytes == all_bytes
})
}
}