Skip to main content

dodb_client/
lib.rs

1use std::fmt;
2use std::io::Cursor;
3use std::net::SocketAddr;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7use dodb_core::{DocumentKey, Revision, RevisionState, TenantId, TransactionRequest};
8use dodb_protocol::{
9    ApplicationError, MutationOutcome, ProtocolError, ProtocolLimits, ResponseEnvelope,
10    decode_header, decode_response_parts, encode_request,
11};
12use dodb_service::{Document, Response, TransactionOutcome};
13use quinn::rustls::pki_types::CertificateDer;
14use quinn::{ClientConfig as QuinnClientConfig, Endpoint, VarInt};
15use tokio::sync::Notify;
16
17const RECONNECT_BACKOFF_BASE: Duration = Duration::from_millis(50);
18const RECONNECT_BACKOFF_MAX: Duration = Duration::from_secs(1);
19
20#[derive(Debug)]
21pub enum ClientError {
22    InvalidInput(String),
23    Tls(String),
24    Endpoint(String),
25    Transport(String),
26    Protocol(ProtocolError),
27    Application(Box<ApplicationError>),
28    UnknownMutationOutcome {
29        detail: String,
30        cause: Option<Box<ApplicationError>>,
31    },
32}
33
34impl fmt::Display for ClientError {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::InvalidInput(detail) => write!(formatter, "invalid client input: {detail}"),
38            Self::Tls(detail) => write!(formatter, "TLS configuration error: {detail}"),
39            Self::Endpoint(detail) => write!(formatter, "QUIC endpoint error: {detail}"),
40            Self::Transport(detail) => write!(formatter, "transport error: {detail}"),
41            Self::Protocol(error) => write!(formatter, "protocol error: {error}"),
42            Self::Application(error) => {
43                write!(
44                    formatter,
45                    "application error {:?}: {}",
46                    error.kind, error.detail
47                )
48            }
49            Self::UnknownMutationOutcome { detail, .. } => {
50                write!(formatter, "mutation outcome is unknown: {detail}")
51            }
52        }
53    }
54}
55
56impl std::error::Error for ClientError {}
57
58impl ClientError {
59    fn invalidates_connection(&self) -> bool {
60        matches!(
61            self,
62            Self::Endpoint(_)
63                | Self::Transport(_)
64                | Self::Protocol(_)
65                | Self::UnknownMutationOutcome { cause: None, .. }
66        )
67    }
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct ClientTlsConfig {
72    root_certificates: Vec<Vec<u8>>,
73}
74
75impl ClientTlsConfig {
76    pub fn from_der(root_certificates: Vec<Vec<u8>>) -> Result<Self, ClientError> {
77        if root_certificates.is_empty() {
78            return Err(ClientError::Tls(
79                "at least one trusted root certificate is required".to_owned(),
80            ));
81        }
82        Ok(Self { root_certificates })
83    }
84
85    pub fn from_pem(root_certificates_pem: &[u8]) -> Result<Self, ClientError> {
86        let mut reader = Cursor::new(root_certificates_pem);
87        let certificates = rustls_pemfile::certs(&mut reader)
88            .collect::<Result<Vec<_>, _>>()
89            .map_err(|error| ClientError::Tls(error.to_string()))?
90            .into_iter()
91            .map(|certificate| certificate.to_vec())
92            .collect::<Vec<_>>();
93        Self::from_der(certificates)
94    }
95
96    fn to_quinn_config(&self) -> Result<QuinnClientConfig, ClientError> {
97        let mut roots = quinn::rustls::RootCertStore::empty();
98        for certificate in &self.root_certificates {
99            roots
100                .add(CertificateDer::from(certificate.clone()))
101                .map_err(|error| ClientError::Tls(error.to_string()))?;
102        }
103        QuinnClientConfig::with_root_certificates(Arc::new(roots))
104            .map_err(|error| ClientError::Tls(error.to_string()))
105    }
106}
107
108#[derive(Clone)]
109pub struct DodbConnection {
110    inner: Arc<DodbConnectionInner>,
111}
112
113struct DodbConnectionInner {
114    endpoint: Endpoint,
115    server_addr: SocketAddr,
116    server_name: String,
117    limits: ProtocolLimits,
118    state: Mutex<ConnectionState>,
119    reconnect_notify: Notify,
120}
121
122struct ConnectionState {
123    current: Option<InstalledConnection>,
124    generation: u64,
125    reconnecting: bool,
126    failures: u32,
127    next_attempt_at: Option<Instant>,
128    closed: bool,
129}
130
131#[derive(Clone)]
132struct InstalledConnection {
133    connection: quinn::Connection,
134    generation: u64,
135}
136
137impl DodbConnection {
138    /// Opens one authenticated QUIC connection that can serve any number of
139    /// tenant-scoped handles.
140    pub async fn connect(
141        bind_addr: SocketAddr,
142        server_addr: SocketAddr,
143        server_name: &str,
144        tls: ClientTlsConfig,
145        limits: ProtocolLimits,
146    ) -> Result<Self, ClientError> {
147        let mut endpoint = Endpoint::client(bind_addr)
148            .map_err(|error| ClientError::Endpoint(error.to_string()))?;
149        endpoint.set_default_client_config(tls.to_quinn_config()?);
150        let connection = Self::from_endpoint(endpoint, server_addr, server_name, limits)?;
151        connection.ensure_connection().await?;
152        Ok(connection)
153    }
154
155    pub async fn connect_with_endpoint(
156        mut endpoint: Endpoint,
157        server_addr: SocketAddr,
158        server_name: &str,
159        tls: ClientTlsConfig,
160        limits: ProtocolLimits,
161    ) -> Result<Self, ClientError> {
162        endpoint.set_default_client_config(tls.to_quinn_config()?);
163        let connection = Self::from_endpoint(endpoint, server_addr, server_name, limits)?;
164        connection.ensure_connection().await?;
165        Ok(connection)
166    }
167
168    /// Creates a shared connection manager without dialing until its first request.
169    pub fn connect_lazy(
170        bind_addr: SocketAddr,
171        server_addr: SocketAddr,
172        server_name: &str,
173        tls: ClientTlsConfig,
174        limits: ProtocolLimits,
175    ) -> Result<Self, ClientError> {
176        let mut endpoint = Endpoint::client(bind_addr)
177            .map_err(|error| ClientError::Endpoint(error.to_string()))?;
178        endpoint.set_default_client_config(tls.to_quinn_config()?);
179        Self::from_endpoint(endpoint, server_addr, server_name, limits)
180    }
181
182    /// Creates a shared connection manager around an existing endpoint without
183    /// dialing until its first request.
184    pub fn connect_lazy_with_endpoint(
185        mut endpoint: Endpoint,
186        server_addr: SocketAddr,
187        server_name: &str,
188        tls: ClientTlsConfig,
189        limits: ProtocolLimits,
190    ) -> Result<Self, ClientError> {
191        endpoint.set_default_client_config(tls.to_quinn_config()?);
192        Self::from_endpoint(endpoint, server_addr, server_name, limits)
193    }
194
195    fn from_endpoint(
196        endpoint: Endpoint,
197        server_addr: SocketAddr,
198        server_name: &str,
199        limits: ProtocolLimits,
200    ) -> Result<Self, ClientError> {
201        limits.validate().map_err(ClientError::Protocol)?;
202        Ok(Self {
203            inner: Arc::new(DodbConnectionInner {
204                endpoint,
205                server_addr,
206                server_name: server_name.to_owned(),
207                limits,
208                state: Mutex::new(ConnectionState {
209                    current: None,
210                    generation: 0,
211                    reconnecting: false,
212                    failures: 0,
213                    next_attempt_at: None,
214                    closed: false,
215                }),
216                reconnect_notify: Notify::new(),
217            }),
218        })
219    }
220
221    pub fn for_tenant(&self, tenant: TenantId) -> DodbClient {
222        DodbClient {
223            connection: self.clone(),
224            tenant,
225        }
226    }
227
228    pub fn remote_addr(&self) -> SocketAddr {
229        self.inner.server_addr
230    }
231
232    /// Explicitly shuts down the shared endpoint and connection.
233    pub fn close(&self) {
234        let connection = {
235            let mut state = self.inner.state.lock().expect("connection state poisoned");
236            state.closed = true;
237            state.reconnecting = false;
238            state.next_attempt_at = None;
239            state.current.take()
240        };
241        if let Some(connection) = connection {
242            connection
243                .connection
244                .close(VarInt::from_u32(0), b"client shutdown");
245        }
246        self.inner
247            .endpoint
248            .close(VarInt::from_u32(0), b"client shutdown");
249        self.inner.reconnect_notify.notify_waiters();
250    }
251
252    async fn ensure_connection(&self) -> Result<InstalledConnection, ClientError> {
253        loop {
254            let notified = self.inner.reconnect_notify.notified();
255            let action = {
256                let mut state = self.inner.state.lock().expect("connection state poisoned");
257                if state.closed {
258                    return Err(ClientError::Endpoint(
259                        "client connection is explicitly closed".to_owned(),
260                    ));
261                }
262                if let Some(current) = &state.current
263                    && current.connection.close_reason().is_none()
264                {
265                    return Ok(current.clone());
266                }
267                state.current.take();
268                if state.reconnecting {
269                    ReconnectAction::Wait
270                } else if let Some(next_attempt_at) = state.next_attempt_at {
271                    if next_attempt_at > Instant::now() {
272                        ReconnectAction::Sleep(
273                            next_attempt_at.saturating_duration_since(Instant::now()),
274                        )
275                    } else {
276                        state.next_attempt_at = None;
277                        state.reconnecting = true;
278                        ReconnectAction::Dial
279                    }
280                } else {
281                    state.reconnecting = true;
282                    ReconnectAction::Dial
283                }
284            };
285            match action {
286                ReconnectAction::Wait => notified.await,
287                ReconnectAction::Sleep(duration) => tokio::time::sleep(duration).await,
288                ReconnectAction::Dial => return self.dial().await,
289            }
290        }
291    }
292
293    async fn dial(&self) -> Result<InstalledConnection, ClientError> {
294        let result = match self
295            .inner
296            .endpoint
297            .connect(self.inner.server_addr, &self.inner.server_name)
298        {
299            Ok(connecting) => connecting
300                .await
301                .map_err(|error| ClientError::Transport(error.to_string())),
302            Err(error) => Err(ClientError::Endpoint(error.to_string())),
303        };
304        let result = {
305            let mut state = self.inner.state.lock().expect("connection state poisoned");
306            state.reconnecting = false;
307            match result {
308                Ok(connection) if state.closed => {
309                    connection.close(VarInt::from_u32(0), b"client shutdown");
310                    Err(ClientError::Endpoint(
311                        "client connection is explicitly closed".to_owned(),
312                    ))
313                }
314                Ok(connection) => {
315                    state.generation = state.generation.wrapping_add(1);
316                    state.failures = 0;
317                    state.next_attempt_at = None;
318                    let installed = InstalledConnection {
319                        connection,
320                        generation: state.generation,
321                    };
322                    state.current = Some(installed.clone());
323                    Ok(installed)
324                }
325                Err(error) => {
326                    if !state.closed {
327                        state.failures = state.failures.saturating_add(1);
328                        state.next_attempt_at =
329                            Some(Instant::now() + reconnect_delay(state.failures));
330                    }
331                    Err(error)
332                }
333            }
334        };
335        self.inner.reconnect_notify.notify_waiters();
336        result
337    }
338
339    fn invalidate(&self, generation: u64) {
340        let connection = {
341            let mut state = self.inner.state.lock().expect("connection state poisoned");
342            if state.closed
343                || !generation_is_current(
344                    state.current.as_ref().map(|current| current.generation),
345                    generation,
346                )
347            {
348                None
349            } else {
350                state.current.take()
351            }
352        };
353        if let Some(connection) = connection {
354            connection
355                .connection
356                .close(VarInt::from_u32(0), b"connection invalidated");
357        }
358    }
359}
360
361enum ReconnectAction {
362    Wait,
363    Sleep(Duration),
364    Dial,
365}
366
367fn generation_is_current(current_generation: Option<u64>, failed_generation: u64) -> bool {
368    current_generation == Some(failed_generation)
369}
370
371fn reconnect_delay(failures: u32) -> Duration {
372    let shift = failures.saturating_sub(1).min(5);
373    RECONNECT_BACKOFF_BASE
374        .checked_mul(1u32 << shift)
375        .unwrap_or(RECONNECT_BACKOFF_MAX)
376        .min(RECONNECT_BACKOFF_MAX)
377}
378
379#[derive(Clone)]
380pub struct DodbClient {
381    connection: DodbConnection,
382    tenant: TenantId,
383}
384
385impl DodbClient {
386    /// Opens a connection and returns a tenant-scoped handle.
387    ///
388    /// New code serving multiple tenants should use [`DodbConnection::connect`]
389    /// once and call [`DodbConnection::for_tenant`] for each tenant.
390    pub async fn connect(
391        bind_addr: std::net::SocketAddr,
392        server_addr: std::net::SocketAddr,
393        server_name: &str,
394        tenant: TenantId,
395        tls: ClientTlsConfig,
396        limits: ProtocolLimits,
397    ) -> Result<Self, ClientError> {
398        Ok(
399            DodbConnection::connect(bind_addr, server_addr, server_name, tls, limits)
400                .await?
401                .for_tenant(tenant),
402        )
403    }
404
405    pub async fn connect_with_endpoint(
406        endpoint: Endpoint,
407        server_addr: std::net::SocketAddr,
408        server_name: &str,
409        tenant: TenantId,
410        tls: ClientTlsConfig,
411        limits: ProtocolLimits,
412    ) -> Result<Self, ClientError> {
413        Ok(
414            DodbConnection::connect_with_endpoint(endpoint, server_addr, server_name, tls, limits)
415                .await?
416                .for_tenant(tenant),
417        )
418    }
419
420    pub fn connection(&self) -> &DodbConnection {
421        &self.connection
422    }
423
424    pub fn tenant(&self) -> TenantId {
425        self.tenant
426    }
427
428    pub fn remote_addr(&self) -> std::net::SocketAddr {
429        self.connection.remote_addr()
430    }
431
432    /// Releases this tenant handle without shutting down other handles.
433    ///
434    /// Call [`DodbConnection::close`] on the shared connection when the
435    /// owner is ready to shut down the transport.
436    pub fn close(&self) {
437        // Kept as a source-compatible convenience for the original
438        // tenant-bound client API. The shared connection has explicit
439        // ownership and is closed through DodbConnection.
440    }
441
442    pub async fn get(&self, key: DocumentKey) -> Result<RevisionState, ClientError> {
443        match self.execute(dodb_service::Request::Get { key }).await? {
444            Response::Get(state) => Ok(state),
445            _ => Err(unexpected_response(1, 0)),
446        }
447    }
448
449    pub async fn put(&self, key: DocumentKey, value: Vec<u8>) -> Result<Revision, ClientError> {
450        match self
451            .execute(dodb_service::Request::Put { key, value })
452            .await?
453        {
454            Response::Put(revision) => Ok(revision),
455            _ => Err(unexpected_response(2, 0)),
456        }
457    }
458
459    pub async fn delete(&self, key: DocumentKey) -> Result<Revision, ClientError> {
460        match self.execute(dodb_service::Request::Delete { key }).await? {
461            Response::Delete(revision) => Ok(revision),
462            _ => Err(unexpected_response(3, 0)),
463        }
464    }
465
466    pub async fn query(
467        &self,
468        pk: dodb_core::PrimaryKey,
469        exclusive_after_sk: Option<dodb_core::SortKey>,
470        limit: usize,
471    ) -> Result<Vec<Document>, ClientError> {
472        match self
473            .execute(dodb_service::Request::Query {
474                pk,
475                exclusive_after_sk,
476                limit,
477            })
478            .await?
479        {
480            Response::Query(rows) => Ok(rows),
481            _ => Err(unexpected_response(4, 0)),
482        }
483    }
484
485    pub async fn scan(
486        &self,
487        exclusive_after_key: Option<DocumentKey>,
488        limit: usize,
489    ) -> Result<Vec<Document>, ClientError> {
490        match self
491            .execute(dodb_service::Request::Scan {
492                exclusive_after_key,
493                limit,
494            })
495            .await?
496        {
497            Response::Scan(rows) => Ok(rows),
498            _ => Err(unexpected_response(5, 0)),
499        }
500    }
501
502    pub async fn transact(
503        &self,
504        request: TransactionRequest,
505    ) -> Result<TransactionOutcome, ClientError> {
506        match self
507            .execute(dodb_service::Request::Transact { request })
508            .await?
509        {
510            Response::Transact(outcome) => Ok(outcome),
511            _ => Err(unexpected_response(8, 0)),
512        }
513    }
514
515    async fn execute(&self, request: dodb_service::Request) -> Result<Response, ClientError> {
516        let mutation = request.is_mutation();
517        let expected_response_type = response_opcode_for_request(&request);
518        let encoded_request = encode_request(self.tenant, &request, self.connection.inner.limits)
519            .map_err(ClientError::Protocol)?;
520        let installed = self.connection.ensure_connection().await?;
521        let result = self
522            .execute_on_connection(
523                &installed.connection,
524                mutation,
525                expected_response_type,
526                &encoded_request,
527            )
528            .await;
529        if let Err(error) = &result
530            && error.invalidates_connection()
531        {
532            self.connection.invalidate(installed.generation);
533        }
534        result
535    }
536
537    async fn execute_on_connection(
538        &self,
539        connection: &quinn::Connection,
540        mutation: bool,
541        expected_response_type: u8,
542        encoded_request: &[u8],
543    ) -> Result<Response, ClientError> {
544        let (mut send, mut receive) = connection
545            .open_bi()
546            .await
547            .map_err(|error| uncertain_mutation(mutation, error.to_string()))?;
548        if let Err(error) = send.write_all(encoded_request).await {
549            return Err(uncertain_mutation(
550                mutation,
551                format!("request write failed: {error}"),
552            ));
553        }
554        if let Err(error) = send.finish() {
555            return Err(uncertain_mutation(
556                mutation,
557                format!("request finish failed: {error}"),
558            ));
559        }
560        let mut header_bytes = [0u8; dodb_protocol::HEADER_SIZE];
561        if let Err(error) = receive.read_exact(&mut header_bytes).await {
562            return Err(uncertain_mutation(
563                mutation,
564                format!("response header failed: {error}"),
565            ));
566        }
567        let header =
568            decode_header(&header_bytes).map_err(|error| uncertain_protocol(mutation, error))?;
569        let frame_length = dodb_protocol::HEADER_SIZE.saturating_add(header.payload_length);
570        if frame_length > self.connection.inner.limits.max_response_frame_size {
571            return Err(uncertain_protocol(
572                mutation,
573                ProtocolError::PayloadTooLarge {
574                    length: frame_length,
575                    maximum: self.connection.inner.limits.max_response_frame_size,
576                },
577            ));
578        }
579        let mut payload = vec![0u8; header.payload_length];
580        if let Err(error) = receive.read_exact(&mut payload).await {
581            return Err(uncertain_mutation(
582                mutation,
583                format!("response payload failed: {error}"),
584            ));
585        }
586        let trailing = receive
587            .read_to_end(1)
588            .await
589            .map_err(|_| uncertain_protocol(mutation, ProtocolError::TrailingBytes))?;
590        if !trailing.is_empty() {
591            return Err(uncertain_protocol(mutation, ProtocolError::TrailingBytes));
592        }
593        match decode_response_parts(
594            header,
595            &payload,
596            Some(expected_response_type),
597            self.connection.inner.limits,
598        )
599        .map_err(|error| uncertain_protocol(mutation, error))?
600        {
601            ResponseEnvelope::Success(response) => Ok(response),
602            ResponseEnvelope::Error(error) => {
603                if mutation && error.mutation_outcome == MutationOutcome::Unknown {
604                    return Err(ClientError::UnknownMutationOutcome {
605                        detail: error.detail.clone(),
606                        cause: Some(Box::new(error)),
607                    });
608                }
609                Err(ClientError::Application(Box::new(error)))
610            }
611        }
612    }
613}
614
615fn response_opcode_for_request(request: &dodb_service::Request) -> u8 {
616    dodb_protocol::request_opcode(request)
617}
618
619fn unexpected_response(expected: u8, actual: u8) -> ClientError {
620    ClientError::Protocol(ProtocolError::InvalidResponseType { expected, actual })
621}
622
623fn uncertain_mutation(mutation: bool, detail: String) -> ClientError {
624    if mutation {
625        ClientError::UnknownMutationOutcome {
626            detail,
627            cause: None,
628        }
629    } else {
630        ClientError::Transport(detail)
631    }
632}
633
634fn uncertain_protocol(mutation: bool, error: ProtocolError) -> ClientError {
635    if mutation {
636        ClientError::UnknownMutationOutcome {
637            detail: error.to_string(),
638            cause: None,
639        }
640    } else {
641        ClientError::Protocol(error)
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use std::time::Duration;
648
649    use super::{generation_is_current, reconnect_delay};
650
651    #[test]
652    fn late_failure_cannot_invalidate_a_newer_generation() {
653        assert!(!generation_is_current(Some(11), 10));
654        assert!(generation_is_current(Some(10), 10));
655        assert!(!generation_is_current(None, 10));
656    }
657
658    #[test]
659    fn reconnect_backoff_is_bounded() {
660        assert_eq!(reconnect_delay(1), Duration::from_millis(50));
661        assert_eq!(reconnect_delay(2), Duration::from_millis(100));
662        assert_eq!(reconnect_delay(5), Duration::from_millis(800));
663        assert_eq!(reconnect_delay(6), Duration::from_secs(1));
664        assert_eq!(reconnect_delay(32), Duration::from_secs(1));
665    }
666}