use crate::error::{ClientResultExt, ZeroFsError};
use crate::failover::{
FailoverClient, MAX_ATTEMPTS, OP_TIMEOUT, RETRY_DELAY, is_transport_failure,
};
use crate::session::{FidGuard, Session};
use crate::types::{Metadata, OpenOptions, SetAttrs};
use arc_swap::ArcSwap;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct Bound {
session: Arc<Session>,
guard: FidGuard,
}
struct Reconnect {
fc: Arc<FailoverClient>,
path: PathBuf,
opts: OpenOptions,
relock: tokio::sync::Mutex<()>,
}
pub struct File {
bound: ArcSwap<Bound>,
reconnect: Option<Reconnect>,
closed: AtomicBool,
path: String,
}
impl std::fmt::Debug for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("File")
.field("path", &self.path)
.field("closed", &self.closed.load(Ordering::Relaxed))
.field("failover", &self.reconnect.is_some())
.finish_non_exhaustive()
}
}
impl File {
pub(crate) fn new(session: Arc<Session>, guard: FidGuard, path: String) -> Arc<Self> {
Arc::new(Self {
bound: ArcSwap::from_pointee(Bound { session, guard }),
reconnect: None,
closed: AtomicBool::new(false),
path,
})
}
pub(crate) fn new_failover(
fc: Arc<FailoverClient>,
session: Arc<Session>,
guard: FidGuard,
path: PathBuf,
reopen_opts: OpenOptions,
) -> Arc<Self> {
let display = crate::path::display(&path);
Arc::new(Self {
bound: ArcSwap::from_pointee(Bound { session, guard }),
reconnect: Some(Reconnect {
fc,
path,
opts: reopen_opts,
relock: tokio::sync::Mutex::new(()),
}),
closed: AtomicBool::new(false),
path: display,
})
}
async fn with_binding<T, F, Fut>(&self, op: F) -> Result<T, ZeroFsError>
where
F: Fn(Arc<Session>, u32) -> Fut,
Fut: Future<Output = Result<T, ZeroFsError>>,
{
if self.closed.load(Ordering::Acquire) {
return Err(ZeroFsError::Closed);
}
let Some(rc) = &self.reconnect else {
let b = self.bound.load_full();
return op(Arc::clone(&b.session), b.guard.fid()).await;
};
let mut last: Option<ZeroFsError> = None;
for _ in 0..MAX_ATTEMPTS {
if self.closed.load(Ordering::Acquire) {
return Err(ZeroFsError::Closed);
}
let b = self.bound.load_full();
match tokio::time::timeout(OP_TIMEOUT, op(Arc::clone(&b.session), b.guard.fid())).await
{
Ok(Ok(v)) => return Ok(v),
Ok(Err(e)) if is_transport_failure(&e) => {
last = Some(e);
self.rebind(rc, &b).await?;
tokio::time::sleep(RETRY_DELAY).await;
}
Ok(Err(e)) => return Err(e),
Err(_) => {
last = Some(ZeroFsError::ConnectFailed {
message: format!("{}: file op timed out; leader unreachable", self.path),
});
self.rebind(rc, &b).await?;
}
}
}
Err(last.unwrap_or(ZeroFsError::ConnectFailed {
message: format!("{}: file failover attempts exhausted", self.path),
}))
}
async fn rebind(&self, rc: &Reconnect, failed: &Arc<Bound>) -> Result<(), ZeroFsError> {
let _g = rc.relock.lock().await;
if !Arc::ptr_eq(&self.bound.load_full(), failed) {
return Ok(());
}
let (session, guard) = rc.fc.reopen_handle(&rc.path, &rc.opts).await?;
if let Some(token) = failed.session.client.unsynced_oldest(failed.guard.fid()) {
session.client.seed_unsynced(guard.fid(), token);
}
self.bound.store(Arc::new(Bound { session, guard }));
Ok(())
}
#[cfg(feature = "tokio-io")]
pub(crate) fn max_read_chunk(&self) -> u32 {
self.bound.load().session.client.max_io()
}
pub async fn read_at(&self, offset: u64, len: u32) -> Result<bytes::Bytes, ZeroFsError> {
let path = self.path.clone();
self.with_binding(move |session, fid| {
let path = path.clone();
async move { session.client.read_bytes(fid, offset, len).await.ctx(&path) }
})
.await
}
pub async fn write_at(&self, offset: u64, data: &[u8]) -> Result<(), ZeroFsError> {
let data = data.to_vec();
let path = self.path.clone();
self.with_binding(move |session, fid| {
let data = data.clone();
let path = path.clone();
async move { session.write_all(fid, offset, &data, &path).await }
})
.await
}
pub async fn metadata(&self) -> Result<Metadata, ZeroFsError> {
let path = self.path.clone();
let stat = self
.with_binding(move |session, fid| {
let path = path.clone();
async move { session.stat_fid(fid, &path).await }
})
.await?;
Ok(Metadata::from_stat(&stat))
}
pub async fn set_len(&self, size: u64) -> Result<(), ZeroFsError> {
self.set_attr(SetAttrs {
size: Some(size),
..Default::default()
})
.await?;
Ok(())
}
pub async fn set_attr(&self, attrs: SetAttrs) -> Result<Metadata, ZeroFsError> {
let path = self.path.clone();
let stat = self
.with_binding(move |session, fid| {
let path = path.clone();
async move { session.setattr_fid(fid, &attrs, &path).await }
})
.await?;
Ok(Metadata::from_stat(&stat))
}
pub async fn sync_all(&self) -> Result<(), ZeroFsError> {
let path = self.path.clone();
self.with_binding(move |session, fid| {
let path = path.clone();
async move { session.client.fsync(fid, 0).await.ctx(&path) }
})
.await
}
pub async fn sync_data(&self) -> Result<(), ZeroFsError> {
let path = self.path.clone();
self.with_binding(move |session, fid| {
let path = path.clone();
async move { session.client.fsync(fid, 1).await.ctx(&path) }
})
.await
}
pub async fn close(&self) {
self.closed.store(true, Ordering::Release);
}
#[cfg(feature = "tokio-io")]
pub fn cursor(self: &Arc<File>) -> crate::io::FileCursor {
crate::io::FileCursor::new(Arc::clone(self))
}
}