Skip to main content

crafty_client/
remote.rs

1//! The [`Client`] trait and the HTTP/3 [`RemoteClient`] (client-api L2, client-routing).
2
3use std::future::Future;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::time::Duration;
7
8use crafty_net::send_client_request;
9use crafty_net::transport::Transport;
10use crafty_proto::{ClientRequest, ClientResponse, NodeId};
11
12use crate::error::ClientError;
13
14/// A cluster client: submit an application-encoded write (`propose`) or
15/// linearizable read (`query`) and get the application-encoded response back.
16///
17/// The raw-bytes layer shared by the remote HTTP/3 client and any in-process
18/// adapter; [`TypedClient`](crate::TypedClient) wraps it with a
19/// [`StateMachine`](crafty_core::StateMachine)'s command/query/response types.
20pub trait Client {
21    /// Submit a write. `payload` is the application-encoded command; the
22    /// returned bytes are the application-encoded response.
23    fn propose(
24        &self,
25        payload: Vec<u8>,
26    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
27
28    /// Submit a linearizable read (`ReadIndex`, read-consistency). `payload` is the
29    /// application-encoded query; the returned bytes are the encoded response.
30    fn query(&self, payload: Vec<u8>) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
31}
32
33/// Extension of [`Client`] for shard-aware multi-Raft routing (write-sharding-multi-raft).
34pub trait KeyedClient: Client {
35    /// Submit a write to the Raft group owning `key`.
36    fn propose_keyed(
37        &self,
38        key: Vec<u8>,
39        payload: Vec<u8>,
40    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
41
42    /// Submit a linearizable read against the Raft group owning `key`.
43    fn query_keyed(
44        &self,
45        key: Vec<u8>,
46        payload: Vec<u8>,
47    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send;
48}
49
50/// How a [`RemoteClient`] retries across nodes and elections (backlog F4).
51#[derive(Debug, Clone)]
52pub struct RetryPolicy {
53    /// Total number of send attempts before giving up (each attempt may target
54    /// a different node).
55    pub max_attempts: u32,
56    /// Deadline for a single attempt (the follower→leader forward hop of client-routing
57    /// happens inside this window on the server side).
58    pub attempt_timeout: Duration,
59    /// Delay between attempts, giving an in-progress election time to settle.
60    pub backoff: Duration,
61}
62
63impl Default for RetryPolicy {
64    fn default() -> Self {
65        Self {
66            max_attempts: 5,
67            attempt_timeout: Duration::from_secs(5),
68            backoff: Duration::from_millis(100),
69        }
70    }
71}
72
73/// A remote client that talks to a crafty cluster over any [`Transport`]
74/// (live QUIC/HTTP/3 in production, the in-memory `LocalNetwork` in tests).
75///
76/// A client may contact **any** node: a follower transparently forwards to the
77/// leader server-side (client-routing), so a single reachable node is enough. The
78/// client is nonetheless configured with several `targets` and a
79/// [`RetryPolicy`] so it survives a node being down or an election in flight —
80/// it rotates across targets, and follows a `NotLeader` hint straight to the
81/// named leader when one is returned.
82pub struct RemoteClient {
83    transport: Arc<dyn Transport>,
84    targets: Vec<NodeId>,
85    retry: RetryPolicy,
86    cursor: AtomicUsize,
87}
88
89impl RemoteClient {
90    /// Build a client over `transport` that contacts `targets` (seed node ids;
91    /// the transport resolves each to an address). Uses the default
92    /// [`RetryPolicy`].
93    #[must_use]
94    pub fn new(transport: Arc<dyn Transport>, targets: impl IntoIterator<Item = NodeId>) -> Self {
95        Self {
96            transport,
97            targets: targets.into_iter().collect(),
98            retry: RetryPolicy::default(),
99            cursor: AtomicUsize::new(0),
100        }
101    }
102
103    /// Override the [`RetryPolicy`].
104    #[must_use]
105    pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
106        self.retry = retry;
107        self
108    }
109
110    /// The seed nodes this client rotates across.
111    #[must_use]
112    pub fn targets(&self) -> &[NodeId] {
113        &self.targets
114    }
115
116    /// Send one request with failover + leader-follow retry.
117    async fn call(&self, request: ClientRequest) -> Result<Vec<u8>, ClientError> {
118        let n = self.targets.len();
119        if n == 0 {
120            return Err(ClientError::NoTargets);
121        }
122        let attempts = self.retry.max_attempts.max(1);
123        // Start from a rotating offset so load spreads across nodes and a
124        // downed seed does not trap every client on the same first hop.
125        let mut idx = self.cursor.fetch_add(1, Ordering::Relaxed) % n;
126        let mut last = ClientError::NoLeader { attempts };
127
128        for attempt in 0..attempts {
129            let target = self.targets[idx % n];
130            let send = send_client_request(&*self.transport, target, &request);
131            match tokio::time::timeout(self.retry.attempt_timeout, send).await {
132                Ok(Ok(ClientResponse::Ok(bytes))) => return Ok(bytes),
133                Ok(Ok(ClientResponse::NotLeader { leader })) => {
134                    last = ClientError::NoLeader { attempts };
135                    // Follow a concrete leader hint straight to that node;
136                    // otherwise rotate to the next target.
137                    idx = leader
138                        .and_then(|l| self.targets.iter().position(|t| *t == l))
139                        .unwrap_or(idx + 1);
140                }
141                Ok(Ok(ClientResponse::Error(msg))) => {
142                    // The runtime returns `Error` for "no leader elected" during
143                    // an election as well as for definitive failures; retry, and
144                    // surface the last message if every attempt fails.
145                    last = ClientError::Server(msg);
146                    idx += 1;
147                }
148                Ok(Ok(ClientResponse::ReadIndexConfirmed { .. })) => {
149                    last =
150                        ClientError::Server("unexpected ReadIndexConfirmed on client wire".into());
151                    idx += 1;
152                }
153                Ok(Err(e)) => {
154                    last = ClientError::Unreachable {
155                        attempts,
156                        last: e.to_string(),
157                    };
158                    idx += 1;
159                }
160                Err(_elapsed) => {
161                    last = ClientError::Timeout { attempts };
162                    idx += 1;
163                }
164            }
165            if attempt + 1 < attempts {
166                tokio::time::sleep(self.retry.backoff).await;
167            }
168        }
169        Err(last)
170    }
171}
172
173impl KeyedClient for RemoteClient {
174    fn propose_keyed(
175        &self,
176        key: Vec<u8>,
177        payload: Vec<u8>,
178    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
179        self.call(ClientRequest::ProposeKeyed {
180            key,
181            command: payload,
182        })
183    }
184
185    fn query_keyed(
186        &self,
187        key: Vec<u8>,
188        payload: Vec<u8>,
189    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
190        self.call(ClientRequest::QueryKeyed {
191            key,
192            query: payload,
193        })
194    }
195}
196
197impl crate::two_phase::TwoPhaseClient for RemoteClient {
198    fn prepare_keyed(
199        &self,
200        tx_id: Vec<u8>,
201        key: Vec<u8>,
202        command: Vec<u8>,
203    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
204        self.call(ClientRequest::TwoPhasePrepare {
205            tx_id,
206            key,
207            command,
208        })
209    }
210
211    fn commit_keyed(
212        &self,
213        tx_id: Vec<u8>,
214        key: Vec<u8>,
215    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
216        self.call(ClientRequest::TwoPhaseCommit { tx_id, key })
217    }
218
219    fn abort_keyed(
220        &self,
221        tx_id: Vec<u8>,
222        key: Vec<u8>,
223    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
224        self.call(ClientRequest::TwoPhaseAbort { tx_id, key })
225    }
226}
227
228impl Client for RemoteClient {
229    fn propose(
230        &self,
231        payload: Vec<u8>,
232    ) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
233        self.call(ClientRequest::Propose(payload))
234    }
235
236    fn query(&self, payload: Vec<u8>) -> impl Future<Output = Result<Vec<u8>, ClientError>> + Send {
237        self.call(ClientRequest::Query(payload))
238    }
239}