Skip to main content

oxia/
client_builder.rs

1//! Builder for configuring and creating an [`OxiaClient`].
2
3use crate::address::ensure_protocol;
4use crate::client::OxiaClient;
5use crate::client_options::OxiaClientOptions;
6use crate::errors::OxiaError;
7use std::time::Duration;
8
9/// Configures and creates an [`OxiaClient`].
10///
11/// | Option | Default |
12/// |---|---|
13/// | [`service_address`](OxiaClientBuilder::service_address) | `127.0.0.1:6648` |
14/// | [`namespace`](OxiaClientBuilder::namespace) | `default` |
15/// | [`identity`](OxiaClientBuilder::identity) | random UUID |
16/// | [`batch_max_size`](OxiaClientBuilder::batch_max_size) | 128 KiB |
17/// | [`max_requests_per_batch`](OxiaClientBuilder::max_requests_per_batch) | 1000 |
18/// | [`max_write_batches_in_flight`](OxiaClientBuilder::max_write_batches_in_flight) | 4 |
19/// | [`max_read_batches_in_flight`](OxiaClientBuilder::max_read_batches_in_flight) | 4 |
20/// | [`session_timeout`](OxiaClientBuilder::session_timeout) | 15 s |
21/// | [`session_keep_alive`](OxiaClientBuilder::session_keep_alive) | `session_timeout / 10` |
22/// | [`request_timeout`](OxiaClientBuilder::request_timeout) | 30 s |
23///
24/// ```no_run
25/// # async fn example() -> Result<(), oxia::OxiaError> {
26/// use oxia::OxiaClient;
27/// use std::time::Duration;
28///
29/// let client = OxiaClient::builder()
30///     .service_address("localhost:6648")
31///     .namespace("my-namespace")
32///     .session_timeout(Duration::from_secs(30))
33///     .build()
34///     .await?;
35/// # Ok(())
36/// # }
37/// ```
38#[derive(Debug, Clone, Default)]
39#[must_use = "builders do nothing unless built"]
40pub struct OxiaClientBuilder {
41    service_address: Option<String>,
42    namespace: Option<String>,
43    identity: Option<String>,
44    batch_max_size: Option<u32>,
45    max_requests_per_batch: Option<u32>,
46    max_write_batches_in_flight: Option<u32>,
47    max_read_batches_in_flight: Option<u32>,
48    session_timeout: Option<Duration>,
49    session_keep_alive: Option<Duration>,
50    request_timeout: Option<Duration>,
51}
52
53impl OxiaClientBuilder {
54    /// Creates a builder with all options at their defaults.
55    pub fn new() -> Self {
56        OxiaClientBuilder::default()
57    }
58
59    /// The address of the Oxia service, as `host:port` (default:
60    /// `127.0.0.1:6648`).
61    pub fn service_address(mut self, service_address: impl Into<String>) -> Self {
62        self.service_address = Some(ensure_protocol(service_address.into()));
63        self
64    }
65
66    /// The Oxia namespace all operations apply to (default: `"default"`).
67    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
68        self.namespace = Some(namespace.into());
69        self
70    }
71
72    /// The client identity reported with ephemeral records (default: a random
73    /// UUID).
74    pub fn identity(mut self, identity: impl Into<String>) -> Self {
75        self.identity = Some(identity.into());
76        self
77    }
78
79    /// The maximum number of write batches in flight per shard (default: 4).
80    ///
81    /// Batching is rate-adaptive: while fewer batches are outstanding on a
82    /// shard's write stream, operations are dispatched almost immediately;
83    /// once the window is exhausted, the open batch accumulates operations, so
84    /// batch size automatically tracks the server's service rate. Must be at
85    /// least 1.
86    pub fn max_write_batches_in_flight(mut self, max: u32) -> Self {
87        self.max_write_batches_in_flight = Some(max);
88        self
89    }
90
91    /// The maximum number of concurrent read batches per shard (default: 4).
92    /// Works like [`max_write_batches_in_flight`](Self::max_write_batches_in_flight),
93    /// bounding the read RPCs outstanding per shard. Must be at least 1.
94    pub fn max_read_batches_in_flight(mut self, max: u32) -> Self {
95        self.max_read_batches_in_flight = Some(max);
96        self
97    }
98
99    /// The maximum byte size of a batch (default: 128KiB).
100    pub fn batch_max_size(mut self, batch_max_size: u32) -> Self {
101        self.batch_max_size = Some(batch_max_size);
102        self
103    }
104
105    /// The maximum number of operations in a batch (default: 1000).
106    pub fn max_requests_per_batch(mut self, max_requests_per_batch: u32) -> Self {
107        self.max_requests_per_batch = Some(max_requests_per_batch);
108        self
109    }
110
111    /// The session timeout for ephemeral records (default: 15s). Ephemeral
112    /// records disappear if the client fails to heartbeat for this long.
113    pub fn session_timeout(mut self, session_timeout: Duration) -> Self {
114        self.session_timeout = Some(session_timeout);
115        self
116    }
117
118    /// The interval between session keep-alive heartbeats (default:
119    /// `session_timeout / 10`).
120    pub fn session_keep_alive(mut self, session_keep_alive: Duration) -> Self {
121        self.session_keep_alive = Some(session_keep_alive);
122        self
123    }
124
125    /// The deadline applied to individual operations (default: 30s).
126    pub fn request_timeout(mut self, request_timeout: Duration) -> Self {
127        self.request_timeout = Some(request_timeout);
128        self
129    }
130
131    fn assemble_options(self) -> OxiaClientOptions {
132        let mut options = OxiaClientOptions::default();
133        if let Some(service_address) = self.service_address {
134            options.service_address = service_address
135        }
136        if let Some(namespace) = self.namespace {
137            options.namespace = namespace;
138        }
139        if let Some(identity) = self.identity {
140            options.identity = identity;
141        }
142        if let Some(batch_max_size) = self.batch_max_size {
143            options.batch_max_size = batch_max_size;
144        }
145        if let Some(max_requests_per_batch) = self.max_requests_per_batch {
146            options.max_requests_per_batch = max_requests_per_batch;
147        }
148        if let Some(max) = self.max_write_batches_in_flight {
149            options.max_write_batches_in_flight = max;
150        }
151        if let Some(max) = self.max_read_batches_in_flight {
152            options.max_read_batches_in_flight = max;
153        }
154        if let Some(session_timeout) = self.session_timeout {
155            options.session_timeout = session_timeout;
156        }
157        // Default the keep-alive interval to session_timeout/10, matching the
158        // reference client, once the final session timeout is known.
159        options.session_keep_alive = self
160            .session_keep_alive
161            .unwrap_or(options.session_timeout / 10);
162        if let Some(request_timeout) = self.request_timeout {
163            options.request_timeout = request_timeout;
164        }
165        options
166    }
167
168    /// Connects to the cluster and returns the client.
169    ///
170    /// Fails fast — within the request timeout — when the cluster is
171    /// unreachable, the namespace does not exist, or the first shard
172    /// assignments cannot be obtained.
173    pub async fn build(self) -> Result<OxiaClient, OxiaError> {
174        let options = self.assemble_options();
175        if options.max_write_batches_in_flight == 0 || options.max_read_batches_in_flight == 0 {
176            return Err(OxiaError::InvalidArgument(
177                "max batches in flight must be at least 1".to_string(),
178            ));
179        }
180        OxiaClient::new(options).await
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn keep_alive_defaults_to_a_tenth_of_the_session_timeout() {
190        let options = OxiaClientBuilder::new()
191            .session_timeout(Duration::from_secs(30))
192            .assemble_options();
193        assert_eq!(options.session_keep_alive, Duration::from_secs(3));
194    }
195
196    #[test]
197    fn keep_alive_can_be_overridden() {
198        let options = OxiaClientBuilder::new()
199            .session_timeout(Duration::from_secs(30))
200            .session_keep_alive(Duration::from_millis(500))
201            .assemble_options();
202        assert_eq!(options.session_keep_alive, Duration::from_millis(500));
203    }
204
205    #[test]
206    fn default_keep_alive_tracks_the_default_session_timeout() {
207        let options = OxiaClientBuilder::new().assemble_options();
208        assert_eq!(options.session_keep_alive, options.session_timeout / 10);
209    }
210}