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 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 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 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 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 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 pub fn close(&self) {
437 }
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}