Skip to main content

brain_db_sdk/
pool.rs

1//! A fixed-size connection pool over [`BrainClient`].
2//!
3//! Each [`BrainClient`] is already a *multiplexed* connection — one socket
4//! that serves many concurrent requests demultiplexed by `stream_id` — so a
5//! pool isn't needed for concurrency. What a pool buys is **socket-level
6//! parallelism**: spreading load across N independent TCP connections (and
7//! thus N server-side connection slots / shards' accept paths), and isolating
8//! a single slow or stalled socket from the rest of the workload.
9//!
10//! [`Pool::connect`] opens `size` connections up front, each running its own
11//! handshake. [`Pool::get`] hands back a shared [`BrainClient`] by round-robin;
12//! callers issue verbs on it directly (the client's verbs take `&self`). The
13//! returned `Arc` can be cloned and moved into tasks freely.
14//!
15//! [`Pool::get_healthy`] replaces a member whose connection has died (the
16//! server restarted, or the socket failed) before handing it out, so a
17//! long-lived pool survives a server restart instead of failing every request
18//! on the dead sockets forever. At most one reconnect runs per member at a
19//! time; concurrent callers wait for it rather than each opening a socket.
20//!
21//! Deferred: periodic background health-checking, and graceful per-member BYE
22//! on shutdown. Dropping the pool drops every client, which closes its socket
23//! (a TCP FIN); it does not send a BYE frame first.
24
25use std::net::SocketAddr;
26use std::sync::atomic::{AtomicUsize, Ordering};
27use std::sync::{Arc, RwLock};
28
29use crate::client::{Auth, BrainClient, ClientConfig};
30use crate::error::{BrainError, Result};
31
32/// One pooled connection, swappable when it dies.
33struct Slot {
34    client: RwLock<Arc<BrainClient>>,
35    /// Serializes reconnects of this slot so a burst opens one socket.
36    reconnecting: tokio::sync::Mutex<()>,
37}
38
39impl Slot {
40    fn new(client: BrainClient) -> Self {
41        Self {
42            client: RwLock::new(Arc::new(client)),
43            reconnecting: tokio::sync::Mutex::new(()),
44        }
45    }
46
47    fn current(&self) -> Arc<BrainClient> {
48        Arc::clone(&self.client.read().expect("pool slot lock poisoned"))
49    }
50}
51
52/// A fixed-size set of [`BrainClient`] connections handed out round-robin.
53pub struct Pool {
54    slots: Vec<Slot>,
55    next: AtomicUsize,
56    addr: SocketAddr,
57    config: ClientConfig,
58}
59
60impl std::fmt::Debug for Pool {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("Pool")
63            .field("size", &self.slots.len())
64            .finish_non_exhaustive()
65    }
66}
67
68impl Pool {
69    /// Open `size` connections to `addr` with the given credential and default
70    /// transport settings, and run every handshake. Fails (closing any
71    /// already-opened members) if `size` is 0 or any connection's handshake
72    /// fails.
73    pub async fn connect(addr: SocketAddr, size: usize, auth: Auth) -> Result<Self> {
74        Self::connect_with(addr, size, &ClientConfig::new(auth)).await
75    }
76
77    /// Open `size` connections to `addr` with an explicit configuration
78    /// template, cloned per connection. Each member runs its own handshake.
79    /// All members share the template's credential, so the server binds them to
80    /// the same agent; they remain individually identifiable by session id.
81    pub async fn connect_with(
82        addr: SocketAddr,
83        size: usize,
84        config: &ClientConfig,
85    ) -> Result<Self> {
86        if size == 0 {
87            return Err(BrainError::Protocol(
88                "connection pool size must be >= 1".to_string(),
89            ));
90        }
91        let mut slots = Vec::with_capacity(size);
92        for _ in 0..size {
93            let cfg = config.clone();
94            match BrainClient::connect_with(addr, cfg).await {
95                Ok(client) => slots.push(Slot::new(client)),
96                Err(e) => {
97                    // Best-effort close of the members opened so far before
98                    // surfacing the failure; dropping each closes its socket.
99                    drop(slots);
100                    return Err(e);
101                }
102            }
103        }
104        Ok(Self {
105            slots,
106            next: AtomicUsize::new(0),
107            addr,
108            config: config.clone(),
109        })
110    }
111
112    /// The number of pooled connections.
113    #[must_use]
114    pub fn size(&self) -> usize {
115        self.slots.len()
116    }
117
118    fn next_slot(&self) -> &Slot {
119        let idx = self.next.fetch_add(1, Ordering::Relaxed) % self.slots.len();
120        &self.slots[idx]
121    }
122
123    /// Borrow the next connection, round-robin, as-is — it may be dead if the
124    /// server went away. Prefer [`Pool::get_healthy`] for long-lived pools.
125    /// The returned `Arc` is cheap to clone and safe to move across tasks; the
126    /// underlying client multiplexes concurrent requests itself.
127    ///
128    /// # Panics
129    /// Only if a slot's lock was poisoned by a panic while swapping a client.
130    #[must_use]
131    pub fn get(&self) -> Arc<BrainClient> {
132        self.next_slot().current()
133    }
134
135    /// Borrow the next live connection, round-robin, reconnecting it first if
136    /// it has died (e.g. the server restarted). Requests already in flight on
137    /// the old connection fail as before; new ones get the fresh socket.
138    ///
139    /// # Errors
140    /// The reconnect's [`BrainError`] if the server is still unreachable.
141    ///
142    /// # Panics
143    /// Only if a slot's lock was poisoned by a panic while swapping a client.
144    pub async fn get_healthy(&self) -> Result<Arc<BrainClient>> {
145        let slot = self.next_slot();
146        let client = slot.current();
147        if !client.is_closed() {
148            return Ok(client);
149        }
150        let _guard = slot.reconnecting.lock().await;
151        // Another caller may have reconnected while we waited.
152        let client = slot.current();
153        if !client.is_closed() {
154            return Ok(client);
155        }
156        let fresh = Arc::new(BrainClient::connect_with(self.addr, self.config.clone()).await?);
157        *slot.client.write().expect("pool slot lock poisoned") = Arc::clone(&fresh);
158        Ok(fresh)
159    }
160}