use super::{block_path, filestem_to_block_cid};
use super::{BlockRm, BlockRmError, RepoCid};
use crate::error::Error;
use crate::repo::{BlockPut, BlockStore};
use crate::Block;
use async_trait::async_trait;
use cid::Cid;
use std::collections::HashMap;
use std::hash::Hash;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::sync::Mutex;
use tokio::fs;
use tokio::sync::broadcast;
use tracing_futures::Instrument;
type ArcMutexMap<A, B> = Arc<Mutex<HashMap<A, B>>>;
#[derive(Debug)]
pub struct FsBlockStore {
path: PathBuf,
writes: ArcMutexMap<RepoCid, broadcast::Sender<Result<(), ()>>>,
written_bytes: AtomicU64,
}
struct RemoveOnDrop<K: Eq + Hash, V>(ArcMutexMap<K, V>, Option<K>);
impl<K: Eq + Hash, V> Drop for RemoveOnDrop<K, V> {
fn drop(&mut self) {
if let Some(key) = self.1.take() {
let mut g = self.0.lock().unwrap();
g.remove(&key);
}
}
}
#[derive(Debug)]
enum WriteCompletion {
KnownGood,
NotObserved,
KnownBad,
NotOngoing,
}
impl FsBlockStore {
async fn write_completion(&self, cid: &Cid) -> WriteCompletion {
use std::collections::hash_map::Entry;
let mut rx = match self
.writes
.lock()
.expect("cannot support poisoned")
.entry(RepoCid(cid.clone()))
{
Entry::Occupied(oe) => oe.get().subscribe(),
Entry::Vacant(_) => return WriteCompletion::NotOngoing,
};
trace!("awaiting concurrent write to completion");
match rx.recv().await {
Ok(Ok(())) => WriteCompletion::KnownGood,
Err(broadcast::RecvError::Closed) => WriteCompletion::NotObserved,
Ok(Err(_)) => WriteCompletion::KnownBad,
Err(broadcast::RecvError::Lagged(_)) => {
unreachable!("sending at most one message to the channel with capacity of one")
}
}
}
}
#[async_trait]
impl BlockStore for FsBlockStore {
fn new(path: PathBuf) -> Self {
FsBlockStore {
path,
writes: Arc::new(Mutex::new(HashMap::with_capacity(8))),
written_bytes: Default::default(),
}
}
async fn init(&self) -> Result<(), Error> {
fs::create_dir_all(self.path.clone()).await?;
Ok(())
}
async fn open(&self) -> Result<(), Error> {
Ok(())
}
async fn contains(&self, cid: &Cid) -> Result<bool, Error> {
let path = block_path(self.path.clone(), cid);
let metadata = match fs::metadata(path).await {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e.into()),
};
Ok(metadata.is_file())
}
async fn get(&self, cid: &Cid) -> Result<Option<Block>, Error> {
let span = tracing::trace_span!("get block", cid = %cid);
async move {
if let WriteCompletion::KnownBad = self.write_completion(cid).await {
return Ok(None);
}
let path = block_path(self.path.clone(), cid);
let cid = cid.to_owned();
tokio::task::spawn_blocking(move || {
let mut file = match std::fs::File::open(path) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(e.into());
}
};
let len = file.metadata()?.len();
let mut data = Vec::with_capacity(len as usize);
file.read_to_end(&mut data)?;
let block = Block::new(data.into_boxed_slice(), cid);
Ok(Some(block))
})
.await?
}
.instrument(span)
.await
}
async fn put(&self, block: Block) -> Result<(Cid, BlockPut), Error> {
use std::collections::hash_map::Entry;
let span = tracing::trace_span!("put block", cid = %block.cid());
let target_path = block_path(self.path.clone(), &block.cid());
let cid = block.cid;
let data = block.data;
let inner_span = debug_span!(parent: &span, "blocking");
async move {
let (tx, mut rx) = {
let mut g = self.writes.lock().expect("cant support poisoned");
match g.entry(RepoCid(cid.to_owned())) {
Entry::Occupied(oe) => {
trace!("joining in on another already writing the block");
(oe.get().clone(), oe.get().subscribe())
}
Entry::Vacant(ve) => {
let (tx, rx) = broadcast::channel(1);
ve.insert(tx.clone());
(tx, rx)
}
}
};
let cleanup = RemoveOnDrop(self.writes.clone(), Some(RepoCid(cid.to_owned())));
let je = tokio::task::spawn_blocking(move || {
let _entered = inner_span.enter();
let sharded = target_path
.parent()
.expect("we already have at least the shard parent");
std::fs::create_dir_all(sharded)?;
let target = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&target_path)?;
let temp_path = target_path.with_extension("tmp");
match write_through_tempfile(target, &target_path, temp_path, &data) {
Ok(()) => {
trace!("successfully wrote the block");
Ok::<_, std::io::Error>(Ok(data.len()))
}
Err(e) => {
match std::fs::remove_file(&target_path) {
Ok(_) => debug!("removed partially written {:?}", target_path),
Err(removal) => warn!(
"failed to remove partially written {:?}: {}",
target_path, removal
),
}
Ok(Err(e))
}
}
})
.await;
drop(cleanup);
match je {
Ok(Ok(Ok(written))) => {
trace!(bytes = written, "block writing succeeded");
let _ = tx
.send(Ok(()))
.expect("this cannot fail as we have at least one receiver on stack");
drop(rx);
drop(tx);
self.written_bytes
.fetch_add(written as u64, Ordering::SeqCst);
Ok((cid, BlockPut::NewBlock))
}
Ok(Ok(Err(e))) => {
trace!("write failed but hopefully the target was removed");
let _ = tx
.send(Err(()))
.expect("this cannot fail as we have at least one receiver on the stack");
drop(rx);
drop(tx);
Err(Error::new(e))
}
Ok(Err(e)) => {
trace!("lost block writing race: {}", e);
drop(tx);
let message = match rx.recv().await {
Ok(message) => {
trace!("synchronized with writer, write outcome: {:?}", message);
message
}
Err(broadcast::RecvError::Closed) => {
Ok(())
}
Err(broadcast::RecvError::Lagged(_)) => {
unreachable!("broadcast channel should only be messaged once here")
}
};
drop(rx);
if message.is_err() {
Err(anyhow::anyhow!("other concurrent write failed"))
} else {
Ok((cid.to_owned(), BlockPut::Existed))
}
}
Err(e) if e.is_cancelled() => {
trace!("runtime is shutting down: {}", e);
Err(e.into())
}
Err(e) => {
error!("blocking put task panicked or something else: {}", e);
Err(e.into())
}
}
}
.instrument(span)
.await
}
async fn remove(&self, cid: &Cid) -> Result<Result<BlockRm, BlockRmError>, Error> {
let path = block_path(self.path.clone(), cid);
let span = trace_span!("remove block", cid = %cid);
match self.write_completion(cid).instrument(span).await {
WriteCompletion::KnownBad => Ok(Err(BlockRmError::NotFound(cid.to_owned()))),
completion => {
trace!(cid = %cid, completion = ?completion, "removing block after synchronizing");
match fs::remove_file(path).await {
Ok(()) => Ok(Ok(BlockRm::Removed(cid.to_owned()))),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(Err(BlockRmError::NotFound(cid.to_owned())))
}
Err(e) => Err(e.into()),
}
}
}
}
async fn list(&self) -> Result<Vec<Cid>, Error> {
use futures::future::{ready, Either};
use futures::stream::{empty, TryStreamExt};
let span = tracing::trace_span!("listing blocks");
async move {
let stream = fs::read_dir(self.path.clone()).await?;
let vec = stream
.and_then(|d| async move {
Ok(if d.file_type().await?.is_dir() {
Either::Left(fs::read_dir(d.path()).await?)
} else {
Either::Right(empty())
})
})
.try_flatten()
.try_filter_map(|d| {
let name = d.file_name();
let path: &std::path::Path = name.as_ref();
ready(if path.extension() != Some("data".as_ref()) {
Ok(None)
} else {
let maybe_cid = filestem_to_block_cid(path.file_stem());
Ok(maybe_cid)
})
})
.try_collect::<Vec<_>>()
.await?;
Ok(vec)
}
.instrument(span)
.await
}
async fn wipe(&self) {
unimplemented!("wipe")
}
}
fn write_through_tempfile(
target: std::fs::File,
target_path: impl AsRef<std::path::Path>,
temp_path: impl AsRef<std::path::Path>,
data: &[u8],
) -> Result<(), std::io::Error> {
use std::io::Write;
let mut temp = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_path)?;
temp.write_all(&*data)?;
temp.flush()?;
temp.sync_all()?;
drop(temp);
drop(target);
std::fs::rename(temp_path, target_path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Block;
use cid::{Cid, Codec};
use hex_literal::hex;
use multihash::Sha2_256;
use std::convert::TryFrom;
use std::env::temp_dir;
use std::sync::Arc;
#[tokio::test(max_threads = 1)]
async fn test_fs_blockstore() {
let mut tmp = temp_dir();
tmp.push("blockstore1");
std::fs::remove_dir_all(tmp.clone()).ok();
let store = FsBlockStore::new(tmp.clone());
let data = b"1".to_vec().into_boxed_slice();
let cid = Cid::new_v1(Codec::Raw, Sha2_256::digest(&data));
let block = Block::new(data, cid.clone());
store.init().await.unwrap();
store.open().await.unwrap();
let contains = store.contains(&cid).await.unwrap();
assert_eq!(contains, false);
let get = store.get(&cid).await.unwrap();
assert_eq!(get, None);
if store.remove(&cid).await.unwrap().is_ok() {
panic!("block should not be found")
}
let put = store.put(block.clone()).await.unwrap();
assert_eq!(put.0, cid.to_owned());
let contains = store.contains(&cid);
assert_eq!(contains.await.unwrap(), true);
let get = store.get(&cid);
assert_eq!(get.await.unwrap(), Some(block.clone()));
store.remove(&cid).await.unwrap().unwrap();
let contains = store.contains(&cid);
assert_eq!(contains.await.unwrap(), false);
let get = store.get(&cid);
assert_eq!(get.await.unwrap(), None);
std::fs::remove_dir_all(tmp).ok();
}
#[tokio::test(max_threads = 1)]
async fn test_fs_blockstore_open() {
let mut tmp = temp_dir();
tmp.push("blockstore2");
std::fs::remove_dir_all(&tmp).ok();
let data = b"1".to_vec().into_boxed_slice();
let cid = Cid::new_v1(Codec::Raw, Sha2_256::digest(&data));
let block = Block::new(data, cid);
let block_store = FsBlockStore::new(tmp.clone());
block_store.init().await.unwrap();
block_store.open().await.unwrap();
assert!(!block_store.contains(block.cid()).await.unwrap());
block_store.put(block.clone()).await.unwrap();
let block_store = FsBlockStore::new(tmp.clone());
block_store.open().await.unwrap();
assert!(block_store.contains(block.cid()).await.unwrap());
assert_eq!(block_store.get(block.cid()).await.unwrap().unwrap(), block);
std::fs::remove_dir_all(&tmp).ok();
}
#[tokio::test(max_threads = 1)]
async fn test_fs_blockstore_list() {
let mut tmp = temp_dir();
tmp.push("blockstore_list");
std::fs::remove_dir_all(&tmp).ok();
let block_store = FsBlockStore::new(tmp.clone());
block_store.init().await.unwrap();
block_store.open().await.unwrap();
for data in &[b"1", b"2", b"3"] {
let data_slice = data.to_vec().into_boxed_slice();
let cid = Cid::new_v1(Codec::Raw, Sha2_256::digest(&data_slice));
let block = Block::new(data_slice, cid);
block_store.put(block.clone()).await.unwrap();
}
let cids = block_store.list().await.unwrap();
assert_eq!(cids.len(), 3);
for cid in cids.iter() {
assert!(block_store.contains(cid).await.unwrap());
}
}
#[tokio::test(max_threads = 1)]
async fn race_to_insert_new() {
let mut tmp = temp_dir();
tmp.push("race_to_insert_new");
std::fs::remove_dir_all(&tmp).ok();
let single = FsBlockStore::new(tmp.clone());
single.init().await.unwrap();
let single = Arc::new(single);
let cid = Cid::try_from("QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL").unwrap();
let data = hex!("0a0d08021207666f6f6261720a1807");
let block = Block {
cid,
data: data.into(),
};
let count = 10;
let (writes, existing) = race_to_insert_scenario(count, block, &single).await;
let single = Arc::try_unwrap(single).unwrap();
assert_eq!(single.written_bytes.into_inner(), 15);
assert_eq!(writes, 1);
assert_eq!(existing, count - 1);
}
#[tokio::test(max_threads = 1)]
async fn race_to_insert_with_existing() {
let mut tmp = temp_dir();
tmp.push("race_to_insert_existing");
std::fs::remove_dir_all(&tmp).ok();
let single = FsBlockStore::new(tmp.clone());
single.init().await.unwrap();
let single = Arc::new(single);
let cid = Cid::try_from("QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL").unwrap();
let data = hex!("0a0d08021207666f6f6261720a1807");
let block = Block {
cid,
data: data.into(),
};
single.put(block.clone()).await.unwrap();
assert_eq!(single.written_bytes.load(Ordering::SeqCst), 15);
let count = 10;
let (writes, existing) = race_to_insert_scenario(count, block, &single).await;
let single = Arc::try_unwrap(single).unwrap();
assert_eq!(single.written_bytes.into_inner(), 15);
assert_eq!(writes, 0);
assert_eq!(existing, count);
}
async fn race_to_insert_scenario(
count: usize,
block: Block,
blockstore: &Arc<FsBlockStore>,
) -> (usize, usize) {
let barrier = Arc::new(tokio::sync::Barrier::new(count));
let join_handles = (0..count)
.map(|_| {
tokio::spawn({
let bs = Arc::clone(&blockstore);
let barrier = Arc::clone(&barrier);
let block = block.clone();
async move {
barrier.wait().await;
bs.put(block).await
}
})
})
.collect::<Vec<_>>();
let mut writes = 0usize;
let mut existing = 0usize;
for jh in join_handles {
let res = jh.await;
match res {
Ok(Ok((_, BlockPut::NewBlock))) => writes += 1,
Ok(Ok((_, BlockPut::Existed))) => existing += 1,
Ok(Err(e)) => println!("joinhandle err: {}", e),
_ => unreachable!("join error"),
}
}
(writes, existing)
}
#[tokio::test(max_threads = 1)]
async fn remove() {
let mut tmp = temp_dir();
tmp.push("remove");
std::fs::remove_dir_all(&tmp).ok();
let single = FsBlockStore::new(tmp.clone());
single.init().await.unwrap();
let cid = Cid::try_from("QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL").unwrap();
let data = hex!("0a0d08021207666f6f6261720a1807");
let block = Block {
cid: cid.clone(),
data: data.into(),
};
assert_eq!(single.list().await.unwrap().len(), 0);
single.put(block).await.unwrap();
assert_eq!(single.list().await.unwrap()[0].hash(), cid.hash());
single.remove(&cid).await.unwrap().unwrap();
assert_eq!(single.list().await.unwrap().len(), 0);
}
}