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}