Skip to main content

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}