use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use crafty_net::send_client_request;
use crafty_net::transport::Transport;
use crafty_proto::{ClientRequest, ClientResponse, NodeId};
use crate::error::ClientError;
pub trait Client {
fn propose(
&self,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
fn query(&self, payload: Vec<u8>) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
}
pub trait KeyedClient: Client {
fn propose_keyed(
&self,
key: Vec<u8>,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
fn query_keyed(
&self,
key: Vec<u8>,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub attempt_timeout: Duration,
pub backoff: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 5,
attempt_timeout: Duration::from_secs(5),
backoff: Duration::from_millis(100),
}
}
}
pub struct RemoteClient {
transport: Arc<dyn Transport>,
targets: Vec<NodeId>,
retry: RetryPolicy,
cursor: AtomicUsize,
}
impl RemoteClient {
#[must_use]
pub fn new(transport: Arc<dyn Transport>, targets: impl IntoIterator<Item = NodeId>) -> Self {
Self {
transport,
targets: targets.into_iter().collect(),
retry: RetryPolicy::default(),
cursor: AtomicUsize::new(0),
}
}
#[must_use]
pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn targets(&self) -> &[NodeId] {
&self.targets
}
async fn call(&self, request: ClientRequest) -> Result<Vec<u8>, ClientError> {
let n = self.targets.len();
if n == 0 {
return Err(ClientError::NoTargets);
}
let attempts = self.retry.max_attempts.max(1);
let mut idx = self.cursor.fetch_add(1, Ordering::Relaxed) % n;
let mut last = ClientError::NoLeader { attempts };
for attempt in 0..attempts {
let target = self.targets[idx % n];
let send = send_client_request(&*self.transport, target, &request);
match tokio::time::timeout(self.retry.attempt_timeout, send).await {
Ok(Ok(ClientResponse::Ok(bytes))) => return Ok(bytes),
Ok(Ok(ClientResponse::NotLeader { leader })) => {
last = ClientError::NoLeader { attempts };
idx = leader
.and_then(|l| self.targets.iter().position(|t| *t == l))
.unwrap_or(idx + 1);
}
Ok(Ok(ClientResponse::Error(msg))) => {
last = ClientError::Server(msg);
idx += 1;
}
Ok(Ok(ClientResponse::ReadIndexConfirmed { .. })) => {
last =
ClientError::Server("unexpected ReadIndexConfirmed on client wire".into());
idx += 1;
}
Ok(Err(e)) => {
last = ClientError::Unreachable {
attempts,
last: e.to_string(),
};
idx += 1;
}
Err(_elapsed) => {
last = ClientError::Timeout { attempts };
idx += 1;
}
}
if attempt + 1 < attempts {
tokio::time::sleep(self.retry.backoff).await;
}
}
Err(last)
}
}
impl KeyedClient for RemoteClient {
fn propose_keyed(
&self,
key: Vec<u8>,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::ProposeKeyed {
key,
command: payload,
})
}
fn query_keyed(
&self,
key: Vec<u8>,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::QueryKeyed {
key,
query: payload,
})
}
}
impl crate::two_phase::TwoPhaseClient for RemoteClient {
fn prepare_keyed(
&self,
tx_id: Vec<u8>,
key: Vec<u8>,
command: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::TwoPhasePrepare {
tx_id,
key,
command,
})
}
fn commit_keyed(
&self,
tx_id: Vec<u8>,
key: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::TwoPhaseCommit { tx_id, key })
}
fn abort_keyed(
&self,
tx_id: Vec<u8>,
key: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::TwoPhaseAbort { tx_id, key })
}
}
impl Client for RemoteClient {
fn propose(
&self,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::Propose(payload))
}
fn query(&self, payload: Vec<u8>) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
self.call(ClientRequest::Query(payload))
}
}