use crate::file::File;
use crate::session::{FidGuard, Session};
use crate::types::OpenOptions;
use crate::{Client, Metadata, NodeKind, ZeroFsError};
use arc_swap::ArcSwapOption;
use bytes::Bytes;
use std::future::Future;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
fn new_op_id() -> [u8; 16] {
use rand::RngCore;
let mut id = [0u8; 16];
rand::thread_rng().fill_bytes(&mut id);
id
}
pub(crate) const MAX_ATTEMPTS: usize = 60;
pub(crate) const RETRY_DELAY: Duration = Duration::from_millis(500);
const PROBE_CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
const PROBE_OP_TIMEOUT: Duration = Duration::from_secs(3);
pub(crate) const OP_TIMEOUT: Duration = Duration::from_secs(8);
pub struct FailoverClient {
targets: Vec<String>,
current: ArcSwapOption<Client>,
}
pub(crate) fn is_transport_failure(e: &ZeroFsError) -> bool {
match e {
ZeroFsError::NotLeader { .. }
| ZeroFsError::ConnectFailed { .. }
| ZeroFsError::Closed
| ZeroFsError::Protocol { .. } => true,
ZeroFsError::Io { errno, .. } => *errno == libc::EIO,
_ => false,
}
}
impl FailoverClient {
pub async fn connect(targets: Vec<String>) -> Result<Arc<Self>, ZeroFsError> {
if targets.is_empty() {
return Err(ZeroFsError::ConnectFailed {
message: "no targets given".into(),
});
}
let fc = Arc::new(Self {
targets,
current: ArcSwapOption::empty(),
});
for _ in 0..MAX_ATTEMPTS {
if fc.obtain().await.is_some() {
return Ok(fc);
}
tokio::time::sleep(RETRY_DELAY).await;
}
Err(ZeroFsError::ConnectFailed {
message: "no serving leader found among targets".into(),
})
}
fn cached(&self) -> Option<Arc<Client>> {
self.current.load_full()
}
fn invalidate(&self) {
self.current.store(None);
}
async fn obtain(&self) -> Option<Arc<Client>> {
if let Some(c) = self.cached() {
return Some(c);
}
let client = self.probe().await?;
self.current.store(Some(client.clone()));
Some(client)
}
async fn probe(&self) -> Option<Arc<Client>> {
for target in &self.targets {
let connected =
tokio::time::timeout(PROBE_CONNECT_TIMEOUT, Client::connect(target)).await;
let Ok(Ok(client)) = connected else {
continue;
};
let healthy = matches!(
tokio::time::timeout(PROBE_OP_TIMEOUT, client.stat("/")).await,
Ok(Ok(_))
);
if healthy {
return Some(client);
}
client.close().await;
}
None
}
async fn with_failover<T, F, Fut>(&self, op: F) -> Result<T, ZeroFsError>
where
F: Fn(Arc<Client>) -> Fut,
Fut: Future<Output = Result<T, ZeroFsError>>,
{
let mut last = None;
for _ in 0..MAX_ATTEMPTS {
let Some(client) = self.obtain().await else {
tokio::time::sleep(RETRY_DELAY).await;
continue;
};
match tokio::time::timeout(OP_TIMEOUT, op(client)).await {
Ok(Ok(v)) => return Ok(v),
Ok(Err(e)) if is_transport_failure(&e) => {
self.invalidate();
last = Some(e);
tokio::time::sleep(RETRY_DELAY).await;
}
Ok(Err(e)) => return Err(e),
Err(_) => {
self.invalidate();
last = Some(ZeroFsError::ConnectFailed {
message: "op timed out; leader unreachable".into(),
});
}
}
}
Err(last.unwrap_or(ZeroFsError::ConnectFailed {
message: "failover attempts exhausted".into(),
}))
}
pub(crate) async fn reopen_handle(
&self,
path: &Path,
opts: &OpenOptions,
) -> Result<(Arc<Session>, FidGuard), ZeroFsError> {
self.invalidate();
let path = path.to_path_buf();
let opts = *opts;
self.with_failover(move |c| {
let path = path.clone();
async move { c.open_guard(&path, &opts).await }
})
.await
}
pub async fn read(&self, path: impl AsRef<Path>) -> Result<Bytes, ZeroFsError> {
let path = path.as_ref().to_path_buf();
self.with_failover(move |c| {
let path = path.clone();
async move { c.read(path).await }
})
.await
}
pub async fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<(), ZeroFsError> {
let path = path.as_ref().to_path_buf();
let data = data.to_vec();
self.with_failover(move |c| {
let path = path.clone();
let data = data.clone();
async move { c.write(path, &data).await }
})
.await
}
pub async fn create_dir(
&self,
path: impl AsRef<Path>,
mode: u32,
) -> Result<Metadata, ZeroFsError> {
let path = path.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let path = path.clone();
async move { c.create_dir_op_id(path, mode, op_id).await }
})
.await
}
pub async fn create(
self: &Arc<Self>,
path: impl AsRef<Path>,
) -> Result<Arc<File>, ZeroFsError> {
let orig = path.as_ref().to_path_buf();
let op_id = new_op_id();
let create_opts = OpenOptions::read_write().create(true).truncate(true);
let (session, guard) = {
let orig = orig.clone();
self.with_failover(move |c| {
let orig = orig.clone();
async move { c.open_guard_op_id(&orig, &create_opts, op_id).await }
})
.await?
};
let reopen_opts = OpenOptions::read_write();
Ok(File::new_failover(
Arc::clone(self),
session,
guard,
orig,
reopen_opts,
))
}
pub async fn remove_file(&self, path: impl AsRef<Path>) -> Result<(), ZeroFsError> {
let path = path.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let path = path.clone();
async move { c.remove_file_op_id(path, op_id).await }
})
.await
}
pub async fn remove_dir(&self, path: impl AsRef<Path>) -> Result<(), ZeroFsError> {
let path = path.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let path = path.clone();
async move { c.remove_dir_op_id(path, op_id).await }
})
.await
}
pub async fn rename(
&self,
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> Result<(), ZeroFsError> {
let from = from.as_ref().to_path_buf();
let to = to.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let from = from.clone();
let to = to.clone();
async move { c.rename_op_id(from, to, op_id).await }
})
.await
}
pub async fn hard_link(
&self,
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) -> Result<Metadata, ZeroFsError> {
let original = original.as_ref().to_path_buf();
let link = link.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let original = original.clone();
let link = link.clone();
async move { c.hard_link_op_id(original, link, op_id).await }
})
.await
}
pub async fn symlink(
&self,
target: impl AsRef<Path>,
link_path: impl AsRef<Path>,
) -> Result<Metadata, ZeroFsError> {
let target = target.as_ref().to_path_buf();
let link_path = link_path.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let target = target.clone();
let link_path = link_path.clone();
async move { c.symlink_op_id(target, link_path, op_id).await }
})
.await
}
pub async fn mknod(
&self,
path: impl AsRef<Path>,
kind: NodeKind,
mode: u32,
) -> Result<Metadata, ZeroFsError> {
let path = path.as_ref().to_path_buf();
let op_id = new_op_id();
self.with_failover(move |c| {
let path = path.clone();
async move { c.mknod_op_id(path, kind, mode, op_id).await }
})
.await
}
}