use std::collections::HashMap;
use std::future::Future;
use std::io;
use std::path::{Path as StdPath, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use bytes::Bytes;
use futures::future;
use futures::stream::{self, StreamExt};
use log::warn;
use object_store::path::Path;
use object_store::{
GetOptions, GetResult, GetResultPayload, ObjectStore, ObjectStoreExt, PutPayload,
};
use parking_lot::Mutex;
use slatedb_common::clock::SystemClock;
use tokio::runtime::Handle;
use tokio_util::sync::CancellationToken;
use crate::download::Downloads;
use crate::error::MIRROR_STORE_NAME;
use crate::layout::{LocalName, LocalObject, PrefixMap};
use crate::ordering::{OpKind, PathOrdering};
use crate::retry::Retry;
use crate::{MirrorError, MirrorPolicy};
use slatedb_common::vfs::{Vfs, VfsLock};
pub(crate) fn local_error(source: io::Error) -> MirrorError {
MirrorError::Local { source }
}
pub(crate) async fn remove_quietly(vfs: &dyn Vfs, path: &StdPath) {
match vfs.remove(path).await {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => warn!(
"failed to remove mirror file [path={}, error={}]",
path.display(),
err
),
}
}
pub(crate) struct Inner {
pub(crate) root: PathBuf,
pub(crate) remote: Arc<dyn ObjectStore>,
pub(crate) policy: Arc<dyn MirrorPolicy>,
pub(crate) vfs: Arc<dyn Vfs>,
pub(crate) clock: Arc<dyn SystemClock>,
pub(crate) retry: Retry,
pub(crate) runtime: Handle,
pub(crate) shutdown: CancellationToken,
pub(crate) entries: Mutex<HashMap<Path, Arc<LocalObject>>>,
pub(crate) prefixes: Mutex<PrefixMap>,
pub(crate) ordering: PathOrdering,
pub(crate) downloads: Downloads,
temp_counter: AtomicU64,
_lock: Box<dyn VfsLock>,
}
impl Inner {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
root: PathBuf,
remote: Arc<dyn ObjectStore>,
policy: Arc<dyn MirrorPolicy>,
vfs: Arc<dyn Vfs>,
clock: Arc<dyn SystemClock>,
retry: Retry,
runtime: Handle,
entries: HashMap<Path, Arc<LocalObject>>,
prefixes: PrefixMap,
download_concurrency: usize,
lock: Box<dyn VfsLock>,
) -> Self {
Self {
root,
remote,
policy,
vfs,
clock,
retry,
runtime,
shutdown: CancellationToken::new(),
entries: Mutex::new(entries),
prefixes: Mutex::new(prefixes),
ordering: PathOrdering::default(),
downloads: Downloads::new(download_concurrency),
temp_counter: AtomicU64::new(0),
_lock: lock,
}
}
pub(crate) fn file(&self, name: &str) -> PathBuf {
self.root.join(name)
}
pub(crate) fn local(&self, path: &Path) -> Option<Arc<LocalObject>> {
self.entries.lock().get(path).cloned()
}
pub(crate) fn temp_file(&self, name: &LocalName) -> TempFile {
self.temp(name.temp(self.temp_counter.fetch_add(1, Ordering::Relaxed)))
}
fn temp(&self, file_name: String) -> TempFile {
TempFile {
vfs: Arc::clone(&self.vfs),
runtime: self.runtime.clone(),
path: self.file(&file_name),
armed: true,
}
}
pub(crate) fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let shutdown = self.shutdown.clone();
self.runtime.spawn(async move {
tokio::select! {
biased;
_ = shutdown.cancelled() => {}
_ = future => {}
}
});
}
pub(crate) async fn write_payload(
&self,
path: &StdPath,
payload: &PutPayload,
) -> io::Result<()> {
let mut writer = self.vfs.create(path).await?;
for chunk in payload {
writer.write(chunk.clone()).await?;
}
writer.finish().await
}
pub(crate) async fn install(
&self,
path: &Path,
name: &LocalName,
temp: TempFile,
object: LocalObject,
) -> Result<Arc<LocalObject>, MirrorError> {
let result = self.install_files(name, &temp, &object).await;
if let Err(err) = result {
temp.remove().await;
return Err(err);
}
temp.disarm();
let object = Arc::new(object);
self.entries
.lock()
.insert(path.clone(), Arc::clone(&object));
Ok(object)
}
async fn install_files(
&self,
name: &LocalName,
temp: &TempFile,
object: &LocalObject,
) -> Result<(), MirrorError> {
self.prefixes.lock().check_or_insert(name)?;
let meta = PutPayload::from(Bytes::from(object.to_meta_bytes()?));
let meta_temp =
self.temp(name.meta_temp(self.temp_counter.fetch_add(1, Ordering::Relaxed)));
let written = match self.write_payload(&meta_temp.path, &meta).await {
Ok(()) => {
self.vfs
.rename(&meta_temp.path, &self.file(&name.meta()))
.await
}
Err(err) => Err(err),
};
if let Err(err) = written {
meta_temp.remove().await;
return Err(local_error(err));
}
meta_temp.disarm();
self.vfs
.rename(&temp.path, &self.file(&name.object_file_name))
.await
.map_err(local_error)
}
pub(crate) async fn remove_local(&self, path: &Path) -> Result<(), MirrorError> {
if self.entries.lock().remove(path).is_none() {
return Ok(());
}
self.remove_files(path).await
}
async fn remove_files(&self, path: &Path) -> Result<(), MirrorError> {
let name = LocalName::new(path);
for file in [&name.object_file_name, &name.meta()] {
match self.vfs.remove(&self.file(file)).await {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => return Err(local_error(err)),
}
}
Ok(())
}
pub(crate) async fn delete(&self, path: &Path) -> object_store::Result<()> {
let ticket = self
.ordering
.register(path, OpKind::Delete)
.expect("deletes are always queued");
ticket.ready().await;
let (remote, local) = future::join(self.remote.delete(path), self.remove_local(path)).await;
drop(ticket);
remote?;
local?;
Ok(())
}
pub(crate) async fn purge_local(&self, path: &Path) -> Result<(), MirrorError> {
let ticket = self
.ordering
.register(path, OpKind::Delete)
.expect("deletes are always queued");
ticket.ready().await;
self.entries.lock().remove(path);
self.remove_files(path).await
}
pub(crate) async fn read_local(
&self,
path: &Path,
object: &LocalObject,
options: &GetOptions,
) -> object_store::Result<GetResult> {
let meta = &object.meta;
if options
.version
.as_ref()
.is_some_and(|version| meta.version.as_ref() != Some(version))
{
return Err(MirrorError::NotLocal { path: path.clone() }.into());
}
options.check_preconditions(meta)?;
let (range, bytes) = if options.head {
(0..meta.size, Bytes::new())
} else {
let range =
match &options.range {
Some(range) => range.as_range(meta.size).map_err(|source| {
object_store::Error::Generic {
store: MIRROR_STORE_NAME,
source: Box::new(source),
}
})?,
None => 0..meta.size,
};
let bytes = if range.is_empty() {
Bytes::new()
} else {
let name = LocalName::new(path);
self.vfs
.read_range(&self.file(&name.object_file_name), range.clone())
.await
.map_err(|err| match err.kind() {
io::ErrorKind::NotFound => MirrorError::NotLocal { path: path.clone() },
_ => local_error(err),
})?
};
(range, bytes)
};
Ok(GetResult {
payload: GetResultPayload::Stream(stream::once(async move { Ok(bytes) }).boxed()),
meta: meta.clone(),
range,
attributes: object.attributes.clone(),
extensions: Default::default(),
})
}
}
#[derive(Debug)]
pub(crate) struct TempFile {
vfs: Arc<dyn Vfs>,
runtime: Handle,
pub(crate) path: PathBuf,
armed: bool,
}
impl TempFile {
pub(crate) async fn remove(mut self) {
self.armed = false;
remove_quietly(self.vfs.as_ref(), &self.path).await;
}
pub(crate) fn disarm(mut self) {
self.armed = false;
}
}
impl Drop for TempFile {
fn drop(&mut self) {
if self.armed {
let vfs = Arc::clone(&self.vfs);
let path = std::mem::take(&mut self.path);
self.runtime
.spawn(async move { remove_quietly(vfs.as_ref(), &path).await });
}
}
}