sqry_nl/classifier/tokenizer_wrapper.rs
1//! `SharedClassifier`: one slot owning one loaded [`IntentClassifier`].
2//!
3//! Each pool slot wraps a single [`SharedClassifier`]. The
4//! `Arc<parking_lot::Mutex<...>>` layer is necessary because
5//! [`ort::session::Session::run`](ort::session::Session) requires
6//! `&mut self` while the daemon serves shared-reference dispatch.
7//! Cloning the inner [`Arc`] is cheap; a clone re-circulates the
8//! **same** loaded session, **NOT** a fan-out across slots. Distinct
9//! slots = distinct [`SharedClassifier`] instances created via separate
10//! [`IntentClassifier::load`] calls (see NL07 `ClassifierPool`).
11//!
12//! [`parking_lot::Mutex`] is used per the workspace memory & concurrency
13//! rule (NOT [`std::sync::Mutex`]). `parking_lot` mutexes are not
14//! poisoned, so a panic during `Session::run` leaves the mutex usable —
15//! important for the NL07 scopeguard panic-safety contract.
16//!
17//! The guarded type is `!Sync` (it owns an [`ort::session::Session`]),
18//! but the wrapper itself is `Send + Sync + Clone` because the
19//! [`parking_lot::Mutex`] guarantees exclusive access at runtime. A
20//! compile-time assertion below pins these auto-trait bounds so future
21//! refactors cannot silently break them.
22
23use crate::classifier::IntentClassifier;
24use parking_lot::Mutex;
25use std::sync::Arc;
26
27/// Shared, lockable handle to a single loaded [`IntentClassifier`].
28///
29/// One handle = one loaded model session. Cloning is `O(1)` and
30/// recirculates the same underlying session; it does NOT duplicate
31/// model weights or fan out to a parallel session. To serve N
32/// concurrent inferences, allocate N independent
33/// [`SharedClassifier`] instances (one [`IntentClassifier::load`]
34/// each) — see NL07's `ClassifierPool`.
35#[derive(Clone)]
36pub struct SharedClassifier(pub(crate) Arc<Mutex<IntentClassifier>>);
37
38impl SharedClassifier {
39 /// Wrap an already-loaded [`IntentClassifier`] for shared,
40 /// lock-mediated access.
41 #[must_use]
42 pub fn new(classifier: IntentClassifier) -> Self {
43 Self(Arc::new(Mutex::new(classifier)))
44 }
45
46 /// Acquire the lock for `&mut` access (required by
47 /// [`ort::session::Session::run`](ort::session::Session)).
48 ///
49 /// Use the returned guard within a small scope; do **NOT** hold it
50 /// across `.await` points (the wrapping crate is sync-only anyway,
51 /// but this rule still applies to any future async wrapper).
52 ///
53 /// The returned [`parking_lot::MutexGuard`] is itself `#[must_use]`,
54 /// so dropping it on the floor is a clippy warning at the call
55 /// site.
56 pub fn lock(&self) -> parking_lot::MutexGuard<'_, IntentClassifier> {
57 self.0.lock()
58 }
59
60 /// Stable identity for this slot's underlying `Arc<Mutex<_>>`.
61 ///
62 /// Distinct loaded sessions = distinct identities. Used by the
63 /// NL07 integration test
64 /// (`sqry-nl/tests/pool_concurrent_load.rs`) to assert that the
65 /// pool's `N` slots map to `N` independent `IntentClassifier`
66 /// instances. Cloning a [`SharedClassifier`] re-circulates the
67 /// same `Arc` and therefore returns the same identity.
68 #[must_use]
69 pub fn identity(&self) -> usize {
70 Arc::as_ptr(&self.0) as usize
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 /// Compile-time assertion: [`SharedClassifier`] must be
79 /// `Send + Sync + Clone` so the NL07 pool can hand clones across
80 /// daemon worker threads. If a future refactor smuggles in a
81 /// non-`Send`/`Sync` field, this stops compiling.
82 #[test]
83 fn shared_classifier_is_send_sync_clone() {
84 fn assert_send<T: Send>() {}
85 fn assert_sync<T: Sync>() {}
86 fn assert_clone<T: Clone>() {}
87 assert_send::<SharedClassifier>();
88 assert_sync::<SharedClassifier>();
89 assert_clone::<SharedClassifier>();
90 }
91}