pub mod cache;
mod cidbytes;
mod db;
mod error;
#[cfg(test)]
mod tests;
mod transaction;
use cache::{CacheTracker, NoopCacheTracker};
use db::*;
pub use error::{BlockStoreError, Result};
use libipld::{cid::Cid, codec::References, store::StoreParams, Block, Ipld};
use parking_lot::Mutex;
use rusqlite::{Connection, DatabaseName, OpenFlags};
use std::{
fmt,
iter::FromIterator,
marker::PhantomData,
mem,
ops::DerefMut,
path::{Path, PathBuf},
sync::{
atomic::{AtomicI64, Ordering},
Arc,
},
time::Duration,
};
use tracing::*;
pub use transaction::Transaction;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum DbPath {
File(PathBuf),
Memory,
}
impl DbPath {
fn is_memory(&self) -> bool {
!matches!(self, DbPath::File(_))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SizeTargets {
pub count: u64,
pub size: u64,
}
impl SizeTargets {
pub fn new(count: u64, size: u64) -> Self {
Self { count, size }
}
pub fn exceeded(&self, stats: &StoreStats) -> bool {
stats.count > self.count || stats.size > self.size
}
pub fn max_value() -> Self {
Self {
count: u64::max_value(),
size: u64::max_value(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Synchronous {
Full,
Normal,
Off,
}
impl fmt::Display for Synchronous {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Synchronous::Full => "FULL",
Synchronous::Normal => "NORMAL",
Synchronous::Off => "OFF",
})
}
}
#[derive(Debug)]
pub struct Config {
size_targets: SizeTargets,
cache_tracker: Arc<dyn CacheTracker>,
pragma_synchronous: Synchronous,
pragma_cache_pages: u64,
read_only: bool,
create: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
size_targets: Default::default(),
cache_tracker: Arc::new(NoopCacheTracker),
pragma_synchronous: Synchronous::Full, pragma_cache_pages: 8192, read_only: false,
create: true,
}
}
}
impl Config {
pub fn with_read_only(mut self, value: bool) -> Self {
self.read_only = value;
self
}
pub fn with_size_targets(mut self, size_targets: SizeTargets) -> Self {
self.size_targets = size_targets;
self
}
pub fn with_cache_tracker<T: CacheTracker + 'static>(mut self, cache_tracker: T) -> Self {
self.cache_tracker = Arc::new(cache_tracker);
self
}
pub fn with_pragma_synchronous(mut self, value: Synchronous) -> Self {
self.pragma_synchronous = value;
self
}
pub fn with_pragma_cache_pages(mut self, value: u64) -> Self {
self.pragma_cache_pages = value;
self
}
}
pub struct BlockStore<S> {
conn: Connection,
expired_temp_pins: Arc<Mutex<Vec<i64>>>,
config: Config,
_s: PhantomData<S>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StoreStats {
count: u64,
size: u64,
}
impl StoreStats {
pub fn count(&self) -> u64 {
self.count
}
pub fn size(&self) -> u64 {
self.size
}
}
pub struct TempPin {
id: AtomicI64,
expired_temp_pins: Arc<Mutex<Vec<i64>>>,
}
impl fmt::Debug for TempPin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.id.load(Ordering::SeqCst);
let mut builder = f.debug_struct("TempAlias");
if id > 0 {
builder.field("id", &id);
}
builder.finish()
}
}
impl Drop for TempPin {
fn drop(&mut self) {
let id = self.id.get_mut();
let alias = *id;
if alias > 0 {
*id = 0;
self.expired_temp_pins.lock().push(alias);
}
}
}
impl<S> BlockStore<S>
where
S: StoreParams,
Ipld: References<S::Codecs>,
{
fn create_connection(db_path: DbPath, config: &Config) -> crate::Result<rusqlite::Connection> {
let mut flags = OpenFlags::SQLITE_OPEN_NO_MUTEX | OpenFlags::SQLITE_OPEN_URI;
flags |= if config.read_only {
OpenFlags::SQLITE_OPEN_READ_ONLY
} else {
OpenFlags::SQLITE_OPEN_READ_WRITE
};
if config.create && !config.read_only {
flags |= OpenFlags::SQLITE_OPEN_CREATE
}
let conn = match db_path {
DbPath::Memory => Connection::open_in_memory()?,
DbPath::File(path) => Connection::open_with_flags(path, flags)?,
};
Ok(conn)
}
pub fn open_path(db_path: DbPath, config: Config) -> crate::Result<Self> {
let is_memory = db_path.is_memory();
let mut conn = Self::create_connection(db_path, &config)?;
init_db(
&mut conn,
is_memory,
config.pragma_cache_pages as i64,
config.pragma_synchronous,
)?;
let ids = in_txn(&mut conn, |txn| get_ids(txn))?;
config.cache_tracker.retain_ids(&ids);
Ok(Self {
conn,
expired_temp_pins: Arc::new(Mutex::new(Vec::new())),
config,
_s: PhantomData,
})
}
pub fn memory(config: Config) -> crate::Result<Self> {
Self::open_path(DbPath::Memory, config)
}
pub fn open(path: impl AsRef<Path>, config: Config) -> crate::Result<Self> {
let mut pb: PathBuf = PathBuf::new();
pb.push(path);
Self::open_path(DbPath::File(pb), config)
}
pub fn open_test(path: impl AsRef<Path>, config: Config) -> crate::Result<Self> {
let mut conn = Self::create_connection(DbPath::Memory, &config)?;
debug!(
"Restoring in memory database from {}",
path.as_ref().display()
);
conn.restore(
DatabaseName::Main,
path,
Some(|p: rusqlite::backup::Progress| {
let percent = if p.pagecount == 0 {
100
} else {
(p.pagecount - p.remaining) * 100 / p.pagecount
};
if percent % 10 == 0 {
debug!("Restoring: {} %", percent);
}
}),
)?;
let ids = in_txn(&mut conn, |txn| get_ids(txn))?;
config.cache_tracker.retain_ids(&ids);
Ok(Self {
conn,
expired_temp_pins: Arc::new(Mutex::new(Vec::new())),
config,
_s: PhantomData,
})
}
pub fn flush(&self) -> crate::Result<()> {
Ok(self.conn.pragma_update(None, "wal_checkpoint", &"FULL")?)
}
pub fn integrity_check(&self) -> crate::Result<()> {
let result = integrity_check(&self.conn)?;
if result == vec!["ok".to_owned()] {
Ok(())
} else {
let error_text = result.join(";");
Err(crate::error::BlockStoreError::SqliteError(
rusqlite::Error::SqliteFailure(rusqlite::ffi::Error::new(11), Some(error_text)),
))
}
}
pub fn transaction(&mut self) -> Result<Transaction<'_, S>> {
Transaction::new(self)
}
pub fn temp_pin(&self) -> TempPin {
TempPin {
id: AtomicI64::new(0),
expired_temp_pins: self.expired_temp_pins.clone(),
}
}
pub fn alias(&mut self, name: impl AsRef<[u8]>, link: Option<&Cid>) -> crate::Result<()> {
let txn = self.transaction()?;
txn.alias(name, link)?;
txn.commit()
}
pub fn resolve(&mut self, name: impl AsRef<[u8]>) -> crate::Result<Option<Cid>> {
self.transaction()?.resolve(name)
}
pub fn extend_temp_pin(
&mut self,
pin: &TempPin,
links: impl IntoIterator<Item = Cid>,
) -> crate::Result<()> {
let txn = self.transaction()?;
for link in links {
txn.extend_temp_pin(pin, &link)?;
}
txn.commit()
}
pub fn reverse_alias(&mut self, cid: &Cid) -> crate::Result<Option<Vec<Vec<u8>>>> {
self.transaction()?.reverse_alias(cid)
}
pub fn has_cid(&mut self, cid: &Cid) -> Result<bool> {
self.transaction()?.has_cid(cid)
}
pub fn has_block(&mut self, cid: &Cid) -> Result<bool> {
self.transaction()?.has_block(cid)
}
pub fn get_store_stats(&mut self) -> Result<StoreStats> {
self.transaction()?.get_store_stats()
}
pub fn get_known_cids<C: FromIterator<Cid>>(&mut self) -> Result<C> {
self.transaction()?.get_known_cids()
}
pub fn get_block_cids<C: FromIterator<Cid>>(&mut self) -> Result<C> {
self.transaction()?.get_block_cids()
}
pub fn get_descendants<C: FromIterator<Cid>>(&mut self, cid: &Cid) -> Result<C> {
self.transaction()?.get_descendants(cid)
}
pub fn get_missing_blocks<C: FromIterator<Cid>>(&mut self, cid: &Cid) -> Result<C> {
self.transaction()?.get_missing_blocks(cid)
}
pub fn aliases<C: FromIterator<(Vec<u8>, Cid)>>(&mut self) -> Result<C> {
self.transaction()?.aliases()
}
pub fn put_blocks(
&mut self,
blocks: impl IntoIterator<Item = Block<S>>,
pin: Option<&TempPin>,
) -> Result<()> {
let txn = self.transaction()?;
for block in blocks {
txn.put_block(&block, pin)?;
}
txn.commit()
}
pub fn put_block(&mut self, block: &Block<S>, pin: Option<&TempPin>) -> Result<()> {
let txn = self.transaction()?;
txn.put_block(block, pin)?;
txn.commit()
}
pub fn get_block(&mut self, cid: &Cid) -> Result<Option<Vec<u8>>> {
self.transaction()?.get_block(cid)
}
pub fn vacuum(&mut self) -> Result<()> {
vacuum(&self.conn)
}
pub fn gc(&mut self) -> Result<()> {
loop {
let complete = self.incremental_gc(20000, Duration::from_secs(1))?;
while !self.incremental_delete_orphaned(20000, Duration::from_secs(1))? {}
if complete {
break;
}
}
Ok(())
}
pub fn incremental_gc(&mut self, min_blocks: usize, max_duration: Duration) -> Result<bool> {
let expired_temp_pins = {
let mut result = Vec::new();
mem::swap(self.expired_temp_pins.lock().deref_mut(), &mut result);
result
};
let (deleted, complete) = log_execution_time("gc", Duration::from_secs(1), || {
let size_targets = self.config.size_targets;
let cache_tracker = &self.config.cache_tracker;
in_txn(&mut self.conn, move |txn| {
for id in expired_temp_pins {
delete_temp_pin(txn, id)?;
}
incremental_gc(&txn, min_blocks, max_duration, size_targets, cache_tracker)
})
})?;
self.config.cache_tracker.blocks_deleted(deleted);
Ok(complete)
}
pub fn incremental_delete_orphaned(
&mut self,
min_blocks: usize,
max_duration: Duration,
) -> Result<bool> {
log_execution_time("delete_orphaned", Duration::from_millis(100), || {
in_txn(&mut self.conn, move |txn| {
Ok(incremental_delete_orphaned(txn, min_blocks, max_duration)?)
})
})
}
}