1use std::collections::HashMap;
8use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
9use std::sync::{Arc, OnceLock};
10
11use async_trait::async_trait;
12use lattice_embed::{
13 CachedEmbeddingService, EmbeddingModel, EmbeddingRole, EmbeddingService,
14 NativeEmbeddingService, DEFAULT_MAX_BATCH_SIZE, MAX_TEXT_BYTES,
15};
16use tokio::sync::{mpsc, Notify, OnceCell};
17
18use crate::error::{RuntimeError, RuntimeResult};
19
20const ADMISSION_WAITING: u8 = 1;
21const ADMISSION_ACCEPTED: u8 = 2;
22
23struct EmbeddingAdmission {
24 deadline: khive_storage::RequestReadDeadline,
25 request: khive_storage::RequestReadContext,
26 state: AtomicU8,
27 changed: Notify,
28}
29
30impl EmbeddingAdmission {
31 fn expired(&self) -> bool {
32 tokio::time::Instant::now() >= self.deadline.async_at()
33 || self.request.stop_reason().is_some()
34 }
35}
36
37tokio::task_local! {
38 static EMBEDDING_ADMISSION: Arc<EmbeddingAdmission>;
39}
40
41pub(crate) async fn with_embedding_admission<T>(
44 future: impl std::future::Future<Output = lattice_embed::Result<T>>,
45) -> RuntimeResult<T> {
46 let deadline = khive_storage::effective_request_read_deadline(
47 khive_storage::RequestReadDeadline::after(khive_storage::request_read_timeout_from_env()),
48 );
49 let timeout = deadline
50 .async_at()
51 .saturating_duration_since(tokio::time::Instant::now());
52 let admission = Arc::new(EmbeddingAdmission {
53 deadline,
54 request: khive_storage::capture_request_read_context(),
55 state: AtomicU8::new(0),
56 changed: Notify::new(),
57 });
58 EMBEDDING_ADMISSION
59 .scope(Arc::clone(&admission), async move {
60 tokio::pin!(future);
61 let stopped = async {
62 tokio::select! {
63 _ = tokio::time::sleep_until(deadline.async_at()) => {},
64 _ = admission.request.clone().wait_for_stop() => {},
65 }
66 };
67 tokio::pin!(stopped);
68 let mut bound_expired = false;
69 loop {
70 tokio::select! {
71 biased;
73 result = &mut future => return result.map_err(RuntimeError::from),
74 _ = admission.changed.notified() => {},
75 _ = &mut stopped, if !bound_expired => bound_expired = true,
76 }
77 match admission.state.load(Ordering::Acquire) {
78 ADMISSION_ACCEPTED => return future.await.map_err(RuntimeError::from),
79 ADMISSION_WAITING if bound_expired => {
80 return Err(RuntimeError::Storage(
81 khive_storage::StorageError::AdmissionTimeout {
82 operation: "embedding admission".into(),
83 timeout_ms: u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX),
84 pool_identity: None,
85 },
86 ));
87 }
88 _ => {}
91 }
92 }
93 })
94 .await
95}
96
97#[derive(Clone, Copy)]
98enum EmbeddingCall {
99 Generic,
100 Query,
101 Passage,
102}
103
104const EMBEDDING_QUEUE_CAPACITY: usize = 32;
105const EMBEDDING_MAX_JOB_BYTES: usize = DEFAULT_MAX_BATCH_SIZE * MAX_TEXT_BYTES;
106const EMBEDDING_QUEUE_BYTE_BUDGET: usize = EMBEDDING_QUEUE_CAPACITY * 128 * MAX_TEXT_BYTES;
108
109struct InFlightBytes {
110 counter: Arc<AtomicUsize>,
111 bytes: usize,
112}
113
114impl InFlightBytes {
115 fn reserve(
116 counter: Arc<AtomicUsize>,
117 byte_budget: usize,
118 bytes: usize,
119 ) -> lattice_embed::Result<Self> {
120 let mut current = counter.load(Ordering::Acquire);
121 loop {
122 let Some(next) = current.checked_add(bytes) else {
123 return Err(lattice_embed::EmbedError::Internal(format!(
124 "embedding worker byte budget exceeded: in-flight byte count overflowed the {byte_budget}-byte budget"
125 )));
126 };
127 if next > byte_budget {
128 return Err(lattice_embed::EmbedError::Internal(format!(
129 "embedding worker byte budget exceeded: {current} in flight + {bytes} job bytes > {byte_budget}"
130 )));
131 }
132 match counter.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire)
133 {
134 Ok(_) => return Ok(Self { counter, bytes }),
135 Err(observed) => current = observed,
136 }
137 }
138 }
139}
140
141impl Drop for InFlightBytes {
142 fn drop(&mut self) {
143 let previous = self.counter.fetch_sub(self.bytes, Ordering::AcqRel);
144 debug_assert!(previous >= self.bytes, "embedding byte counter underflow");
145 }
146}
147
148struct EmbeddingJob {
149 texts: Vec<String>,
150 model: EmbeddingModel,
151 call: EmbeddingCall,
152 reply: tokio::sync::oneshot::Sender<lattice_embed::Result<Vec<Vec<f32>>>>,
153 _in_flight: InFlightBytes,
154}
155
156pub(crate) struct BlockingEmbeddingService<S> {
159 inner: Arc<S>,
160 worker: OnceLock<Result<mpsc::Sender<EmbeddingJob>, String>>,
161 in_flight_bytes: Arc<AtomicUsize>,
162 byte_budget: usize,
163}
164
165impl<S> BlockingEmbeddingService<S> {
166 pub(crate) fn new(inner: Arc<S>) -> Self {
167 Self {
168 inner,
169 worker: OnceLock::new(),
170 in_flight_bytes: Arc::new(AtomicUsize::new(0)),
171 byte_budget: EMBEDDING_QUEUE_BYTE_BUDGET,
172 }
173 }
174
175 #[cfg(test)]
176 fn with_byte_budget(inner: Arc<S>, byte_budget: usize) -> Self {
177 Self {
178 inner,
179 worker: OnceLock::new(),
180 in_flight_bytes: Arc::new(AtomicUsize::new(0)),
181 byte_budget,
182 }
183 }
184}
185
186impl<S: EmbeddingService + 'static> BlockingEmbeddingService<S> {
187 fn input_bytes(texts: &[String]) -> lattice_embed::Result<usize> {
188 if texts.is_empty() {
189 return Err(lattice_embed::EmbedError::InvalidInput(
190 "no texts provided".to_owned(),
191 ));
192 }
193 let input_bytes = texts.iter().try_fold(0usize, |total, text| {
194 total.checked_add(text.len()).ok_or_else(|| {
195 lattice_embed::EmbedError::InvalidInput(format!(
196 "embedding job input exceeds the {EMBEDDING_MAX_JOB_BYTES}-byte maximum"
197 ))
198 })
199 })?;
200 if input_bytes > EMBEDDING_MAX_JOB_BYTES {
201 return Err(lattice_embed::EmbedError::InvalidInput(format!(
202 "embedding job input is {input_bytes} bytes; maximum is {EMBEDDING_MAX_JOB_BYTES} bytes"
203 )));
204 }
205 if texts.len() > DEFAULT_MAX_BATCH_SIZE {
206 return Err(lattice_embed::EmbedError::InvalidInput(format!(
207 "batch size {} exceeds maximum {DEFAULT_MAX_BATCH_SIZE}",
208 texts.len()
209 )));
210 }
211 if let Some(text) = texts.iter().find(|text| text.len() > MAX_TEXT_BYTES) {
212 return Err(lattice_embed::EmbedError::TextTooLong {
213 length: text.len(),
214 max: MAX_TEXT_BYTES,
215 });
216 }
217 Ok(input_bytes)
218 }
219
220 fn worker(&self) -> lattice_embed::Result<&mpsc::Sender<EmbeddingJob>> {
221 self.worker
222 .get_or_init(|| {
223 let (sender, receiver) = mpsc::channel(EMBEDDING_QUEUE_CAPACITY);
224 let inner = Arc::clone(&self.inner);
225 let runtime = tokio::runtime::Handle::current();
226 std::thread::Builder::new()
227 .name("khive-embedding".to_owned())
228 .spawn(move || Self::run_worker(inner, runtime, receiver))
229 .map(|_| sender)
230 .map_err(|error| error.to_string())
231 })
232 .as_ref()
233 .map_err(|error| lattice_embed::EmbedError::Internal(error.clone()))
234 }
235
236 fn run_worker(
237 inner: Arc<S>,
238 runtime: tokio::runtime::Handle,
239 mut receiver: mpsc::Receiver<EmbeddingJob>,
240 ) {
241 while let Some(job) = receiver.blocking_recv() {
242 if job.reply.is_closed() {
243 continue;
244 }
245 let result = runtime.block_on(async {
246 match job.call {
247 EmbeddingCall::Generic => inner.embed(&job.texts, job.model).await,
248 EmbeddingCall::Query => inner.embed_query(&job.texts, job.model).await,
249 EmbeddingCall::Passage => inner.embed_passage(&job.texts, job.model).await,
250 }
251 });
252 let _ = job.reply.send(result);
253 }
254 }
255
256 async fn run(
257 &self,
258 texts: &[String],
259 model: EmbeddingModel,
260 call: EmbeddingCall,
261 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
262 let input_bytes = Self::input_bytes(texts)?;
263 let sender = self.worker()?;
264 let admission = EMBEDDING_ADMISSION.try_with(Arc::clone).ok();
265 let permit = if let Some(admission) = &admission {
266 admission.state.store(ADMISSION_WAITING, Ordering::Release);
267 admission.changed.notify_one();
268 if admission.expired() {
269 return std::future::pending().await;
270 }
271 sender.reserve().await.map_err(|_| {
272 lattice_embed::EmbedError::Internal(
273 "embedding worker channel is disconnected".to_owned(),
274 )
275 })?
276 } else {
277 sender.try_reserve().map_err(|error| match error {
280 mpsc::error::TrySendError::Full(_) => {
281 lattice_embed::EmbedError::Internal("embedding worker queue is full".to_owned())
282 }
283 mpsc::error::TrySendError::Closed(_) => lattice_embed::EmbedError::Internal(
284 "embedding worker channel is disconnected".to_owned(),
285 ),
286 })?
287 };
288 if admission
291 .as_ref()
292 .is_some_and(|admission| admission.expired())
293 {
294 return std::future::pending().await;
295 }
296 let in_flight = InFlightBytes::reserve(
297 Arc::clone(&self.in_flight_bytes),
298 self.byte_budget,
299 input_bytes,
300 )?;
301 let (reply, receiver) = tokio::sync::oneshot::channel();
302 let job = EmbeddingJob {
303 texts: texts.to_vec(),
304 model,
305 call,
306 reply,
307 _in_flight: in_flight,
308 };
309 if let Some(admission) = &admission {
310 if admission.expired() {
311 return std::future::pending().await;
312 }
313 admission.state.store(ADMISSION_ACCEPTED, Ordering::Release);
314 admission.changed.notify_one();
315 }
316 permit.send(job);
317 receiver
318 .await
319 .map_err(|error| lattice_embed::EmbedError::Internal(error.to_string()))?
320 }
321}
322
323#[async_trait]
324impl<S: EmbeddingService + 'static> EmbeddingService for BlockingEmbeddingService<S> {
325 async fn embed(
326 &self,
327 texts: &[String],
328 model: EmbeddingModel,
329 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
330 self.run(texts, model, EmbeddingCall::Generic).await
331 }
332
333 async fn embed_with_role(
334 &self,
335 texts: &[String],
336 model: EmbeddingModel,
337 role: EmbeddingRole,
338 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
339 let call = match role {
342 EmbeddingRole::Generic => EmbeddingCall::Generic,
343 EmbeddingRole::Query => EmbeddingCall::Query,
344 EmbeddingRole::Passage => EmbeddingCall::Passage,
345 _ => {
346 return Err(lattice_embed::EmbedError::InvalidInput(
347 "unsupported embedding role".to_owned(),
348 ))
349 }
350 };
351 self.run(texts, model, call).await
352 }
353
354 async fn embed_query(
355 &self,
356 texts: &[String],
357 model: EmbeddingModel,
358 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
359 self.run(texts, model, EmbeddingCall::Query).await
360 }
361
362 async fn embed_passage(
363 &self,
364 texts: &[String],
365 model: EmbeddingModel,
366 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
367 self.run(texts, model, EmbeddingCall::Passage).await
368 }
369
370 fn model_config(&self, model: EmbeddingModel) -> lattice_embed::ModelConfig {
371 self.inner.model_config(model)
372 }
373
374 fn supports_model(&self, model: EmbeddingModel) -> bool {
375 self.inner.supports_model(model)
376 }
377
378 fn name(&self) -> &'static str {
379 self.inner.name()
380 }
381}
382
383#[async_trait]
393pub trait EmbedderProvider: Send + Sync {
394 fn name(&self) -> &str;
401
402 fn dimensions(&self) -> usize;
407
408 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>>;
414}
415
416pub(crate) struct EmbedderEntry {
419 provider: Arc<dyn EmbedderProvider>,
420 cell: Arc<OnceCell<Arc<dyn EmbeddingService>>>,
421 serving: Arc<AtomicBool>,
422 audited_document_preparation: bool,
425}
426
427impl Clone for EmbedderEntry {
428 fn clone(&self) -> Self {
429 Self {
430 provider: Arc::clone(&self.provider),
431 cell: Arc::clone(&self.cell),
432 serving: Arc::clone(&self.serving),
433 audited_document_preparation: self.audited_document_preparation,
434 }
435 }
436}
437
438#[derive(Clone, Default)]
445pub struct EmbedderRegistry {
446 entries: HashMap<String, EmbedderEntry>,
447 ordered_names: Vec<String>,
448}
449
450impl EmbedderRegistry {
451 pub fn new() -> Self {
453 Self {
454 entries: HashMap::new(),
455 ordered_names: Vec::new(),
456 }
457 }
458
459 pub fn register<P: EmbedderProvider + 'static>(&mut self, provider: P) -> RuntimeResult<()> {
465 self.insert(provider, false)
466 }
467
468 pub(crate) fn register_builtin(&mut self, provider: LatticeEmbedderProvider) {
471 self.insert(provider, true)
472 .expect("configured built-in embedding models must have unique names");
473 }
474
475 #[cfg(feature = "test-internals")]
478 pub fn register_test_audited<P: EmbedderProvider + 'static>(
479 &mut self,
480 model: EmbeddingModel,
481 provider: P,
482 ) {
483 assert_eq!(provider.name(), model.to_string());
484 self.insert(TestAuditedProvider { provider }, true)
485 .expect("test audited provider names must be unique");
486 }
487
488 fn insert<P: EmbedderProvider + 'static>(
489 &mut self,
490 provider: P,
491 audited_document_preparation: bool,
492 ) -> RuntimeResult<()> {
493 let name = provider.name().to_owned();
494 if self
495 .entries
496 .get(&name)
497 .is_some_and(|entry| entry.serving.load(Ordering::Acquire))
498 {
499 return Err(RuntimeError::InvalidInput(format!(
500 "embedding provider `{name}` is already serving"
501 )));
502 }
503 let entry = EmbedderEntry {
504 provider: Arc::new(provider),
505 cell: Arc::new(OnceCell::new()),
506 serving: Arc::new(AtomicBool::new(false)),
507 audited_document_preparation,
508 };
509 if self.entries.contains_key(&name) {
510 self.entries.insert(name, entry);
511 } else {
512 self.ordered_names.push(name.clone());
513 self.entries.insert(name, entry);
514 }
515 Ok(())
516 }
517
518 pub fn get_provider(&self, name: &str) -> Option<&dyn EmbedderProvider> {
520 self.entries.get(name).map(|e| e.provider.as_ref())
521 }
522
523 pub fn contains(&self, name: &str) -> bool {
525 self.entries.contains_key(name)
526 }
527
528 pub fn names(&self) -> Vec<String> {
530 self.ordered_names.clone()
531 }
532
533 pub(crate) fn get_entry(&self, name: &str) -> Option<EmbedderEntry> {
541 let entry = self.entries.get(name)?;
542 entry.serving.store(true, Ordering::Release);
543 Some(entry.clone())
544 }
545
546 pub async fn get_service(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
555 let entry = self
556 .entries
557 .get(name)
558 .ok_or_else(|| RuntimeError::UnknownModel(name.to_string()))?
559 .clone();
560 entry.serving.store(true, Ordering::Release);
561
562 Ok(entry.resolve().await?.0)
563 }
564}
565
566#[cfg(feature = "test-internals")]
567struct TestAuditedProvider<P> {
568 provider: P,
569}
570
571#[cfg(feature = "test-internals")]
572#[async_trait]
573impl<P: EmbedderProvider> EmbedderProvider for TestAuditedProvider<P> {
574 fn name(&self) -> &str {
575 self.provider.name()
576 }
577
578 fn dimensions(&self) -> usize {
579 self.provider.dimensions()
580 }
581
582 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
583 Ok(Arc::new(TestAuditedService(self.provider.build().await?)))
584 }
585}
586
587#[cfg(feature = "test-internals")]
591struct TestAuditedService(Arc<dyn EmbeddingService>);
592
593#[cfg(feature = "test-internals")]
594#[async_trait]
595impl EmbeddingService for TestAuditedService {
596 async fn embed(
597 &self,
598 texts: &[String],
599 model: EmbeddingModel,
600 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
601 self.0.embed(texts, model).await
602 }
603
604 fn supports_model(&self, model: EmbeddingModel) -> bool {
605 self.0.supports_model(model)
606 }
607
608 fn name(&self) -> &'static str {
609 self.0.name()
610 }
611}
612
613impl EmbedderEntry {
614 pub(crate) fn cached_service(&self) -> Option<Arc<dyn EmbeddingService>> {
615 self.cell.get().map(Arc::clone)
616 }
617
618 pub(crate) fn has_audited_document_preparation(&self) -> bool {
619 self.audited_document_preparation
620 }
621
622 pub(crate) async fn resolve(self) -> RuntimeResult<(Arc<dyn EmbeddingService>, Option<i64>)> {
633 let mut own_init_duration_us: Option<i64> = None;
634 let provider = Arc::clone(&self.provider);
635 let init_duration_us = &mut own_init_duration_us;
636 let svc = self
637 .cell
638 .get_or_try_init(|| async move {
639 let init_start = std::time::Instant::now();
640 let svc = provider.build().await.map_err(|e| {
641 crate::error::RuntimeError::Internal(format!(
642 "EmbedderProvider '{}' build() failed: {e}",
643 provider.name()
644 ))
645 })?;
646 *init_duration_us = Some(init_start.elapsed().as_micros() as i64);
647 Ok::<_, RuntimeError>(svc)
648 })
649 .await?;
650 Ok((Arc::clone(svc), own_init_duration_us))
651 }
652}
653
654pub struct LatticeEmbedderProvider {
664 model: EmbeddingModel,
665 name: String,
667}
668
669impl LatticeEmbedderProvider {
670 pub fn new(model: EmbeddingModel) -> Self {
672 let name = model.to_string();
673 Self { model, name }
674 }
675}
676
677#[async_trait]
678impl EmbedderProvider for LatticeEmbedderProvider {
679 fn name(&self) -> &str {
680 &self.name
681 }
682
683 fn dimensions(&self) -> usize {
684 self.model.dimensions()
685 }
686
687 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
688 let native = Arc::new(NativeEmbeddingService::with_model(self.model));
689 native.ensure_loaded().await?;
690 Ok(cached_blocking_service(native))
691 }
692}
693
694fn cached_blocking_service<S: EmbeddingService + 'static>(
696 inner: Arc<S>,
697) -> Arc<dyn EmbeddingService> {
698 let blocking = Arc::new(BlockingEmbeddingService::new(inner));
699 Arc::new(CachedEmbeddingService::with_default_cache(blocking))
700}
701
702#[cfg(test)]
703#[path = "embedding_cache_admission_tests.rs"]
704mod cache_admission_tests;
705
706#[cfg(test)]
709mod tests {
710 use super::*;
711 use std::collections::HashSet;
712 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
713 use std::sync::{Condvar, Mutex};
714 use std::time::Duration;
715 use tokio::sync::Notify;
716
717 struct ConstVecProvider {
718 name: String,
719 dims: usize,
720 build_calls: Arc<AtomicUsize>,
721 }
722
723 impl ConstVecProvider {
724 fn new(name: &str, dims: usize) -> Self {
725 Self {
726 name: name.to_owned(),
727 dims,
728 build_calls: Arc::new(AtomicUsize::new(0)),
729 }
730 }
731 }
732
733 pub(super) struct ConstVecService {
737 pub(super) dims: usize,
738 }
739
740 #[async_trait]
741 impl EmbeddingService for ConstVecService {
742 async fn embed(
743 &self,
744 texts: &[String],
745 _model: EmbeddingModel,
746 ) -> std::result::Result<Vec<Vec<f32>>, lattice_embed::EmbedError> {
747 Ok(texts.iter().map(|_| vec![1.0_f32; self.dims]).collect())
748 }
749
750 fn supports_model(&self, _model: EmbeddingModel) -> bool {
751 true
752 }
753
754 fn name(&self) -> &'static str {
755 "const-vec-service"
756 }
757 }
758
759 #[async_trait]
760 impl EmbedderProvider for ConstVecProvider {
761 fn name(&self) -> &str {
762 &self.name
763 }
764
765 fn dimensions(&self) -> usize {
766 self.dims
767 }
768
769 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
770 self.build_calls.fetch_add(1, Ordering::SeqCst);
771 Ok(Arc::new(ConstVecService { dims: self.dims }))
772 }
773 }
774
775 #[test]
776 fn builtin_provider_can_be_replaced_before_resolution_without_reordering() {
777 let model = EmbeddingModel::MultilingualE5Small;
778 let name = model.to_string();
779 let mut registry = EmbedderRegistry::new();
780 registry.register_builtin(LatticeEmbedderProvider::new(model));
781 let original_names = registry.names();
782
783 registry
784 .register(ConstVecProvider::new(&name, model.dimensions()))
785 .expect("pre-serving replacement should succeed");
786 assert_eq!(registry.names(), original_names);
787 assert!(!registry
788 .get_entry(&name)
789 .expect("replacement entry")
790 .has_audited_document_preparation());
791 }
792
793 struct FirstLoadBlockingService {
794 loaded: AtomicBool,
795 }
796
797 #[async_trait]
798 impl EmbeddingService for FirstLoadBlockingService {
799 async fn embed(
800 &self,
801 texts: &[String],
802 _model: EmbeddingModel,
803 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
804 if !self.loaded.swap(true, Ordering::SeqCst) {
805 tokio::task::spawn_blocking(|| {})
806 .await
807 .map_err(|error| lattice_embed::EmbedError::Internal(error.to_string()))?;
808 }
809 Ok(texts.iter().map(|_| vec![1.0]).collect())
810 }
811
812 fn supports_model(&self, _model: EmbeddingModel) -> bool {
813 true
814 }
815
816 fn name(&self) -> &'static str {
817 "first-load-blocking-service"
818 }
819 }
820
821 pub(super) struct BlockingTestService {
822 pub(super) calls: Mutex<Vec<String>>,
823 pub(super) entered: AtomicUsize,
824 release: (Mutex<bool>, Condvar),
825 pub(super) thread_ids: Mutex<HashSet<std::thread::ThreadId>>,
826 }
827
828 impl BlockingTestService {
829 pub(super) fn new() -> Self {
830 Self {
831 calls: Mutex::new(Vec::new()),
832 entered: AtomicUsize::new(0),
833 release: (Mutex::new(false), Condvar::new()),
834 thread_ids: Mutex::new(HashSet::new()),
835 }
836 }
837
838 pub(super) fn release(&self) {
839 *self
840 .release
841 .0
842 .lock()
843 .expect("release lock must not be poisoned") = true;
844 self.release.1.notify_all();
845 }
846 }
847
848 #[async_trait]
849 impl EmbeddingService for BlockingTestService {
850 async fn embed(
851 &self,
852 texts: &[String],
853 _model: EmbeddingModel,
854 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
855 let text = texts.first().cloned().unwrap_or_default();
856 self.thread_ids
857 .lock()
858 .expect("thread id lock must not be poisoned")
859 .insert(std::thread::current().id());
860 self.calls
861 .lock()
862 .expect("call lock must not be poisoned")
863 .push(text.clone());
864 self.entered.fetch_add(1, Ordering::Release);
865
866 if text != "later" {
867 let (released, wake) = &self.release;
868 let guard = released.lock().expect("release lock must not be poisoned");
869 let _guard = wake
870 .wait_while(guard, |released| !*released)
871 .expect("release lock must not be poisoned");
872 }
873
874 Ok(texts.iter().map(|_| vec![1.0]).collect())
875 }
876
877 fn supports_model(&self, _model: EmbeddingModel) -> bool {
878 true
879 }
880
881 fn name(&self) -> &'static str {
882 "blocking-test-service"
883 }
884 }
885
886 #[test]
887 fn blocking_adapter_first_use_completes_with_single_blocking_thread() {
888 let runtime = tokio::runtime::Builder::new_current_thread()
889 .enable_time()
890 .max_blocking_threads(1)
891 .build()
892 .expect("current-thread runtime must build");
893 let service = BlockingEmbeddingService::new(Arc::new(FirstLoadBlockingService {
894 loaded: AtomicBool::new(false),
895 }));
896
897 let result = runtime.block_on(async {
898 tokio::time::timeout(
899 Duration::from_secs(5),
900 service.embed(&["first use".to_owned()], EmbeddingModel::default()),
901 )
902 .await
903 });
904 runtime.shutdown_timeout(Duration::from_secs(1));
905
906 let embeddings = result
907 .expect("first-use embedding must not exhaust the blocking pool")
908 .expect("first-use embedding must succeed");
909 assert_eq!(embeddings, vec![vec![1.0]]);
910 }
911
912 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
913 async fn blocking_adapter_uses_one_worker_for_concurrent_calls() {
914 const CALL_COUNT: usize = 32;
915 let inner = Arc::new(BlockingTestService::new());
916 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
917 let mut calls = Vec::with_capacity(CALL_COUNT);
918
919 for index in 0..CALL_COUNT {
920 let service = Arc::clone(&service);
921 calls.push(tokio::spawn(async move {
922 service
923 .embed(&[format!("request-{index}")], EmbeddingModel::default())
924 .await
925 }));
926 }
927
928 let _ = tokio::time::timeout(Duration::from_millis(250), async {
929 while inner.entered.load(Ordering::Acquire) < 2 {
930 tokio::task::yield_now().await;
931 }
932 })
933 .await;
934 inner.release();
935
936 for call in calls {
937 call.await
938 .expect("embedding task must not panic")
939 .expect("embedding call must succeed");
940 }
941 assert_eq!(
942 inner
943 .thread_ids
944 .lock()
945 .expect("thread id lock must not be poisoned")
946 .len(),
947 1,
948 "concurrent calls must share one native worker thread"
949 );
950 }
951
952 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
953 async fn blocking_adapter_skips_timed_out_call_and_serves_later_call() {
954 let inner = Arc::new(BlockingTestService::new());
955 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
956
957 let first_service = Arc::clone(&service);
958 let first = tokio::spawn(async move {
959 first_service
960 .embed(&["first".to_owned()], EmbeddingModel::default())
961 .await
962 });
963 tokio::time::timeout(Duration::from_secs(1), async {
964 while inner.entered.load(Ordering::Acquire) == 0 {
965 tokio::task::yield_now().await;
966 }
967 })
968 .await
969 .expect("first embedding call must enter the native service");
970
971 let abandoned = tokio::time::timeout(
972 Duration::from_millis(50),
973 service.embed(&["abandoned".to_owned()], EmbeddingModel::default()),
974 )
975 .await;
976 let later_service = Arc::clone(&service);
977 let later = tokio::spawn(async move {
978 later_service
979 .embed(&["later".to_owned()], EmbeddingModel::default())
980 .await
981 });
982 inner.release();
983
984 first
985 .await
986 .expect("first embedding task must not panic")
987 .expect("first embedding call must succeed");
988 let later_result = tokio::time::timeout(Duration::from_secs(1), later)
989 .await
990 .expect("later embedding call must be served")
991 .expect("later embedding task must not panic")
992 .expect("later embedding call must succeed");
993
994 assert!(abandoned.is_err(), "queued embedding call must time out");
995 assert_eq!(later_result, vec![vec![1.0]]);
996 assert_eq!(
997 *inner.calls.lock().expect("call lock must not be poisoned"),
998 vec!["first".to_owned(), "later".to_owned()],
999 "the worker must skip a queued call whose receiver is closed"
1000 );
1001 }
1002
1003 pub(super) struct ReleaseWorkerOnDrop(pub(super) Arc<BlockingTestService>);
1004
1005 impl Drop for ReleaseWorkerOnDrop {
1006 fn drop(&mut self) {
1007 self.0.release();
1008 }
1009 }
1010
1011 struct ServiceProvider(Arc<dyn EmbeddingService>);
1012
1013 #[async_trait]
1014 impl EmbedderProvider for ServiceProvider {
1015 fn name(&self) -> &str {
1016 "queue-test"
1017 }
1018 fn dimensions(&self) -> usize {
1019 1
1020 }
1021 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
1022 Ok(Arc::clone(&self.0))
1023 }
1024 }
1025
1026 pub(super) fn queue_runtime(service: Arc<dyn EmbeddingService>) -> crate::KhiveRuntime {
1027 let runtime = crate::KhiveRuntime::memory().expect("memory runtime");
1028 runtime.register_embedder(ServiceProvider(service));
1029 runtime
1030 }
1031
1032 pub(super) async fn poll_once<F: std::future::Future + ?Sized>(
1033 mut future: std::pin::Pin<&mut F>,
1034 ) -> std::task::Poll<F::Output> {
1035 std::future::poll_fn(|cx| std::task::Poll::Ready(future.as_mut().poll(cx))).await
1036 }
1037
1038 pub(super) async fn wait_for_entered(inner: &BlockingTestService, count: usize) {
1039 let watchdog = std::time::Instant::now() + Duration::from_secs(2);
1040 while inner.entered.load(Ordering::Acquire) < count {
1041 assert!(
1042 std::time::Instant::now() < watchdog,
1043 "setup watchdog: native worker did not enter"
1044 );
1045 tokio::task::yield_now().await;
1046 }
1047 }
1048
1049 async fn drive_until_entered<F: std::future::Future + ?Sized>(
1050 mut future: std::pin::Pin<&mut F>,
1051 inner: &BlockingTestService,
1052 count: usize,
1053 ) {
1054 let watchdog = std::time::Instant::now() + Duration::from_secs(2);
1055 while inner.entered.load(Ordering::Acquire) < count {
1056 assert!(
1057 std::time::Instant::now() < watchdog,
1058 "setup watchdog: driven runtime call did not reach the native worker"
1059 );
1060 assert!(
1061 poll_once(future.as_mut()).await.is_pending(),
1062 "held inference must remain pending while the runtime call is driven"
1063 );
1064 tokio::task::yield_now().await;
1065 }
1066 }
1067
1068 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1069 async fn runtime_embedding_waits_for_capacity_and_drains_n_plus_one_calls() {
1070 let inner = Arc::new(BlockingTestService::new());
1071 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1072 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
1073 let runtime = queue_runtime(service.clone());
1074 runtime
1075 .embedder("queue-test")
1076 .await
1077 .expect("resolve provider before the admission fixture");
1078 let first_text = vec!["first".to_owned()];
1079 let mut first = Box::pin(runtime.embed_batch_with_model("queue-test", &first_text));
1080 assert!(poll_once(first.as_mut()).await.is_pending());
1081 drive_until_entered(first.as_mut(), &inner, 1).await;
1082
1083 let texts: Vec<_> = (0..EMBEDDING_QUEUE_CAPACITY)
1084 .map(|index| vec![format!("queued-{index}")])
1085 .collect();
1086 let mut queued = Vec::new();
1087 for text in &texts {
1088 let mut call = Box::pin(runtime.embed_batch_with_model("queue-test", text));
1089 assert!(
1090 poll_once(call.as_mut()).await.is_pending(),
1091 "the bounded queue must admit its N actual runtime calls"
1092 );
1093 queued.push(call);
1094 }
1095 assert_eq!(
1096 service.in_flight_bytes.load(Ordering::Acquire),
1097 first_text.iter().map(String::len).sum::<usize>()
1098 + texts.iter().flatten().map(String::len).sum::<usize>(),
1099 "the held worker and N queued runtime jobs must own their exact input bytes"
1100 );
1101 let overflow_text = vec!["overflow".to_owned()];
1102 let mut overflow = Box::pin(runtime.embed_batch_with_model("queue-test", &overflow_text));
1103 let overflow_before_release = poll_once(overflow.as_mut()).await;
1104 inner.release();
1105 assert!(
1106 overflow_before_release.is_pending(),
1107 "request N+1 must wait for a slot instead of failing: {overflow_before_release:?}"
1108 );
1109 assert_eq!(first.await.unwrap(), vec![vec![1.0]]);
1110 for call in queued {
1111 assert_eq!(call.await.unwrap(), vec![vec![1.0]]);
1112 }
1113 assert_eq!(overflow.await.unwrap(), vec![vec![1.0]]);
1114 assert_eq!(
1115 inner.entered.load(Ordering::Acquire),
1116 EMBEDDING_QUEUE_CAPACITY + 2
1117 );
1118 assert_eq!(inner.thread_ids.lock().unwrap().len(), 1);
1119 }
1120
1121 #[tokio::test(start_paused = true)]
1122 async fn runtime_embedding_expired_admission_is_retryable_and_never_enqueued() {
1123 let inner = Arc::new(BlockingTestService::new());
1124 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1125 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
1126 let runtime = queue_runtime(service.clone());
1127 runtime
1128 .embedder("queue-test")
1129 .await
1130 .expect("resolve provider before the admission fixture");
1131 let first_text = vec!["first".to_owned()];
1132 let mut first = Box::pin(runtime.embed_batch_with_model("queue-test", &first_text));
1133 assert!(poll_once(first.as_mut()).await.is_pending());
1134 drive_until_entered(first.as_mut(), &inner, 1).await;
1135 let texts: Vec<_> = (0..EMBEDDING_QUEUE_CAPACITY)
1136 .map(|index| vec![format!("queued-{index}")])
1137 .collect();
1138 let mut queued = Vec::new();
1139 for text in &texts {
1140 let mut call = Box::pin(runtime.embed_batch_with_model("queue-test", text));
1141 assert!(poll_once(call.as_mut()).await.is_pending());
1142 queued.push(call);
1143 }
1144 assert_eq!(
1145 service.in_flight_bytes.load(Ordering::Acquire),
1146 first_text.iter().map(String::len).sum::<usize>()
1147 + texts.iter().flatten().map(String::len).sum::<usize>(),
1148 "the held worker and N queued runtime jobs must own their exact input bytes"
1149 );
1150 let deadline = khive_storage::RequestReadDeadline::after(Duration::from_millis(100));
1151 let mut expired = Box::pin(khive_storage::scope_request_read_deadline_at(
1152 deadline,
1153 runtime.embed_with_model("queue-test", "expired"),
1154 ));
1155 assert!(
1156 poll_once(expired.as_mut()).await.is_pending(),
1157 "an unexpired full-queue call must wait"
1158 );
1159 tokio::time::advance(Duration::from_millis(100)).await;
1160 let result = poll_once(expired.as_mut()).await;
1161 inner.release();
1162 let error = match result {
1163 std::task::Poll::Ready(Err(error)) => error,
1164 other => panic!("expired admission must produce a typed runtime error: {other:?}"),
1165 };
1166 assert!(
1167 matches!(&error, RuntimeError::Storage(khive_storage::StorageError::AdmissionTimeout {
1168 operation, timeout_ms: 100, pool_identity: None,
1169 }) if operation == "embedding admission"),
1170 "{error:?}"
1171 );
1172 assert!(
1173 error.retryable_failure_context().is_some(),
1174 "pre-admission expiry must use the existing retryable classification"
1175 );
1176 let projected = crate::error_projection::runtime_error_value(
1177 error,
1178 crate::DomainDisposition::NotCommitted,
1179 );
1180 assert_eq!(projected["retryable"], true);
1181 assert_eq!(projected["operation"], "embedding admission");
1182 assert_eq!(first.await.unwrap(), vec![vec![1.0]]);
1183 for call in queued {
1184 assert_eq!(call.await.unwrap(), vec![vec![1.0]]);
1185 }
1186 assert!(
1187 !inner
1188 .calls
1189 .lock()
1190 .unwrap()
1191 .iter()
1192 .any(|text| text == "expired"),
1193 "an expired waiter must never reach inference"
1194 );
1195 }
1196
1197 #[tokio::test(start_paused = true)]
1198 async fn runtime_embedding_earlier_absolute_bound_is_not_renewed_when_slot_is_ready() {
1199 let inner = Arc::new(BlockingTestService::new());
1200 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1201 let runtime = queue_runtime(Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner))));
1202 runtime
1203 .embedder("queue-test")
1204 .await
1205 .expect("resolve provider before the admission deadline");
1206 let deadline = khive_storage::RequestReadDeadline::after(Duration::from_millis(100));
1207 tokio::time::advance(Duration::from_millis(100)).await;
1208 let mut call = Box::pin(khive_storage::scope_request_read_deadline_at(
1209 deadline,
1210 runtime.embed_with_model("queue-test", "expired-ready-slot"),
1211 ));
1212 let result = poll_once(call.as_mut()).await;
1213 inner.release();
1214 assert!(
1215 matches!(
1216 result,
1217 std::task::Poll::Ready(Err(RuntimeError::Storage(
1218 khive_storage::StorageError::AdmissionTimeout { timeout_ms: 0, .. }
1219 )))
1220 ),
1221 "an already-expired original bound must refuse even an available slot: {result:?}"
1222 );
1223 assert_eq!(
1224 inner.entered.load(Ordering::Acquire),
1225 0,
1226 "capacity becoming ready must not bypass expiry"
1227 );
1228 }
1229
1230 #[tokio::test(start_paused = true)]
1231 async fn runtime_embedding_all_six_call_families_refuse_expired_admission() {
1232 let inner = Arc::new(BlockingTestService::new());
1233 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1234 let runtime = queue_runtime(Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner))));
1235 let texts = vec!["later".to_owned()];
1236 for family in 0..6 {
1237 let result = khive_storage::scope_request_read_deadline(Duration::ZERO, async {
1238 match family {
1239 0 => runtime.embed_with_model("queue-test", "later").await,
1240 1 => runtime
1241 .embed_document_with_model_outcome("queue-test", "later")
1242 .await
1243 .map(|outcome| outcome.vector),
1244 2 => runtime.embed_query_with_model("queue-test", "later").await,
1245 3 => runtime
1246 .embed_batch_with_model("queue-test", &texts)
1247 .await
1248 .map(|vectors| vectors[0].clone()),
1249 4 => runtime
1250 .embed_document_batch_with_model("queue-test", &texts)
1251 .await
1252 .map(|vectors| vectors[0].clone()),
1253 _ => runtime
1254 .embed_query_batch_with_model("queue-test", &texts)
1255 .await
1256 .map(|vectors| vectors[0].clone()),
1257 }
1258 })
1259 .await;
1260 assert!(
1261 matches!(
1262 result,
1263 Err(RuntimeError::Storage(
1264 khive_storage::StorageError::AdmissionTimeout { .. }
1265 ))
1266 ),
1267 "family {family} must reach the actual runtime admission boundary: {result:?}"
1268 );
1269 }
1270 assert_eq!(inner.entered.load(Ordering::Acquire), 0);
1271 }
1272
1273 #[tokio::test(start_paused = true)]
1274 async fn runtime_embedding_admitted_inference_is_not_reported_as_admission_timeout() {
1275 let inner = Arc::new(BlockingTestService::new());
1276 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1277 let runtime = queue_runtime(Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner))));
1278 runtime
1279 .embedder("queue-test")
1280 .await
1281 .expect("resolve provider before the admission fixture");
1282 let mut call = Box::pin(khive_storage::scope_request_read_deadline(
1283 Duration::from_millis(100),
1284 runtime.embed_with_model("queue-test", "first"),
1285 ));
1286 assert!(poll_once(call.as_mut()).await.is_pending());
1287 drive_until_entered(call.as_mut(), &inner, 1).await;
1288 tokio::time::advance(Duration::from_millis(100)).await;
1289 let after_deadline = poll_once(call.as_mut()).await;
1290 inner.release();
1291 assert!(after_deadline.is_pending(),
1292 "already-running inference cannot be called a pre-admission refusal: {after_deadline:?}");
1293 assert_eq!(call.await.unwrap(), vec![1.0]);
1294 }
1295
1296 struct CustomWaitingService {
1297 release: Notify,
1298 }
1299
1300 #[async_trait]
1301 impl EmbeddingService for CustomWaitingService {
1302 async fn embed(
1303 &self,
1304 texts: &[String],
1305 _model: EmbeddingModel,
1306 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
1307 self.release.notified().await;
1308 Ok(texts.iter().map(|_| vec![1.0]).collect())
1309 }
1310 fn supports_model(&self, _model: EmbeddingModel) -> bool {
1311 true
1312 }
1313 fn name(&self) -> &'static str {
1314 "custom-waiting"
1315 }
1316 }
1317
1318 #[tokio::test(start_paused = true)]
1319 async fn runtime_custom_inference_is_not_reported_as_builtin_admission_timeout() {
1320 let inner = Arc::new(CustomWaitingService {
1321 release: Notify::new(),
1322 });
1323 let runtime = queue_runtime(inner.clone());
1324 let mut call = Box::pin(khive_storage::scope_request_read_deadline(
1325 Duration::from_millis(100),
1326 runtime.embed_with_model("queue-test", "custom"),
1327 ));
1328 assert!(poll_once(call.as_mut()).await.is_pending());
1329 tokio::time::advance(Duration::from_millis(100)).await;
1330 let after_deadline = poll_once(call.as_mut()).await;
1331 inner.release.notify_one();
1332 assert!(
1333 after_deadline.is_pending(),
1334 "custom inference did not enter the built-in pre-admission wait: {after_deadline:?}"
1335 );
1336 assert_eq!(call.await.unwrap(), vec![1.0]);
1337 }
1338
1339 #[tokio::test]
1340 async fn blocking_adapter_rejects_oversized_job_before_enqueue() {
1341 let service = BlockingEmbeddingService::new(Arc::new(ConstVecService { dims: 1 }));
1342 let oversized =
1343 "x".repeat(lattice_embed::DEFAULT_MAX_BATCH_SIZE * lattice_embed::MAX_TEXT_BYTES + 1);
1344
1345 let error = service
1346 .embed(&[oversized], EmbeddingModel::default())
1347 .await
1348 .expect_err("an oversized embedding job must be rejected");
1349
1350 assert!(
1351 error.to_string().contains("embedding job input"),
1352 "oversized admission must use the embedding error path: {error}"
1353 );
1354 assert!(
1355 service.worker.get().is_none(),
1356 "oversized work must be rejected before the worker queue is initialized"
1357 );
1358 }
1359
1360 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1361 async fn blocking_adapter_byte_budget_rejects_excess_and_queued_jobs_complete() {
1362 const ADMITTED_JOBS: usize = 4;
1363 let byte_budget = ADMITTED_JOBS * EMBEDDING_MAX_JOB_BYTES;
1364 let inner = Arc::new(BlockingTestService::new());
1365 let service = Arc::new(BlockingEmbeddingService::with_byte_budget(
1366 Arc::clone(&inner),
1367 byte_budget,
1368 ));
1369 let max_texts = Arc::new(vec![
1370 "x".repeat(lattice_embed::MAX_TEXT_BYTES);
1371 lattice_embed::DEFAULT_MAX_BATCH_SIZE
1372 ]);
1373 let mut admitted = Vec::with_capacity(ADMITTED_JOBS);
1374
1375 for _ in 0..ADMITTED_JOBS {
1376 let service = Arc::clone(&service);
1377 let texts = Arc::clone(&max_texts);
1378 admitted.push(tokio::spawn(async move {
1379 service.embed(&texts, EmbeddingModel::default()).await
1380 }));
1381 }
1382 tokio::time::timeout(Duration::from_secs(1), async {
1383 while service.in_flight_bytes.load(Ordering::Acquire) < byte_budget {
1384 tokio::task::yield_now().await;
1385 }
1386 })
1387 .await
1388 .expect("all jobs within the byte budget must be admitted");
1389
1390 let overflow = tokio::time::timeout(
1391 Duration::from_millis(100),
1392 service.embed(&max_texts, EmbeddingModel::default()),
1393 )
1394 .await
1395 .expect("a byte-budget overflow must fail without waiting")
1396 .expect_err("a byte-budget overflow must return an embedding error");
1397
1398 inner.release();
1399 for call in admitted {
1400 call.await
1401 .expect("admitted embedding task must not panic")
1402 .expect("admitted embedding job must complete");
1403 }
1404 assert!(
1405 overflow.to_string().contains("byte budget"),
1406 "byte saturation must use the embedding failure path: {overflow}"
1407 );
1408 }
1409
1410 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1411 async fn blocking_adapter_releases_byte_budget_after_completion_and_skipped_job() {
1412 let byte_budget = "first".len() + "abandoned".len();
1413 let inner = Arc::new(BlockingTestService::new());
1414 let service = Arc::new(BlockingEmbeddingService::with_byte_budget(
1415 Arc::clone(&inner),
1416 byte_budget,
1417 ));
1418
1419 let first_service = Arc::clone(&service);
1420 let first = tokio::spawn(async move {
1421 first_service
1422 .embed(&["first".to_owned()], EmbeddingModel::default())
1423 .await
1424 });
1425 tokio::time::timeout(Duration::from_secs(1), async {
1426 while inner.entered.load(Ordering::Acquire) == 0 {
1427 tokio::task::yield_now().await;
1428 }
1429 })
1430 .await
1431 .expect("first embedding call must occupy the native worker");
1432
1433 let abandoned = tokio::time::timeout(
1434 Duration::from_millis(50),
1435 service.embed(&["abandoned".to_owned()], EmbeddingModel::default()),
1436 )
1437 .await;
1438 assert!(abandoned.is_err(), "queued embedding call must time out");
1439 assert_eq!(
1440 service.in_flight_bytes.load(Ordering::Acquire),
1441 byte_budget,
1442 "running and queued jobs must both consume the byte budget"
1443 );
1444
1445 inner.release();
1446 first
1447 .await
1448 .expect("first embedding task must not panic")
1449 .expect("first embedding call must succeed");
1450 tokio::time::timeout(Duration::from_secs(1), async {
1451 while service.in_flight_bytes.load(Ordering::Acquire) != 0 {
1452 tokio::task::yield_now().await;
1453 }
1454 })
1455 .await
1456 .expect("completed and skipped jobs must release their byte reservations");
1457
1458 let later = service
1459 .embed(&["later".to_owned()], EmbeddingModel::default())
1460 .await
1461 .expect("a later call must succeed after the byte budget is released");
1462 assert_eq!(later, vec![vec![1.0]]);
1463 }
1464
1465 #[test]
1466 fn register_and_get_provider_round_trip() {
1467 let mut reg = EmbedderRegistry::new();
1468 reg.register(ConstVecProvider::new("mock-384", 384))
1469 .expect("first registration should succeed");
1470
1471 assert!(reg.contains("mock-384"), "registered name must be present");
1472 let provider = reg.get_provider("mock-384").expect("provider must exist");
1473 assert_eq!(provider.name(), "mock-384");
1474 assert_eq!(provider.dimensions(), 384);
1475 }
1476
1477 #[tokio::test]
1478 async fn duplicate_registration_preserves_serving_provider() {
1479 let mut reg = EmbedderRegistry::new();
1480 reg.register(ConstVecProvider::new("shared", 128))
1481 .expect("first registration should succeed");
1482 reg.register(ConstVecProvider::new("other", 64))
1483 .expect("unique registration should succeed");
1484 reg.get_service("shared")
1485 .await
1486 .expect("serving provider should build");
1487 let error = reg
1488 .register(ConstVecProvider::new("shared", 256))
1489 .expect_err("duplicate registration should fail");
1490 assert!(
1491 matches!(error, RuntimeError::InvalidInput(message) if message.contains("shared") && message.contains("serving"))
1492 );
1493
1494 let provider = reg.get_provider("shared").expect("provider must exist");
1495 assert_eq!(provider.dimensions(), 128);
1496 assert_eq!(reg.names(), vec!["shared".to_owned(), "other".to_owned()]);
1497 }
1498
1499 #[tokio::test]
1500 async fn runtime_try_register_embedder_returns_duplicate_error() {
1501 let runtime = crate::KhiveRuntime::memory().expect("memory runtime");
1502 runtime
1503 .try_register_embedder(ConstVecProvider::new("first-custom", 128))
1504 .expect("first registration should succeed");
1505 runtime
1506 .try_register_embedder(ConstVecProvider::new("second-custom", 256))
1507 .expect("second registration should succeed");
1508
1509 let expected = runtime.registered_embedding_model_names();
1510 assert!(expected.ends_with(&["first-custom".to_owned(), "second-custom".to_owned()]));
1511 for _ in 0..100 {
1512 assert_eq!(runtime.registered_embedding_model_names(), expected);
1513 }
1514 runtime
1515 .embedder("first-custom")
1516 .await
1517 .expect("first custom provider should build");
1518
1519 let error = runtime
1520 .try_register_embedder(ConstVecProvider::new("first-custom", 512))
1521 .expect_err("duplicate registration should fail");
1522 assert!(
1523 matches!(error, RuntimeError::InvalidInput(message) if message.contains("first-custom") && message.contains("serving"))
1524 );
1525 }
1526
1527 #[test]
1528 fn names_preserve_registration_order_across_calls() {
1529 let mut reg = EmbedderRegistry::new();
1530 for (name, dimensions) in [("e", 5), ("b", 4), ("d", 3), ("a", 2), ("c", 1)] {
1531 reg.register(ConstVecProvider::new(name, dimensions))
1532 .expect("unique registration should succeed");
1533 }
1534
1535 let expected = ["e", "b", "d", "a", "c"].map(|name| name.to_owned());
1536 for _ in 0..100 {
1537 assert_eq!(reg.names(), expected);
1538 }
1539 }
1540
1541 #[test]
1542 fn names_returns_all_registered() {
1543 let mut reg = EmbedderRegistry::new();
1544 reg.register(ConstVecProvider::new("model-a", 64))
1545 .expect("unique registration should succeed");
1546 reg.register(ConstVecProvider::new("model-b", 128))
1547 .expect("unique registration should succeed");
1548 reg.register(ConstVecProvider::new("model-c", 256))
1549 .expect("unique registration should succeed");
1550
1551 let mut names = reg.names();
1552 names.sort();
1553 assert_eq!(names, vec!["model-a", "model-b", "model-c"]);
1554 }
1555
1556 #[tokio::test]
1557 async fn get_service_unknown_name_returns_error() {
1558 let reg = EmbedderRegistry::new();
1559 let result = reg.get_service("does-not-exist").await;
1560 let err = result.err().expect("expected Err for unknown name, got Ok");
1561 assert!(
1562 matches!(err, RuntimeError::UnknownModel(ref n) if n == "does-not-exist"),
1563 "expected UnknownModel, got {err:?}"
1564 );
1565 }
1566
1567 #[tokio::test]
1568 async fn get_service_calls_build_once() {
1569 let counter = Arc::new(AtomicUsize::new(0));
1570 let provider = ConstVecProvider {
1571 name: "cached-model".to_owned(),
1572 dims: 32,
1573 build_calls: Arc::clone(&counter),
1574 };
1575 let mut reg = EmbedderRegistry::new();
1576 reg.register(provider)
1577 .expect("unique registration should succeed");
1578
1579 let _ = reg.get_service("cached-model").await.unwrap();
1580 let _ = reg.get_service("cached-model").await.unwrap();
1581 let _ = reg.get_service("cached-model").await.unwrap();
1582
1583 assert_eq!(
1584 counter.load(Ordering::SeqCst),
1585 1,
1586 "build must be called exactly once regardless of get_service call count"
1587 );
1588 }
1589
1590 struct SlowBuildProvider {
1591 name: String,
1592 dims: usize,
1593 build_calls: Arc<AtomicUsize>,
1594 }
1595
1596 #[async_trait]
1597 impl EmbedderProvider for SlowBuildProvider {
1598 fn name(&self) -> &str {
1599 &self.name
1600 }
1601
1602 fn dimensions(&self) -> usize {
1603 self.dims
1604 }
1605
1606 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
1607 self.build_calls.fetch_add(1, Ordering::SeqCst);
1608 tokio::time::sleep(Duration::from_millis(50)).await;
1609 Ok(Arc::new(ConstVecService { dims: self.dims }))
1610 }
1611 }
1612
1613 #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
1614 async fn concurrent_cold_resolutions_single_flight_one_build() {
1615 const CALLERS: usize = 16;
1616 let counter = Arc::new(AtomicUsize::new(0));
1617 let mut reg = EmbedderRegistry::new();
1618 reg.register(SlowBuildProvider {
1619 name: "cold-model".to_owned(),
1620 dims: 8,
1621 build_calls: Arc::clone(&counter),
1622 })
1623 .expect("unique registration should succeed");
1624 let reg = Arc::new(reg);
1625
1626 let mut callers = Vec::with_capacity(CALLERS);
1627 for _ in 0..CALLERS {
1628 let reg = Arc::clone(®);
1629 callers.push(tokio::spawn(
1630 async move { reg.get_service("cold-model").await },
1631 ));
1632 }
1633
1634 for caller in callers {
1635 let service = caller
1636 .await
1637 .expect("resolution task must not panic")
1638 .expect("every concurrent cold resolution must receive a working service");
1639 assert_eq!(service.name(), "const-vec-service");
1640 }
1641
1642 assert_eq!(
1643 counter.load(Ordering::SeqCst),
1644 1,
1645 "concurrent cold resolutions must share a single in-flight build()"
1646 );
1647 }
1648}