mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-17 14:08:10 -07:00
530 lines
15 KiB
Rust
530 lines
15 KiB
Rust
use std::{
|
|
fs::{self, OpenOptions},
|
|
os::unix::io::AsRawFd,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
|
|
use brk_core::{Error, Result};
|
|
use libc::off_t;
|
|
use log::info;
|
|
use memmap2::{MmapMut, MmapOptions};
|
|
use parking_lot::{RwLock, RwLockReadGuard};
|
|
|
|
mod identifier;
|
|
mod layout;
|
|
mod reader;
|
|
mod region;
|
|
mod regions;
|
|
|
|
pub use identifier::*;
|
|
use layout::*;
|
|
use rayon::prelude::*;
|
|
pub use reader::*;
|
|
pub use region::*;
|
|
use regions::*;
|
|
|
|
pub const PAGE_SIZE: u64 = 4096;
|
|
pub const PAGE_SIZE_MINUS_1: u64 = PAGE_SIZE - 1;
|
|
|
|
#[derive(Debug)]
|
|
pub struct File {
|
|
path: PathBuf,
|
|
regions: RwLock<Regions>,
|
|
layout: RwLock<Layout>,
|
|
file: RwLock<fs::File>,
|
|
mmap: RwLock<MmapMut>,
|
|
}
|
|
|
|
impl File {
|
|
pub fn open(path: &Path) -> Result<Self> {
|
|
fs::create_dir_all(path)?;
|
|
|
|
let regions = Regions::open(path)?;
|
|
|
|
let layout = Layout::from(®ions);
|
|
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(false)
|
|
.open(Self::data_path_(path))?;
|
|
|
|
let mmap = Self::create_mmap(&file)?;
|
|
|
|
Ok(Self {
|
|
path: path.to_owned(),
|
|
file: RwLock::new(file),
|
|
mmap: RwLock::new(mmap),
|
|
regions: RwLock::new(regions),
|
|
layout: RwLock::new(layout),
|
|
})
|
|
}
|
|
|
|
pub fn file_len(&self) -> Result<u64> {
|
|
Ok(self.file.read().metadata()?.len())
|
|
}
|
|
|
|
pub fn set_min_len(&self, len: u64) -> Result<()> {
|
|
let len = Self::ceil_number_to_page_size_multiple(len);
|
|
|
|
let file_len = self.file_len()?;
|
|
if file_len < len {
|
|
let mut mmap = self.mmap.write();
|
|
let file = self.file.write();
|
|
file.set_len(len)?;
|
|
*mmap = Self::create_mmap(&file)?;
|
|
Ok(())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn set_min_regions(&self, regions: usize) -> Result<()> {
|
|
self.regions
|
|
.write()
|
|
.set_min_len((regions * SIZE_OF_REGION) as u64)?;
|
|
self.set_min_len(regions as u64 * PAGE_SIZE)
|
|
}
|
|
|
|
pub fn create_region_if_needed(&self, id: &str) -> Result<(usize, Arc<RwLock<Region>>)> {
|
|
let regions = self.regions.read();
|
|
if let Some(index) = regions.get_region_index_from_id(id) {
|
|
return Ok((index, regions.get_region_from_index(index).unwrap()));
|
|
}
|
|
drop(regions);
|
|
|
|
let mut regions = self.regions.write();
|
|
let mut layout = self.layout.write();
|
|
|
|
let start = if let Some(start) = layout.find_smallest_adequate_hole(PAGE_SIZE) {
|
|
layout.remove_or_compress_hole(start, PAGE_SIZE);
|
|
start
|
|
} else {
|
|
let start = layout
|
|
.get_last_region_index()
|
|
.map(|index| {
|
|
let region_opt = regions.get_region_from_index(index);
|
|
let region = region_opt.as_ref().unwrap().read();
|
|
region.start() + region.reserved()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let len = start + PAGE_SIZE;
|
|
|
|
self.set_min_len(len)?;
|
|
|
|
start
|
|
};
|
|
|
|
let (index, region) = regions.create_region(id.to_owned(), start)?;
|
|
|
|
layout.insert_region(start, index);
|
|
|
|
Ok((index, region))
|
|
}
|
|
|
|
pub fn get_region(&self, identifier: Identifier) -> Result<RwLockReadGuard<'static, Region>> {
|
|
let regions = self.regions.read();
|
|
let region_opt = regions.get_region(identifier);
|
|
let region_arc = region_opt.ok_or(Error::Str("Unknown region"))?;
|
|
let region = region_arc.read();
|
|
let region: RwLockReadGuard<'static, Region> = unsafe { std::mem::transmute(region) };
|
|
Ok(region)
|
|
}
|
|
|
|
pub fn create_region_reader<'a>(&'a self, identifier: Identifier) -> Result<Reader<'a>> {
|
|
let mmap: RwLockReadGuard<'a, MmapMut> = self.mmap.read();
|
|
let region = self.get_region(identifier)?;
|
|
Ok(Reader::new(mmap, region))
|
|
}
|
|
|
|
#[inline]
|
|
pub fn write_all_to_region(&self, identifier: Identifier, data: &[u8]) -> Result<()> {
|
|
self.write_all_to_region_at_(identifier, data, None)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn write_all_to_region_at(
|
|
&self,
|
|
identifier: Identifier,
|
|
data: &[u8],
|
|
at: u64,
|
|
) -> Result<()> {
|
|
self.write_all_to_region_at_(identifier, data, Some(at))
|
|
}
|
|
|
|
fn write_all_to_region_at_(
|
|
&self,
|
|
identifier: Identifier,
|
|
data: &[u8],
|
|
at: Option<u64>,
|
|
) -> Result<()> {
|
|
let regions = self.regions.read();
|
|
let Some(region_lock) = regions.get_region(identifier.clone()) else {
|
|
return Err(Error::Str("Unknown region"));
|
|
};
|
|
|
|
let region_index = regions.identifier_to_index(identifier).unwrap();
|
|
|
|
let region = region_lock.read();
|
|
let start = region.start();
|
|
let reserved = region.reserved();
|
|
let len = region.len();
|
|
drop(region);
|
|
|
|
let data_len = data.len() as u64;
|
|
let new_len = at.map_or(len + data_len, |at| (at + data_len).max(len));
|
|
|
|
if at.is_some_and(|at| at >= start + reserved) {
|
|
return Err(Error::Str("Invalid at parameter"));
|
|
}
|
|
|
|
// Write to reserved space if possible
|
|
if new_len <= reserved {
|
|
// println!(
|
|
// "Write to {region_index} reserved space at {}",
|
|
// start + at.unwrap_or(len)
|
|
// );
|
|
|
|
if at.is_none() {
|
|
self.write(start + len, data);
|
|
}
|
|
|
|
let mut region = region_lock.write();
|
|
if let Some(at) = at {
|
|
self.write(start + at, data);
|
|
}
|
|
if len != new_len {
|
|
region.set_len(new_len);
|
|
}
|
|
regions.write_to_mmap(®ion, region_index);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
assert!(new_len > reserved);
|
|
let mut new_reserved = reserved;
|
|
while new_len > new_reserved {
|
|
new_reserved *= 2;
|
|
}
|
|
assert!(new_len <= new_reserved);
|
|
let added_reserve = new_reserved - reserved;
|
|
|
|
let mut layout = self.layout.write();
|
|
|
|
// If is last continue writing
|
|
if layout.is_last_anything(region_index) {
|
|
// println!(
|
|
// "{region_index} Append to file at {}",
|
|
// start + at.unwrap_or(len)
|
|
// );
|
|
|
|
self.set_min_len(start + new_reserved)?;
|
|
let mut region = region_lock.write();
|
|
region.set_reserved(new_reserved);
|
|
drop(region);
|
|
drop(layout);
|
|
|
|
self.write(start + len, data);
|
|
|
|
let mut region = region_lock.write();
|
|
region.set_len(new_len);
|
|
regions.write_to_mmap(®ion, region_index);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
// Expand region to the right if gap is wide enough
|
|
let hole_start = start + reserved;
|
|
if layout
|
|
.get_hole(hole_start)
|
|
.is_some_and(|gap| gap >= added_reserve)
|
|
{
|
|
// println!("Expand {region_index} to hole");
|
|
|
|
layout.remove_or_compress_hole(hole_start, added_reserve);
|
|
let mut region = region_lock.write();
|
|
region.set_reserved(new_reserved);
|
|
drop(region);
|
|
drop(layout);
|
|
|
|
self.write(start + len, data);
|
|
|
|
let mut region = region_lock.write();
|
|
region.set_len(new_len);
|
|
regions.write_to_mmap(®ion, region_index);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
// Find hole big enough to move the region
|
|
if let Some(hole_start) = layout.find_smallest_adequate_hole(new_reserved) {
|
|
// println!("Move {region_index} to hole at {hole_start}");
|
|
|
|
layout.remove_or_compress_hole(hole_start, new_reserved);
|
|
drop(layout);
|
|
|
|
self.write(
|
|
hole_start,
|
|
&self.mmap.read()[start as usize..(start + len) as usize],
|
|
);
|
|
self.write(hole_start + len, data);
|
|
|
|
let mut region = region_lock.write();
|
|
let mut layout = self.layout.write();
|
|
layout.move_region(hole_start, region_index, ®ion)?;
|
|
drop(layout);
|
|
|
|
region.set_start(hole_start);
|
|
region.set_reserved(new_reserved);
|
|
region.set_len(new_len);
|
|
regions.write_to_mmap(®ion, region_index);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let new_start = layout.len(®ions);
|
|
// Write at the end
|
|
// println!(
|
|
// "Move {region_index} to the end, from {start}..{} to {new_start}..{}",
|
|
// start + reserved,
|
|
// new_start + new_reserved
|
|
// );
|
|
self.set_min_len(new_start + new_reserved)?;
|
|
layout.reserve(new_start, new_reserved);
|
|
drop(layout);
|
|
|
|
self.write(
|
|
new_start,
|
|
&self.mmap.read()[start as usize..(start + len) as usize],
|
|
);
|
|
self.write(new_start + len, data);
|
|
|
|
let mut region = region_lock.write();
|
|
let mut layout = self.layout.write();
|
|
layout.move_region(new_start, region_index, ®ion)?;
|
|
drop(layout);
|
|
|
|
region.set_start(new_start);
|
|
region.set_reserved(new_reserved);
|
|
region.set_len(new_len);
|
|
regions.write_to_mmap(®ion, region_index);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write(&self, at: u64, data: &[u8]) {
|
|
let mmap = self.mmap.read();
|
|
|
|
let data_len = data.len();
|
|
let start = at as usize;
|
|
let end = start + data_len;
|
|
|
|
if end > mmap.len() {
|
|
unreachable!("Trying to write beyond mmap")
|
|
}
|
|
|
|
let slice = unsafe { std::slice::from_raw_parts_mut(mmap.as_ptr() as *mut u8, mmap.len()) };
|
|
|
|
slice[start..end].copy_from_slice(data);
|
|
}
|
|
|
|
/// From relative to start
|
|
pub fn truncate_region(&self, identifier: Identifier, from: u64) -> Result<()> {
|
|
let Some(region) = self.regions.read().get_region(identifier) else {
|
|
return Err(Error::Str("Unknown region"));
|
|
};
|
|
let mut region_ = region.write();
|
|
let start = region_.start();
|
|
let len = region_.len();
|
|
let reserved = region_.reserved();
|
|
|
|
// dbg!(from, start);
|
|
|
|
if from == len {
|
|
return Ok(());
|
|
} else if from > len {
|
|
return Err(Error::Str("Truncating further than length"));
|
|
}
|
|
|
|
region_.set_len(from);
|
|
|
|
let end = start + reserved;
|
|
let start = Self::ceil_number_to_page_size_multiple(start + from);
|
|
if start > end {
|
|
unreachable!("Should not be possible");
|
|
} else if start < end {
|
|
self.punch_hole(start, end - start)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remove_region(&self, identifier: Identifier) -> Result<Option<Arc<RwLock<Region>>>> {
|
|
let mut regions = self.regions.write();
|
|
|
|
let mut layout = self.layout.write();
|
|
|
|
let index_opt = regions.identifier_to_index(identifier.clone());
|
|
|
|
let Some(region) = regions.remove_region(identifier)? else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let index = index_opt.unwrap();
|
|
|
|
let region_ = region.write();
|
|
|
|
layout.remove_region(index, ®ion_)?;
|
|
|
|
self.punch_hole(region_.start(), region_.reserved())?;
|
|
|
|
drop(region_);
|
|
|
|
Ok(Some(region))
|
|
}
|
|
|
|
fn create_mmap(file: &fs::File) -> Result<MmapMut> {
|
|
Ok(unsafe { MmapOptions::new().map_mut(file)? })
|
|
}
|
|
|
|
pub fn regions(&self) -> RwLockReadGuard<'_, Regions> {
|
|
self.regions.read()
|
|
}
|
|
|
|
pub fn layout(&self) -> RwLockReadGuard<'_, Layout> {
|
|
self.layout.read()
|
|
}
|
|
|
|
pub fn mmap(&self) -> RwLockReadGuard<'_, MmapMut> {
|
|
self.mmap.read()
|
|
}
|
|
|
|
fn ceil_number_to_page_size_multiple(num: u64) -> u64 {
|
|
(num + PAGE_SIZE_MINUS_1) & !PAGE_SIZE_MINUS_1
|
|
}
|
|
|
|
fn data_path(&self) -> PathBuf {
|
|
Self::data_path_(&self.path)
|
|
}
|
|
fn data_path_(path: &Path) -> PathBuf {
|
|
path.join("data")
|
|
}
|
|
|
|
pub fn disk_usage(&self) -> String {
|
|
let path = self.data_path();
|
|
|
|
let output = std::process::Command::new("du")
|
|
.arg("-h")
|
|
.arg(&path)
|
|
.output()
|
|
.expect("Failed to run du");
|
|
|
|
String::from_utf8_lossy(&output.stdout)
|
|
.replace(path.to_str().unwrap(), " ")
|
|
.trim()
|
|
.to_string()
|
|
}
|
|
|
|
pub fn flush(&self) -> Result<()> {
|
|
let mmap = self.mmap.read();
|
|
let regions = self.regions.read();
|
|
mmap.flush()?;
|
|
regions.flush()
|
|
}
|
|
|
|
pub fn punch_holes(&self) -> Result<()> {
|
|
let file = self.file.write();
|
|
let mmap = self.mmap.read();
|
|
let layout = self.layout.read();
|
|
Self::punch_holes_(&file, &mmap, &layout)
|
|
}
|
|
|
|
fn punch_holes_(file: &fs::File, mmap: &MmapMut, layout: &Layout) -> Result<()> {
|
|
layout
|
|
.start_to_hole()
|
|
.par_iter()
|
|
.try_for_each(|(&start, &hole)| -> Result<()> {
|
|
assert!(start % PAGE_SIZE == 0);
|
|
assert!(hole % PAGE_SIZE == 0);
|
|
let has_old_data =
|
|
mmap[start as usize] != 0 || mmap[(start + hole - PAGE_SIZE) as usize] != 0;
|
|
if has_old_data {
|
|
info!("Punching a hole of {hole} bytes at {start}...");
|
|
Self::punch_hole_(file, start, hole)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})
|
|
}
|
|
|
|
fn punch_hole(&self, start: u64, length: u64) -> Result<()> {
|
|
let file = self.file.write();
|
|
Self::punch_hole_impl(&file, start, length)
|
|
}
|
|
|
|
fn punch_hole_(file: &fs::File, start: u64, length: u64) -> Result<()> {
|
|
Self::punch_hole_impl(file, start, length)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn punch_hole_impl(file: &fs::File, start: u64, length: u64) -> Result<()> {
|
|
let fpunchhole = FPunchhole {
|
|
fp_flags: 0,
|
|
reserved: 0,
|
|
fp_offset: start as libc::off_t,
|
|
fp_length: length as libc::off_t,
|
|
};
|
|
|
|
let result = unsafe {
|
|
libc::fcntl(
|
|
file.as_raw_fd(),
|
|
libc::F_PUNCHHOLE,
|
|
&fpunchhole as *const FPunchhole,
|
|
)
|
|
};
|
|
|
|
if result == -1 {
|
|
let err = std::io::Error::last_os_error();
|
|
return Err(Error::String(format!("Failed to punch hole: {err}")));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn punch_hole_impl(file: &fs::File, start: u64, length: u64) -> Result<()> {
|
|
let result = unsafe {
|
|
libc::fallocate(
|
|
file.as_raw_fd(),
|
|
libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
|
|
start as libc::off_t,
|
|
length as libc::off_t,
|
|
)
|
|
};
|
|
|
|
if result == -1 {
|
|
let err = std::io::Error::last_os_error();
|
|
return Err(Error::String(format!("Failed to punch hole: {err}")));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
|
fn punch_hole_impl(_file: &fs::File, _start: u64, _length: u64) -> Result<()> {
|
|
Err(Error::String(
|
|
"Hole punching not supported on this platform".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct FPunchhole {
|
|
fp_flags: u32,
|
|
reserved: u32,
|
|
fp_offset: off_t,
|
|
fp_length: off_t,
|
|
}
|