Skip to main content

sie_sdk/
blocking.rs

1//! A blocking facade over the async client.
2//!
3//! There is one implementation of every endpoint, and this module drives it on a runtime it
4//! owns. Rather than mirroring every builder in a second, drift-prone surface, it hands you
5//! the real [`crate::Client`] and runs the future it produces:
6//!
7//! ```no_run
8//! use sie_sdk::{Item, blocking::Client};
9//!
10//! # fn main() -> sie_sdk::Result<()> {
11//! let client = Client::new("http://localhost:8080")?;
12//!
13//! let result = client.call(|sie| sie.encode("BAAI/bge-m3", [Item::text("Hello")]).send_one())?;
14//! println!("{:?}", result.dense);
15//!
16//! for model in client.call(|sie| sie.list_models())? {
17//!     println!("{}", model.name);
18//! }
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! Every builder, option and error is the same as on the async client.
24//!
25//! # Do not call this from async code
26//!
27//! Blocking a thread that is already inside a Tokio runtime deadlocks it. In an async
28//! context, use [`crate::Client`] directly.
29
30use std::future::Future;
31use std::sync::Arc;
32use std::time::Duration;
33
34use futures_util::StreamExt;
35
36use crate::error::{Error, Result};
37
38/// A blocking handle to one SIE server.
39///
40/// Cloning shares the runtime and the connection pool.
41#[derive(Clone)]
42pub struct Client {
43    api: crate::Client,
44    runtime: Arc<tokio::runtime::Runtime>,
45}
46
47impl std::fmt::Debug for Client {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("blocking::Client")
50            .field("api", &self.api)
51            .finish_non_exhaustive()
52    }
53}
54
55impl Client {
56    /// A blocking client with default settings.
57    pub fn new(base_url: impl AsRef<str>) -> Result<Self> {
58        Self::builder(base_url).build()
59    }
60
61    /// Start configuring a blocking client.
62    pub fn builder(base_url: impl AsRef<str>) -> ClientBuilder {
63        ClientBuilder {
64            inner: crate::Client::builder(base_url),
65            worker_threads: None,
66        }
67    }
68
69    /// The async client underneath, for building requests.
70    pub fn api(&self) -> &crate::Client {
71        &self.api
72    }
73
74    /// The server root, always with a trailing slash.
75    pub fn base_url(&self) -> &str {
76        self.api.base_url()
77    }
78
79    /// Build a request from the async client and run it to completion.
80    ///
81    /// The closure's borrow is tied to this client, so a request that borrows it (the
82    /// namespace accessors do) is as easy to write as one that owns its state.
83    pub fn call<'a, F, Fut, T>(&'a self, request: F) -> Result<T>
84    where
85        F: FnOnce(&'a crate::Client) -> Fut,
86        Fut: Future<Output = Result<T>> + 'a,
87    {
88        self.runtime.block_on(request(&self.api))
89    }
90
91    /// Run any future on this client's runtime.
92    ///
93    /// Useful for futures that are not a single request, such as joining several calls.
94    pub fn block_on<Fut: Future>(&self, future: Fut) -> Fut::Output {
95        self.runtime.block_on(future)
96    }
97
98    /// Drain a stream into a `Vec`, stopping at the first error.
99    ///
100    /// Streaming endpoints exist so a caller can act on each chunk as it lands; when the
101    /// whole result is what you want, prefer the buffered endpoint instead of this.
102    pub fn collect<S, T>(&self, stream: S) -> Result<Vec<T>>
103    where
104        S: futures_core::Stream<Item = Result<T>>,
105    {
106        self.runtime.block_on(async move {
107            let mut stream = std::pin::pin!(stream);
108            let mut items = Vec::new();
109            while let Some(item) = stream.next().await {
110                items.push(item?);
111            }
112            Ok(items)
113        })
114    }
115
116    /// Consume a stream chunk by chunk, without collecting it.
117    ///
118    /// The callback returns `false` to stop early, which closes the connection.
119    pub fn for_each<S, T, F>(&self, stream: S, mut handler: F) -> Result<()>
120    where
121        S: futures_core::Stream<Item = Result<T>>,
122        F: FnMut(T) -> bool,
123    {
124        self.runtime.block_on(async move {
125            let mut stream = std::pin::pin!(stream);
126            while let Some(item) = stream.next().await {
127                if !handler(item?) {
128                    break;
129                }
130            }
131            Ok(())
132        })
133    }
134}
135
136/// Configures a blocking [`Client`].
137pub struct ClientBuilder {
138    inner: crate::ClientBuilder,
139    worker_threads: Option<usize>,
140}
141
142impl ClientBuilder {
143    /// How many threads the owned runtime uses. Defaults to one.
144    ///
145    /// One is right for a client used from a single thread. Raise it when the same client
146    /// is shared across threads that make concurrent calls.
147    pub fn worker_threads(mut self, threads: usize) -> Self {
148        self.worker_threads = Some(threads);
149        self
150    }
151
152    /// Bearer token sent as `Authorization` on every request to the server.
153    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
154        self.inner = self.inner.api_key(api_key);
155        self
156    }
157
158    /// Per-attempt HTTP timeout.
159    pub fn timeout(mut self, timeout: Duration) -> Self {
160        self.inner = self.inner.timeout(timeout);
161        self
162    }
163
164    /// Default machine profile, optionally pool-qualified as `"pool/profile"`.
165    pub fn gpu(mut self, gpu: impl Into<String>) -> Self {
166        self.inner = self.inner.gpu(gpu);
167        self
168    }
169
170    /// Default runtime options, shallow-merged under any per-call options.
171    pub fn options(mut self, options: serde_json::Value) -> Self {
172        self.inner = self.inner.options(options);
173        self
174    }
175
176    /// Cap on pooled connections.
177    pub fn max_connections(mut self, max: usize) -> Self {
178        self.inner = self.inner.max_connections(max);
179        self
180    }
181
182    /// Cap on requests in flight at once.
183    pub fn max_concurrency(mut self, max: usize) -> Self {
184        self.inner = self.inner.max_concurrency(max);
185        self
186    }
187
188    /// Control-plane root, required by the connections namespace.
189    pub fn control_plane_url(mut self, url: impl Into<String>) -> Self {
190        self.inner = self.inner.control_plane_url(url);
191        self
192    }
193
194    /// Organisation slug, required by the connections namespace.
195    pub fn org(mut self, org: impl Into<String>) -> Self {
196        self.inner = self.inner.org(org);
197        self
198    }
199
200    /// Extra headers for an HTTP edge in front of the gateway.
201    pub fn base_url_headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
202        self.inner = self.inner.base_url_headers(headers);
203        self
204    }
205
206    /// Whether calls wait out provisioning by default.
207    pub fn wait_for_capacity(mut self, wait: bool) -> Self {
208        self.inner = self.inner.wait_for_capacity(wait);
209        self
210    }
211
212    /// Default wall-clock budget for a call including its retries.
213    pub fn provision_timeout(mut self, timeout: Duration) -> Self {
214        self.inner = self.inner.provision_timeout(timeout);
215        self
216    }
217
218    /// Default cap on `RESOURCE_EXHAUSTED` retries.
219    pub fn max_oom_retries(mut self, retries: u32) -> Self {
220        self.inner = self.inner.max_oom_retries(retries);
221        self
222    }
223
224    /// Build the runtime and the client.
225    pub fn build(self) -> Result<Client> {
226        let mut runtime = tokio::runtime::Builder::new_multi_thread();
227        runtime
228            .worker_threads(self.worker_threads.unwrap_or(1))
229            .enable_all();
230        let runtime = runtime.build().map_err(|err| {
231            Error::invalid(format!("could not start the blocking runtime: {err}"))
232        })?;
233
234        // The transport is constructed inside the runtime so its background resources are
235        // registered with the reactor that will drive them.
236        let api = runtime.block_on(async { self.inner.build() })?;
237        Ok(Client {
238            api,
239            runtime: Arc::new(runtime),
240        })
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::types::Item;
248
249    #[test]
250    fn builder_options_reach_the_async_client() {
251        let client = Client::builder("https://sie.example.com/")
252            .timeout(Duration::from_secs(5))
253            .max_oom_retries(0)
254            .wait_for_capacity(false)
255            .build()
256            .unwrap();
257        assert_eq!(client.base_url(), "https://sie.example.com/");
258        assert!(!client.api().default_options().wait_for_capacity);
259        assert_eq!(client.api().default_options().max_oom_retries, 0);
260    }
261
262    #[test]
263    fn call_runs_a_request_and_returns_its_error() {
264        let client = Client::builder("http://127.0.0.1:1")
265            .timeout(Duration::from_millis(200))
266            .wait_for_capacity(false)
267            .build()
268            .unwrap();
269        let result = client.call(|sie| sie.encode("m", [Item::text("hi")]).send_one());
270        assert!(
271            matches!(result, Err(Error::Connection { .. })),
272            "{result:?}"
273        );
274    }
275
276    #[test]
277    fn client_side_validation_still_applies() {
278        let client = Client::new("https://sie.invalid").unwrap();
279        let result = client.call(|sie| sie.encode("m", Vec::new()).send());
280        assert!(
281            matches!(result, Err(Error::InvalidRequest(_))),
282            "{result:?}"
283        );
284    }
285
286    #[test]
287    fn block_on_drives_arbitrary_futures() {
288        let client = Client::new("https://sie.invalid").unwrap();
289        assert_eq!(client.block_on(async { 1 + 1 }), 2);
290    }
291
292    #[test]
293    fn collect_stops_at_the_first_error() {
294        let client = Client::new("https://sie.invalid").unwrap();
295        let stream =
296            futures_util::stream::iter(vec![Ok(1), Err(Error::invalid("stop here")), Ok(3)]);
297        let result: Result<Vec<i32>> = client.collect(stream);
298        assert!(result.is_err());
299
300        let ok = futures_util::stream::iter(vec![Ok(1), Ok(2)]);
301        assert_eq!(client.collect::<_, i32>(ok).unwrap(), vec![1, 2]);
302    }
303
304    #[test]
305    fn for_each_can_stop_early() {
306        let client = Client::new("https://sie.invalid").unwrap();
307        let stream = futures_util::stream::iter(vec![Ok(1), Ok(2), Ok(3)]);
308        let mut seen = Vec::new();
309        client
310            .for_each(stream, |value: i32| {
311                seen.push(value);
312                value < 2
313            })
314            .unwrap();
315        assert_eq!(seen, vec![1, 2]);
316    }
317}