Skip to main content

phostt/inference/
pool.rs

1//! Session pool for ONNX inference triplets.
2//!
3//! Generic [`Pool<T>`] backed by an async-channel. The only production
4//! instantiation is [`SessionPool`] = `Pool<SessionTriplet>`.
5
6use ort::session::Session;
7use std::ops::{Deref, DerefMut};
8
9/// A set of ONNX sessions for one inference pipeline (encoder + decoder + joiner).
10///
11/// Moved out of the pool on checkout and returned on checkin.
12/// Each triplet is independent and can run inference concurrently with others.
13pub struct SessionTriplet {
14    pub(crate) encoder: Session,
15    pub(crate) decoder: Session,
16    pub(crate) joiner: Session,
17}
18
19/// Errors returned by [`Pool::checkout`].
20#[derive(Debug)]
21pub enum PoolError {
22    /// The pool was closed (graceful shutdown). All current and future
23    /// waiters resolve to this variant; the caller should respond with a
24    /// 503 / `pool_closed` to the client.
25    Closed,
26}
27
28impl std::fmt::Display for PoolError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            PoolError::Closed => write!(f, "session pool is closed"),
32        }
33    }
34}
35
36impl std::error::Error for PoolError {}
37
38/// Pool of pre-loaded items of type `T` backed by an MPMC `async-channel`.
39///
40/// `SessionPool = Pool<SessionTriplet>` is the only public instantiation
41/// outside this module. Generic `T` exists so the pool semantics can be
42/// unit-tested without ONNX models.
43///
44/// Checkout = `recv` from the channel, checkin = `send` back via the
45/// [`PoolGuard`] returned by [`checkout`](Self::checkout). The pool size acts
46/// as the concurrency limit — no separate semaphore needed. FIFO ordering is
47/// intrinsic to the underlying channel, and `close()` flips all current and
48/// future waiters into [`PoolError::Closed`] so graceful shutdown can drain
49/// without panicking.
50pub struct Pool<T> {
51    sender: async_channel::Sender<T>,
52    receiver: async_channel::Receiver<T>,
53    total: usize,
54}
55
56/// Public alias for the production pool: holds [`SessionTriplet`] instances.
57pub type SessionPool = Pool<SessionTriplet>;
58
59impl<T> Pool<T> {
60    /// Create a pool pre-filled with the given items.
61    pub fn new(items: Vec<T>) -> Self {
62        let total = items.len();
63        // Bounded channel with capacity == total: send is always immediate
64        // (try_send never returns Full while we own the only sender for
65        // checked-out items), and the channel's internal queue holds the
66        // available pool inventory.
67        let (sender, receiver) = async_channel::bounded(total.max(1));
68        for item in items {
69            sender
70                .try_send(item)
71                .expect("channel capacity matches item count");
72        }
73        Self {
74            sender,
75            receiver,
76            total,
77        }
78    }
79
80    /// Checkout an item from the pool. Awaits FIFO if none available.
81    ///
82    /// Returns [`PoolError::Closed`] if the pool was shut down via
83    /// [`close`](Self::close) before an item became available.
84    pub async fn checkout(&self) -> Result<PoolGuard<'_, T>, PoolError> {
85        match self.receiver.recv().await {
86            Ok(item) => Ok(PoolGuard {
87                pool: self,
88                item: Some(item),
89            }),
90            Err(_) => Err(PoolError::Closed),
91        }
92    }
93
94    /// Checkout an item from the pool synchronously (blocks until one is
95    /// available).  This is the FFI-friendly counterpart to [`checkout`](Self::checkout).
96    ///
97    /// Returns [`PoolError::Closed`] if the pool was shut down.
98    pub fn checkout_blocking(&self) -> Result<PoolGuard<'_, T>, PoolError> {
99        match self.receiver.recv_blocking() {
100            Ok(item) => Ok(PoolGuard {
101                pool: self,
102                item: Some(item),
103            }),
104            Err(_) => Err(PoolError::Closed),
105        }
106    }
107
108    /// Close the pool: all current and future [`checkout`](Self::checkout)
109    /// callers resolve to [`PoolError::Closed`]. Used by graceful shutdown.
110    /// Idempotent.
111    pub fn close(&self) {
112        self.sender.close();
113        self.receiver.close();
114    }
115
116    /// Total number of items the pool was created with.
117    pub fn total(&self) -> usize {
118        self.total
119    }
120
121    /// Number of currently available (not checked-out) items. O(1).
122    pub fn available(&self) -> usize {
123        self.receiver.len()
124    }
125}
126
127/// RAII guard that auto-checks-in an item when dropped.
128///
129/// Returned by [`Pool::checkout`]. Deref to access the inner item.
130/// On drop (including panic unwind) the item is returned to the pool;
131/// if the pool was closed in the meantime the item is silently dropped.
132pub struct PoolGuard<'a, T> {
133    pool: &'a Pool<T>,
134    item: Option<T>,
135}
136
137impl<T> PoolGuard<'_, T> {
138    /// Strip the lifetime so the guard can be moved into a `'static`
139    /// context (e.g. `tokio::task::spawn_blocking`). Returns the owned
140    /// item together with an [`OwnedReservation`] that must receive the
141    /// item back via [`OwnedReservation::checkin`] when the blocking task
142    /// is done. Forgets the original guard so the inner Drop does not also
143    /// try to check-in.
144    pub fn into_owned(mut self) -> (T, OwnedReservation<T>) {
145        let item = self
146            .item
147            .take()
148            .expect("PoolGuard::into_owned called after drop");
149        let reservation = OwnedReservation {
150            sender: self.pool.sender.clone(),
151        };
152        (item, reservation)
153    }
154}
155
156impl<T> Deref for PoolGuard<'_, T> {
157    type Target = T;
158
159    fn deref(&self) -> &Self::Target {
160        self.item
161            .as_ref()
162            .expect("PoolGuard accessed after item taken")
163    }
164}
165
166impl<T> DerefMut for PoolGuard<'_, T> {
167    fn deref_mut(&mut self) -> &mut Self::Target {
168        self.item
169            .as_mut()
170            .expect("PoolGuard accessed after item taken")
171    }
172}
173
174impl<T> Drop for PoolGuard<'_, T> {
175    fn drop(&mut self) {
176        if let Some(item) = self.item.take() {
177            // Best-effort checkin. `try_send` is non-blocking and the
178            // channel capacity equals total items, so it can only fail
179            // if the pool was closed — in which case dropping the item
180            // is the right thing.
181            let _ = self.pool.sender.try_send(item);
182        }
183    }
184}
185
186/// Owned counterpart to [`PoolGuard`] for `'static` contexts (e.g.
187/// `spawn_blocking`). The item is returned to the pool automatically on Drop
188/// via [`Self::checkin`], or if the guard is forgotten, via the Drop impl.
189///
190/// After a panic the guard is dropped during unwind, so the item is recovered
191/// without requiring the caller to manually invoke `checkin`.
192pub struct OwnedReservation<T> {
193    sender: async_channel::Sender<T>,
194}
195
196impl<T> OwnedReservation<T> {
197    /// Return the item to the pool from a synchronous (blocking) context.
198    /// Silently drops the item if the pool has been closed.
199    pub fn checkin(self, item: T) {
200        let _ = self.sender.try_send(item);
201    }
202
203    /// Create an RAII guard that holds both the item and the reservation.
204    /// On drop (including during panic unwind) the item is returned to the pool.
205    pub fn guard(self, item: T) -> PoolItemGuard<T> {
206        PoolItemGuard {
207            reservation: self,
208            item: Some(item),
209        }
210    }
211}
212
213/// RAII guard that couples an owned pool item with its reservation.
214///
215/// On drop the item is automatically checked back into the pool. This is the
216/// recommended pattern for `spawn_blocking` tasks where a panic would otherwise
217/// leak the pool slot.
218pub struct PoolItemGuard<T> {
219    reservation: OwnedReservation<T>,
220    item: Option<T>,
221}
222
223impl<T> PoolItemGuard<T> {
224    /// Mutable access to the inner item.
225    pub fn item_mut(&mut self) -> &mut T {
226        self.item
227            .as_mut()
228            .expect("PoolItemGuard item already taken")
229    }
230
231    /// Immutable access to the inner item.
232    pub fn item(&self) -> &T {
233        self.item
234            .as_ref()
235            .expect("PoolItemGuard item already taken")
236    }
237
238    /// Consume the guard and return the item, **without** checking it back in.
239    /// The caller is responsible for returning the item via `checkin`.
240    pub fn into_inner(mut self) -> T {
241        self.item.take().expect("PoolItemGuard item already taken")
242    }
243}
244
245impl<T> Deref for PoolItemGuard<T> {
246    type Target = T;
247    fn deref(&self) -> &T {
248        self.item()
249    }
250}
251
252impl<T> DerefMut for PoolItemGuard<T> {
253    fn deref_mut(&mut self) -> &mut T {
254        self.item_mut()
255    }
256}
257
258impl<T> Drop for PoolItemGuard<T> {
259    fn drop(&mut self) {
260        if let Some(item) = self.item.take() {
261            let _ = self.reservation.sender.try_send(item);
262        }
263    }
264}