1use std::collections::HashMap;
8use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
9use std::sync::{Arc, Mutex, 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 bound_engines: Option<Vec<crate::engine_config::EngineConfig>>,
449 initialization: Arc<Mutex<()>>,
450}
451
452impl EmbedderRegistry {
453 pub fn new() -> Self {
455 Self {
456 entries: HashMap::new(),
457 ordered_names: Vec::new(),
458 bound_engines: None,
459 initialization: Arc::new(Mutex::new(())),
460 }
461 }
462
463 pub fn register<P: EmbedderProvider + 'static>(&mut self, provider: P) -> RuntimeResult<()> {
469 self.insert(provider, false)
470 }
471
472 pub(crate) fn register_builtin(&mut self, provider: LatticeEmbedderProvider) {
475 self.insert(provider, true)
476 .expect("configured built-in embedding models must have unique names");
477 }
478
479 #[cfg(feature = "test-internals")]
482 pub fn register_test_audited<P: EmbedderProvider + 'static>(
483 &mut self,
484 model: EmbeddingModel,
485 provider: P,
486 ) {
487 assert_eq!(provider.name(), model.to_string());
488 self.insert(TestAuditedProvider { provider }, true)
489 .expect("test audited provider names must be unique");
490 }
491
492 #[cfg(feature = "test-internals")]
501 pub fn replace_bound_for_test<P: EmbedderProvider + 'static>(
502 &mut self,
503 provider: P,
504 ) -> RuntimeResult<()> {
505 let name = provider.name().to_owned();
506 if !self
507 .bound_engines
508 .as_ref()
509 .is_some_and(|engines| engines.iter().any(|engine| engine.name == name))
510 {
511 return Err(RuntimeError::InvalidInput(format!(
512 "embedding provider `{name}` is not bound to configured engines"
513 )));
514 }
515 let bound = self.entries.get(&name).ok_or_else(|| {
516 RuntimeError::InvalidInput(format!("embedding provider `{name}` is not registered"))
517 })?;
518 if bound.serving.load(Ordering::Acquire) {
519 return Err(RuntimeError::InvalidInput(format!(
520 "embedding provider `{name}` is already serving"
521 )));
522 }
523 let expected = bound.provider.dimensions();
524 if provider.dimensions() != expected {
525 return Err(RuntimeError::InvalidInput(format!(
526 "embedding provider `{name}` has dimensions {}, but the bound provider has {expected}",
527 provider.dimensions()
528 )));
529 }
530 self.entries.insert(
531 name,
532 EmbedderEntry {
533 provider: Arc::new(provider),
534 cell: Arc::new(OnceCell::new()),
535 serving: Arc::new(AtomicBool::new(false)),
536 audited_document_preparation: false,
537 },
538 );
539 Ok(())
540 }
541
542 fn insert<P: EmbedderProvider + 'static>(
543 &mut self,
544 provider: P,
545 audited_document_preparation: bool,
546 ) -> RuntimeResult<()> {
547 let name = provider.name().to_owned();
548 if self
549 .bound_engines
550 .as_ref()
551 .is_some_and(|engines| engines.iter().any(|engine| engine.name == name))
552 {
553 return Err(RuntimeError::InvalidInput(format!(
554 "embedding provider `{name}` is already bound to configured engines"
555 )));
556 }
557 if self
558 .entries
559 .get(&name)
560 .is_some_and(|entry| entry.serving.load(Ordering::Acquire))
561 {
562 return Err(RuntimeError::InvalidInput(format!(
563 "embedding provider `{name}` is already serving"
564 )));
565 }
566 let entry = EmbedderEntry {
567 provider: Arc::new(provider),
568 cell: Arc::new(OnceCell::new()),
569 serving: Arc::new(AtomicBool::new(false)),
570 audited_document_preparation,
571 };
572 if self.entries.contains_key(&name) {
573 self.entries.insert(name, entry);
574 } else {
575 self.ordered_names.push(name.clone());
576 self.entries.insert(name, entry);
577 }
578 Ok(())
579 }
580
581 pub fn get_provider(&self, name: &str) -> Option<&dyn EmbedderProvider> {
583 self.entries.get(name).map(|e| e.provider.as_ref())
584 }
585
586 pub fn contains(&self, name: &str) -> bool {
588 self.entries.contains_key(name)
589 }
590
591 pub fn names(&self) -> Vec<String> {
593 self.ordered_names.clone()
594 }
595
596 pub(crate) fn initialization_lock(&self) -> Arc<Mutex<()>> {
597 Arc::clone(&self.initialization)
598 }
599
600 pub(crate) fn bound_engines(&self) -> Option<&[crate::engine_config::EngineConfig]> {
601 self.bound_engines.as_deref()
602 }
603
604 pub(crate) fn bind_engines(
605 &mut self,
606 engines: &[crate::engine_config::EngineConfig],
607 ) -> RuntimeResult<()> {
608 if let Some(bound) = &self.bound_engines {
609 return if bound == engines {
610 Ok(())
611 } else {
612 Err(RuntimeError::InvalidInput(
613 "configured embedding engines are already bound to a different ordered list"
614 .into(),
615 ))
616 };
617 }
618 for engine in engines {
619 let provider = self.get_provider(&engine.name).ok_or_else(|| {
620 RuntimeError::InvalidInput(format!(
621 "embedding provider `{}` is not registered at startup",
622 engine.name
623 ))
624 })?;
625 let actual = provider.dimensions();
626 if actual == 0 || u32::try_from(actual).is_err() {
627 return Err(RuntimeError::InvalidInput(format!(
628 "embedding provider `{}` has invalid dimensions {actual}; expected 1..=4294967295",
629 engine.name
630 )));
631 }
632 if let Some(model) = crate::config::parse_embedding_model_alias(&engine.name) {
633 if actual != model.dimensions() {
634 return Err(RuntimeError::InvalidInput(format!(
635 "embedding provider `{}` has dimensions {actual}, but its built-in storage binding requires {}",
636 engine.name,
637 model.dimensions()
638 )));
639 }
640 }
641 engine
642 .check_dimensions(actual)
643 .map_err(|error| RuntimeError::InvalidInput(error.to_string()))?;
644 }
645 self.bound_engines = Some(engines.to_vec());
646 Ok(())
647 }
648
649 pub(crate) fn get_entry(&self, name: &str) -> Option<EmbedderEntry> {
657 let entry = self.entries.get(name)?;
658 entry.serving.store(true, Ordering::Release);
659 Some(entry.clone())
660 }
661
662 pub async fn get_service(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
671 let entry = self
672 .entries
673 .get(name)
674 .ok_or_else(|| RuntimeError::UnknownModel(name.to_string()))?
675 .clone();
676 entry.serving.store(true, Ordering::Release);
677
678 Ok(entry.resolve().await?.0)
679 }
680}
681
682#[cfg(feature = "test-internals")]
683struct TestAuditedProvider<P> {
684 provider: P,
685}
686
687#[cfg(feature = "test-internals")]
688#[async_trait]
689impl<P: EmbedderProvider> EmbedderProvider for TestAuditedProvider<P> {
690 fn name(&self) -> &str {
691 self.provider.name()
692 }
693
694 fn dimensions(&self) -> usize {
695 self.provider.dimensions()
696 }
697
698 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
699 Ok(Arc::new(TestAuditedService(self.provider.build().await?)))
700 }
701}
702
703#[cfg(feature = "test-internals")]
707struct TestAuditedService(Arc<dyn EmbeddingService>);
708
709#[cfg(feature = "test-internals")]
710#[async_trait]
711impl EmbeddingService for TestAuditedService {
712 async fn embed(
713 &self,
714 texts: &[String],
715 model: EmbeddingModel,
716 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
717 self.0.embed(texts, model).await
718 }
719
720 fn supports_model(&self, model: EmbeddingModel) -> bool {
721 self.0.supports_model(model)
722 }
723
724 fn name(&self) -> &'static str {
725 self.0.name()
726 }
727}
728
729impl EmbedderEntry {
730 pub(crate) fn cached_service(&self) -> Option<Arc<dyn EmbeddingService>> {
731 self.cell.get().map(Arc::clone)
732 }
733
734 pub(crate) fn has_audited_document_preparation(&self) -> bool {
735 self.audited_document_preparation
736 }
737
738 pub(crate) async fn resolve(self) -> RuntimeResult<(Arc<dyn EmbeddingService>, Option<i64>)> {
749 let mut own_init_duration_us: Option<i64> = None;
750 let provider = Arc::clone(&self.provider);
751 let init_duration_us = &mut own_init_duration_us;
752 let svc = self
753 .cell
754 .get_or_try_init(|| async move {
755 let init_start = std::time::Instant::now();
756 let svc = provider.build().await.map_err(|e| {
757 crate::error::RuntimeError::Internal(format!(
758 "EmbedderProvider '{}' build() failed: {e}",
759 provider.name()
760 ))
761 })?;
762 *init_duration_us = Some(init_start.elapsed().as_micros() as i64);
763 Ok::<_, RuntimeError>(svc)
764 })
765 .await?;
766 Ok((Arc::clone(svc), own_init_duration_us))
767 }
768}
769
770pub struct LatticeEmbedderProvider {
780 model: EmbeddingModel,
781 name: String,
783}
784
785impl LatticeEmbedderProvider {
786 pub fn new(model: EmbeddingModel) -> Self {
788 let name = model.to_string();
789 Self { model, name }
790 }
791}
792
793#[async_trait]
794impl EmbedderProvider for LatticeEmbedderProvider {
795 fn name(&self) -> &str {
796 &self.name
797 }
798
799 fn dimensions(&self) -> usize {
800 self.model.dimensions()
801 }
802
803 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
804 let native = Arc::new(NativeEmbeddingService::with_model(self.model));
805 native.ensure_loaded().await?;
806 #[cfg(all(target_os = "linux", target_env = "gnu"))]
808 unsafe {
810 libc::malloc_trim(0);
811 }
812 Ok(cached_blocking_service(native))
813 }
814}
815
816fn cached_blocking_service<S: EmbeddingService + 'static>(
818 inner: Arc<S>,
819) -> Arc<dyn EmbeddingService> {
820 let blocking = Arc::new(BlockingEmbeddingService::new(inner));
821 Arc::new(CachedEmbeddingService::with_default_cache(blocking))
822}
823
824#[cfg(test)]
825#[path = "embedding_cache_admission_tests.rs"]
826mod cache_admission_tests;
827
828#[cfg(test)]
831mod tests {
832 use super::*;
833 use std::collections::HashSet;
834 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
835 use std::sync::{Condvar, Mutex};
836 use std::time::Duration;
837 use tokio::sync::Notify;
838
839 struct ConstVecProvider {
840 name: String,
841 dims: usize,
842 build_calls: Arc<AtomicUsize>,
843 }
844
845 impl ConstVecProvider {
846 fn new(name: &str, dims: usize) -> Self {
847 Self {
848 name: name.to_owned(),
849 dims,
850 build_calls: Arc::new(AtomicUsize::new(0)),
851 }
852 }
853 }
854
855 pub(super) struct ConstVecService {
859 pub(super) dims: usize,
860 }
861
862 #[async_trait]
863 impl EmbeddingService for ConstVecService {
864 async fn embed(
865 &self,
866 texts: &[String],
867 _model: EmbeddingModel,
868 ) -> std::result::Result<Vec<Vec<f32>>, lattice_embed::EmbedError> {
869 Ok(texts.iter().map(|_| vec![1.0_f32; self.dims]).collect())
870 }
871
872 fn supports_model(&self, _model: EmbeddingModel) -> bool {
873 true
874 }
875
876 fn name(&self) -> &'static str {
877 "const-vec-service"
878 }
879 }
880
881 #[async_trait]
882 impl EmbedderProvider for ConstVecProvider {
883 fn name(&self) -> &str {
884 &self.name
885 }
886
887 fn dimensions(&self) -> usize {
888 self.dims
889 }
890
891 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
892 self.build_calls.fetch_add(1, Ordering::SeqCst);
893 Ok(Arc::new(ConstVecService { dims: self.dims }))
894 }
895 }
896
897 #[test]
898 fn builtin_provider_can_be_replaced_before_resolution_without_reordering() {
899 let model = EmbeddingModel::MultilingualE5Small;
900 let name = model.to_string();
901 let mut registry = EmbedderRegistry::new();
902 registry.register_builtin(LatticeEmbedderProvider::new(model));
903 let original_names = registry.names();
904
905 registry
906 .register(ConstVecProvider::new(&name, model.dimensions()))
907 .expect("pre-serving replacement should succeed");
908 assert_eq!(registry.names(), original_names);
909 assert!(!registry
910 .get_entry(&name)
911 .expect("replacement entry")
912 .has_audited_document_preparation());
913 }
914
915 struct FirstLoadBlockingService {
916 loaded: AtomicBool,
917 }
918
919 #[async_trait]
920 impl EmbeddingService for FirstLoadBlockingService {
921 async fn embed(
922 &self,
923 texts: &[String],
924 _model: EmbeddingModel,
925 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
926 if !self.loaded.swap(true, Ordering::SeqCst) {
927 tokio::task::spawn_blocking(|| {})
928 .await
929 .map_err(|error| lattice_embed::EmbedError::Internal(error.to_string()))?;
930 }
931 Ok(texts.iter().map(|_| vec![1.0]).collect())
932 }
933
934 fn supports_model(&self, _model: EmbeddingModel) -> bool {
935 true
936 }
937
938 fn name(&self) -> &'static str {
939 "first-load-blocking-service"
940 }
941 }
942
943 pub(super) struct BlockingTestService {
944 pub(super) calls: Mutex<Vec<String>>,
945 pub(super) entered: AtomicUsize,
946 release: (Mutex<bool>, Condvar),
947 pub(super) thread_ids: Mutex<HashSet<std::thread::ThreadId>>,
948 }
949
950 impl BlockingTestService {
951 pub(super) fn new() -> Self {
952 Self {
953 calls: Mutex::new(Vec::new()),
954 entered: AtomicUsize::new(0),
955 release: (Mutex::new(false), Condvar::new()),
956 thread_ids: Mutex::new(HashSet::new()),
957 }
958 }
959
960 pub(super) fn release(&self) {
961 *self
962 .release
963 .0
964 .lock()
965 .expect("release lock must not be poisoned") = true;
966 self.release.1.notify_all();
967 }
968 }
969
970 #[async_trait]
971 impl EmbeddingService for BlockingTestService {
972 async fn embed(
973 &self,
974 texts: &[String],
975 _model: EmbeddingModel,
976 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
977 let text = texts.first().cloned().unwrap_or_default();
978 self.thread_ids
979 .lock()
980 .expect("thread id lock must not be poisoned")
981 .insert(std::thread::current().id());
982 self.calls
983 .lock()
984 .expect("call lock must not be poisoned")
985 .push(text.clone());
986 self.entered.fetch_add(1, Ordering::Release);
987
988 if text != "later" {
989 let (released, wake) = &self.release;
990 let guard = released.lock().expect("release lock must not be poisoned");
991 let _guard = wake
992 .wait_while(guard, |released| !*released)
993 .expect("release lock must not be poisoned");
994 }
995
996 Ok(texts.iter().map(|_| vec![1.0]).collect())
997 }
998
999 async fn embed_query(
1000 &self,
1001 texts: &[String],
1002 model: EmbeddingModel,
1003 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
1004 self.embed(texts, model).await
1007 }
1008
1009 fn supports_model(&self, _model: EmbeddingModel) -> bool {
1010 true
1011 }
1012
1013 fn name(&self) -> &'static str {
1014 "blocking-test-service"
1015 }
1016 }
1017
1018 #[test]
1019 fn blocking_adapter_first_use_completes_with_single_blocking_thread() {
1020 let runtime = tokio::runtime::Builder::new_current_thread()
1021 .enable_time()
1022 .max_blocking_threads(1)
1023 .build()
1024 .expect("current-thread runtime must build");
1025 let service = BlockingEmbeddingService::new(Arc::new(FirstLoadBlockingService {
1026 loaded: AtomicBool::new(false),
1027 }));
1028
1029 let result = runtime.block_on(async {
1030 tokio::time::timeout(
1031 Duration::from_secs(5),
1032 service.embed(&["first use".to_owned()], EmbeddingModel::default()),
1033 )
1034 .await
1035 });
1036 runtime.shutdown_timeout(Duration::from_secs(1));
1037
1038 let embeddings = result
1039 .expect("first-use embedding must not exhaust the blocking pool")
1040 .expect("first-use embedding must succeed");
1041 assert_eq!(embeddings, vec![vec![1.0]]);
1042 }
1043
1044 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1045 async fn blocking_adapter_uses_one_worker_for_concurrent_calls() {
1046 const CALL_COUNT: usize = 32;
1047 let inner = Arc::new(BlockingTestService::new());
1048 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
1049 let mut calls = Vec::with_capacity(CALL_COUNT);
1050
1051 for index in 0..CALL_COUNT {
1052 let service = Arc::clone(&service);
1053 calls.push(tokio::spawn(async move {
1054 service
1055 .embed(&[format!("request-{index}")], EmbeddingModel::default())
1056 .await
1057 }));
1058 }
1059
1060 let _ = tokio::time::timeout(Duration::from_millis(250), async {
1061 while inner.entered.load(Ordering::Acquire) < 2 {
1062 tokio::task::yield_now().await;
1063 }
1064 })
1065 .await;
1066 inner.release();
1067
1068 for call in calls {
1069 call.await
1070 .expect("embedding task must not panic")
1071 .expect("embedding call must succeed");
1072 }
1073 assert_eq!(
1074 inner
1075 .thread_ids
1076 .lock()
1077 .expect("thread id lock must not be poisoned")
1078 .len(),
1079 1,
1080 "concurrent calls must share one native worker thread"
1081 );
1082 }
1083
1084 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1085 async fn blocking_adapter_skips_timed_out_call_and_serves_later_call() {
1086 let inner = Arc::new(BlockingTestService::new());
1087 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
1088
1089 let first_service = Arc::clone(&service);
1090 let first = tokio::spawn(async move {
1091 first_service
1092 .embed(&["first".to_owned()], EmbeddingModel::default())
1093 .await
1094 });
1095 tokio::time::timeout(Duration::from_secs(1), async {
1096 while inner.entered.load(Ordering::Acquire) == 0 {
1097 tokio::task::yield_now().await;
1098 }
1099 })
1100 .await
1101 .expect("first embedding call must enter the native service");
1102
1103 let abandoned = tokio::time::timeout(
1104 Duration::from_millis(50),
1105 service.embed(&["abandoned".to_owned()], EmbeddingModel::default()),
1106 )
1107 .await;
1108 let later_service = Arc::clone(&service);
1109 let later = tokio::spawn(async move {
1110 later_service
1111 .embed(&["later".to_owned()], EmbeddingModel::default())
1112 .await
1113 });
1114 inner.release();
1115
1116 first
1117 .await
1118 .expect("first embedding task must not panic")
1119 .expect("first embedding call must succeed");
1120 let later_result = tokio::time::timeout(Duration::from_secs(1), later)
1121 .await
1122 .expect("later embedding call must be served")
1123 .expect("later embedding task must not panic")
1124 .expect("later embedding call must succeed");
1125
1126 assert!(abandoned.is_err(), "queued embedding call must time out");
1127 assert_eq!(later_result, vec![vec![1.0]]);
1128 assert_eq!(
1129 *inner.calls.lock().expect("call lock must not be poisoned"),
1130 vec!["first".to_owned(), "later".to_owned()],
1131 "the worker must skip a queued call whose receiver is closed"
1132 );
1133 }
1134
1135 pub(super) struct ReleaseWorkerOnDrop(pub(super) Arc<BlockingTestService>);
1136
1137 impl Drop for ReleaseWorkerOnDrop {
1138 fn drop(&mut self) {
1139 self.0.release();
1140 }
1141 }
1142
1143 struct ServiceProvider(Arc<dyn EmbeddingService>);
1144
1145 #[async_trait]
1146 impl EmbedderProvider for ServiceProvider {
1147 fn name(&self) -> &str {
1148 "queue-test"
1149 }
1150 fn dimensions(&self) -> usize {
1151 1
1152 }
1153 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
1154 Ok(Arc::clone(&self.0))
1155 }
1156 }
1157
1158 pub(super) fn queue_runtime(service: Arc<dyn EmbeddingService>) -> crate::KhiveRuntime {
1159 let runtime = crate::KhiveRuntime::memory().expect("memory runtime");
1160 runtime.register_embedder(ServiceProvider(service));
1161 runtime
1162 }
1163
1164 pub(super) async fn poll_once<F: std::future::Future + ?Sized>(
1165 mut future: std::pin::Pin<&mut F>,
1166 ) -> std::task::Poll<F::Output> {
1167 std::future::poll_fn(|cx| std::task::Poll::Ready(future.as_mut().poll(cx))).await
1168 }
1169
1170 pub(super) async fn wait_for_entered(inner: &BlockingTestService, count: usize) {
1171 let watchdog = std::time::Instant::now() + Duration::from_secs(2);
1172 while inner.entered.load(Ordering::Acquire) < count {
1173 assert!(
1174 std::time::Instant::now() < watchdog,
1175 "setup watchdog: native worker did not enter"
1176 );
1177 tokio::task::yield_now().await;
1178 }
1179 }
1180
1181 async fn drive_until_entered<F: std::future::Future + ?Sized>(
1182 mut future: std::pin::Pin<&mut F>,
1183 inner: &BlockingTestService,
1184 count: usize,
1185 ) {
1186 let watchdog = std::time::Instant::now() + Duration::from_secs(2);
1187 while inner.entered.load(Ordering::Acquire) < count {
1188 assert!(
1189 std::time::Instant::now() < watchdog,
1190 "setup watchdog: driven runtime call did not reach the native worker"
1191 );
1192 assert!(
1193 poll_once(future.as_mut()).await.is_pending(),
1194 "held inference must remain pending while the runtime call is driven"
1195 );
1196 tokio::task::yield_now().await;
1197 }
1198 }
1199
1200 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1201 async fn runtime_embedding_waits_for_capacity_and_drains_n_plus_one_calls() {
1202 let inner = Arc::new(BlockingTestService::new());
1203 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1204 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
1205 let runtime = queue_runtime(service.clone());
1206 runtime
1207 .embedder("queue-test")
1208 .await
1209 .expect("resolve provider before the admission fixture");
1210 let first_text = vec!["first".to_owned()];
1211 let mut first = Box::pin(runtime.embed_batch_with_model("queue-test", &first_text));
1212 assert!(poll_once(first.as_mut()).await.is_pending());
1213 drive_until_entered(first.as_mut(), &inner, 1).await;
1214
1215 let texts: Vec<_> = (0..EMBEDDING_QUEUE_CAPACITY)
1216 .map(|index| vec![format!("queued-{index}")])
1217 .collect();
1218 let mut queued = Vec::new();
1219 for text in &texts {
1220 let mut call = Box::pin(runtime.embed_batch_with_model("queue-test", text));
1221 assert!(
1222 poll_once(call.as_mut()).await.is_pending(),
1223 "the bounded queue must admit its N actual runtime calls"
1224 );
1225 queued.push(call);
1226 }
1227 assert_eq!(
1228 service.in_flight_bytes.load(Ordering::Acquire),
1229 first_text.iter().map(String::len).sum::<usize>()
1230 + texts.iter().flatten().map(String::len).sum::<usize>(),
1231 "the held worker and N queued runtime jobs must own their exact input bytes"
1232 );
1233 let overflow_text = vec!["overflow".to_owned()];
1234 let mut overflow = Box::pin(runtime.embed_batch_with_model("queue-test", &overflow_text));
1235 let overflow_before_release = poll_once(overflow.as_mut()).await;
1236 inner.release();
1237 assert!(
1238 overflow_before_release.is_pending(),
1239 "request N+1 must wait for a slot instead of failing: {overflow_before_release:?}"
1240 );
1241 assert_eq!(first.await.unwrap(), vec![vec![1.0]]);
1242 for call in queued {
1243 assert_eq!(call.await.unwrap(), vec![vec![1.0]]);
1244 }
1245 assert_eq!(overflow.await.unwrap(), vec![vec![1.0]]);
1246 assert_eq!(
1247 inner.entered.load(Ordering::Acquire),
1248 EMBEDDING_QUEUE_CAPACITY + 2
1249 );
1250 assert_eq!(inner.thread_ids.lock().unwrap().len(), 1);
1251 }
1252
1253 #[tokio::test(start_paused = true)]
1254 async fn runtime_embedding_expired_admission_is_retryable_and_never_enqueued() {
1255 let inner = Arc::new(BlockingTestService::new());
1256 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1257 let service = Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner)));
1258 let runtime = queue_runtime(service.clone());
1259 runtime
1260 .embedder("queue-test")
1261 .await
1262 .expect("resolve provider before the admission fixture");
1263 let first_text = vec!["first".to_owned()];
1264 let mut first = Box::pin(runtime.embed_batch_with_model("queue-test", &first_text));
1265 assert!(poll_once(first.as_mut()).await.is_pending());
1266 drive_until_entered(first.as_mut(), &inner, 1).await;
1267 let texts: Vec<_> = (0..EMBEDDING_QUEUE_CAPACITY)
1268 .map(|index| vec![format!("queued-{index}")])
1269 .collect();
1270 let mut queued = Vec::new();
1271 for text in &texts {
1272 let mut call = Box::pin(runtime.embed_batch_with_model("queue-test", text));
1273 assert!(poll_once(call.as_mut()).await.is_pending());
1274 queued.push(call);
1275 }
1276 assert_eq!(
1277 service.in_flight_bytes.load(Ordering::Acquire),
1278 first_text.iter().map(String::len).sum::<usize>()
1279 + texts.iter().flatten().map(String::len).sum::<usize>(),
1280 "the held worker and N queued runtime jobs must own their exact input bytes"
1281 );
1282 let deadline = khive_storage::RequestReadDeadline::after(Duration::from_millis(100));
1283 let mut expired = Box::pin(khive_storage::scope_request_read_deadline_at(
1284 deadline,
1285 runtime.embed_with_model("queue-test", "expired"),
1286 ));
1287 assert!(
1288 poll_once(expired.as_mut()).await.is_pending(),
1289 "an unexpired full-queue call must wait"
1290 );
1291 tokio::time::advance(Duration::from_millis(100)).await;
1292 let result = poll_once(expired.as_mut()).await;
1293 inner.release();
1294 let error = match result {
1295 std::task::Poll::Ready(Err(error)) => error,
1296 other => panic!("expired admission must produce a typed runtime error: {other:?}"),
1297 };
1298 assert!(
1299 matches!(&error, RuntimeError::Storage(khive_storage::StorageError::AdmissionTimeout {
1300 operation, timeout_ms: 100, pool_identity: None,
1301 }) if operation == "embedding admission"),
1302 "{error:?}"
1303 );
1304 assert!(
1305 error.retryable_failure_context().is_some(),
1306 "pre-admission expiry must use the existing retryable classification"
1307 );
1308 let projected = crate::error_projection::runtime_error_value(
1309 error,
1310 crate::DomainDisposition::NotCommitted,
1311 );
1312 assert_eq!(projected["retryable"], true);
1313 assert_eq!(projected["operation"], "embedding admission");
1314 assert_eq!(first.await.unwrap(), vec![vec![1.0]]);
1315 for call in queued {
1316 assert_eq!(call.await.unwrap(), vec![vec![1.0]]);
1317 }
1318 assert!(
1319 !inner
1320 .calls
1321 .lock()
1322 .unwrap()
1323 .iter()
1324 .any(|text| text == "expired"),
1325 "an expired waiter must never reach inference"
1326 );
1327 }
1328
1329 #[tokio::test(start_paused = true)]
1330 async fn runtime_embedding_earlier_absolute_bound_is_not_renewed_when_slot_is_ready() {
1331 let inner = Arc::new(BlockingTestService::new());
1332 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1333 let runtime = queue_runtime(Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner))));
1334 runtime
1335 .embedder("queue-test")
1336 .await
1337 .expect("resolve provider before the admission deadline");
1338 let deadline = khive_storage::RequestReadDeadline::after(Duration::from_millis(100));
1339 tokio::time::advance(Duration::from_millis(100)).await;
1340 let mut call = Box::pin(khive_storage::scope_request_read_deadline_at(
1341 deadline,
1342 runtime.embed_with_model("queue-test", "expired-ready-slot"),
1343 ));
1344 let result = poll_once(call.as_mut()).await;
1345 inner.release();
1346 assert!(
1347 matches!(
1348 result,
1349 std::task::Poll::Ready(Err(RuntimeError::Storage(
1350 khive_storage::StorageError::AdmissionTimeout { timeout_ms: 0, .. }
1351 )))
1352 ),
1353 "an already-expired original bound must refuse even an available slot: {result:?}"
1354 );
1355 assert_eq!(
1356 inner.entered.load(Ordering::Acquire),
1357 0,
1358 "capacity becoming ready must not bypass expiry"
1359 );
1360 }
1361
1362 #[tokio::test(start_paused = true)]
1363 async fn runtime_embedding_all_six_call_families_refuse_expired_admission() {
1364 let inner = Arc::new(BlockingTestService::new());
1365 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1366 let runtime = queue_runtime(Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner))));
1367 let texts = vec!["later".to_owned()];
1368 for family in 0..6 {
1369 let result = khive_storage::scope_request_read_deadline(Duration::ZERO, async {
1370 match family {
1371 0 => runtime.embed_with_model("queue-test", "later").await,
1372 1 => runtime
1373 .embed_document_with_model_outcome("queue-test", "later")
1374 .await
1375 .map(|outcome| outcome.vector),
1376 2 => runtime.embed_query_with_model("queue-test", "later").await,
1377 3 => runtime
1378 .embed_batch_with_model("queue-test", &texts)
1379 .await
1380 .map(|vectors| vectors[0].clone()),
1381 4 => runtime
1382 .embed_document_batch_with_model("queue-test", &texts)
1383 .await
1384 .map(|vectors| vectors[0].clone()),
1385 _ => runtime
1386 .embed_query_batch_with_model("queue-test", &texts)
1387 .await
1388 .map(|vectors| vectors[0].clone()),
1389 }
1390 })
1391 .await;
1392 assert!(
1393 matches!(
1394 result,
1395 Err(RuntimeError::Storage(
1396 khive_storage::StorageError::AdmissionTimeout { .. }
1397 ))
1398 ),
1399 "family {family} must reach the actual runtime admission boundary: {result:?}"
1400 );
1401 }
1402 assert_eq!(inner.entered.load(Ordering::Acquire), 0);
1403 }
1404
1405 #[tokio::test(start_paused = true)]
1406 async fn runtime_embedding_admitted_inference_is_not_reported_as_admission_timeout() {
1407 let inner = Arc::new(BlockingTestService::new());
1408 let _release = ReleaseWorkerOnDrop(Arc::clone(&inner));
1409 let runtime = queue_runtime(Arc::new(BlockingEmbeddingService::new(Arc::clone(&inner))));
1410 runtime
1411 .embedder("queue-test")
1412 .await
1413 .expect("resolve provider before the admission fixture");
1414 let mut call = Box::pin(khive_storage::scope_request_read_deadline(
1415 Duration::from_millis(100),
1416 runtime.embed_with_model("queue-test", "first"),
1417 ));
1418 assert!(poll_once(call.as_mut()).await.is_pending());
1419 drive_until_entered(call.as_mut(), &inner, 1).await;
1420 tokio::time::advance(Duration::from_millis(100)).await;
1421 let after_deadline = poll_once(call.as_mut()).await;
1422 inner.release();
1423 assert!(after_deadline.is_pending(),
1424 "already-running inference cannot be called a pre-admission refusal: {after_deadline:?}");
1425 assert_eq!(call.await.unwrap(), vec![1.0]);
1426 }
1427
1428 struct CustomWaitingService {
1429 release: Notify,
1430 }
1431
1432 #[async_trait]
1433 impl EmbeddingService for CustomWaitingService {
1434 async fn embed(
1435 &self,
1436 texts: &[String],
1437 _model: EmbeddingModel,
1438 ) -> lattice_embed::Result<Vec<Vec<f32>>> {
1439 self.release.notified().await;
1440 Ok(texts.iter().map(|_| vec![1.0]).collect())
1441 }
1442 fn supports_model(&self, _model: EmbeddingModel) -> bool {
1443 true
1444 }
1445 fn name(&self) -> &'static str {
1446 "custom-waiting"
1447 }
1448 }
1449
1450 #[tokio::test(start_paused = true)]
1451 async fn runtime_custom_inference_is_not_reported_as_builtin_admission_timeout() {
1452 let inner = Arc::new(CustomWaitingService {
1453 release: Notify::new(),
1454 });
1455 let runtime = queue_runtime(inner.clone());
1456 let mut call = Box::pin(khive_storage::scope_request_read_deadline(
1457 Duration::from_millis(100),
1458 runtime.embed_with_model("queue-test", "custom"),
1459 ));
1460 assert!(poll_once(call.as_mut()).await.is_pending());
1461 tokio::time::advance(Duration::from_millis(100)).await;
1462 let after_deadline = poll_once(call.as_mut()).await;
1463 inner.release.notify_one();
1464 assert!(
1465 after_deadline.is_pending(),
1466 "custom inference did not enter the built-in pre-admission wait: {after_deadline:?}"
1467 );
1468 assert_eq!(call.await.unwrap(), vec![1.0]);
1469 }
1470
1471 #[tokio::test]
1472 async fn blocking_adapter_rejects_oversized_job_before_enqueue() {
1473 let service = BlockingEmbeddingService::new(Arc::new(ConstVecService { dims: 1 }));
1474 let oversized =
1475 "x".repeat(lattice_embed::DEFAULT_MAX_BATCH_SIZE * lattice_embed::MAX_TEXT_BYTES + 1);
1476
1477 let error = service
1478 .embed(&[oversized], EmbeddingModel::default())
1479 .await
1480 .expect_err("an oversized embedding job must be rejected");
1481
1482 assert!(
1483 error.to_string().contains("embedding job input"),
1484 "oversized admission must use the embedding error path: {error}"
1485 );
1486 assert!(
1487 service.worker.get().is_none(),
1488 "oversized work must be rejected before the worker queue is initialized"
1489 );
1490 }
1491
1492 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1493 async fn blocking_adapter_byte_budget_rejects_excess_and_queued_jobs_complete() {
1494 const ADMITTED_JOBS: usize = 4;
1495 let byte_budget = ADMITTED_JOBS * EMBEDDING_MAX_JOB_BYTES;
1496 let inner = Arc::new(BlockingTestService::new());
1497 let service = Arc::new(BlockingEmbeddingService::with_byte_budget(
1498 Arc::clone(&inner),
1499 byte_budget,
1500 ));
1501 let max_texts = Arc::new(vec![
1502 "x".repeat(lattice_embed::MAX_TEXT_BYTES);
1503 lattice_embed::DEFAULT_MAX_BATCH_SIZE
1504 ]);
1505 let mut admitted = Vec::with_capacity(ADMITTED_JOBS);
1506
1507 for _ in 0..ADMITTED_JOBS {
1508 let service = Arc::clone(&service);
1509 let texts = Arc::clone(&max_texts);
1510 admitted.push(tokio::spawn(async move {
1511 service.embed(&texts, EmbeddingModel::default()).await
1512 }));
1513 }
1514 tokio::time::timeout(Duration::from_secs(1), async {
1515 while service.in_flight_bytes.load(Ordering::Acquire) < byte_budget {
1516 tokio::task::yield_now().await;
1517 }
1518 })
1519 .await
1520 .expect("all jobs within the byte budget must be admitted");
1521
1522 let overflow = tokio::time::timeout(
1523 Duration::from_millis(100),
1524 service.embed(&max_texts, EmbeddingModel::default()),
1525 )
1526 .await
1527 .expect("a byte-budget overflow must fail without waiting")
1528 .expect_err("a byte-budget overflow must return an embedding error");
1529
1530 inner.release();
1531 for call in admitted {
1532 call.await
1533 .expect("admitted embedding task must not panic")
1534 .expect("admitted embedding job must complete");
1535 }
1536 assert!(
1537 overflow.to_string().contains("byte budget"),
1538 "byte saturation must use the embedding failure path: {overflow}"
1539 );
1540 }
1541
1542 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1543 async fn blocking_adapter_releases_byte_budget_after_completion_and_skipped_job() {
1544 let byte_budget = "first".len() + "abandoned".len();
1545 let inner = Arc::new(BlockingTestService::new());
1546 let service = Arc::new(BlockingEmbeddingService::with_byte_budget(
1547 Arc::clone(&inner),
1548 byte_budget,
1549 ));
1550
1551 let first_service = Arc::clone(&service);
1552 let first = tokio::spawn(async move {
1553 first_service
1554 .embed(&["first".to_owned()], EmbeddingModel::default())
1555 .await
1556 });
1557 tokio::time::timeout(Duration::from_secs(1), async {
1558 while inner.entered.load(Ordering::Acquire) == 0 {
1559 tokio::task::yield_now().await;
1560 }
1561 })
1562 .await
1563 .expect("first embedding call must occupy the native worker");
1564
1565 let abandoned = tokio::time::timeout(
1566 Duration::from_millis(50),
1567 service.embed(&["abandoned".to_owned()], EmbeddingModel::default()),
1568 )
1569 .await;
1570 assert!(abandoned.is_err(), "queued embedding call must time out");
1571 assert_eq!(
1572 service.in_flight_bytes.load(Ordering::Acquire),
1573 byte_budget,
1574 "running and queued jobs must both consume the byte budget"
1575 );
1576
1577 inner.release();
1578 first
1579 .await
1580 .expect("first embedding task must not panic")
1581 .expect("first embedding call must succeed");
1582 tokio::time::timeout(Duration::from_secs(1), async {
1583 while service.in_flight_bytes.load(Ordering::Acquire) != 0 {
1584 tokio::task::yield_now().await;
1585 }
1586 })
1587 .await
1588 .expect("completed and skipped jobs must release their byte reservations");
1589
1590 let later = service
1591 .embed(&["later".to_owned()], EmbeddingModel::default())
1592 .await
1593 .expect("a later call must succeed after the byte budget is released");
1594 assert_eq!(later, vec![vec![1.0]]);
1595 }
1596
1597 #[test]
1598 fn register_and_get_provider_round_trip() {
1599 let mut reg = EmbedderRegistry::new();
1600 reg.register(ConstVecProvider::new("mock-384", 384))
1601 .expect("first registration should succeed");
1602
1603 assert!(reg.contains("mock-384"), "registered name must be present");
1604 let provider = reg.get_provider("mock-384").expect("provider must exist");
1605 assert_eq!(provider.name(), "mock-384");
1606 assert_eq!(provider.dimensions(), 384);
1607 }
1608
1609 #[tokio::test]
1610 async fn duplicate_registration_preserves_serving_provider() {
1611 let mut reg = EmbedderRegistry::new();
1612 reg.register(ConstVecProvider::new("shared", 128))
1613 .expect("first registration should succeed");
1614 reg.register(ConstVecProvider::new("other", 64))
1615 .expect("unique registration should succeed");
1616 reg.get_service("shared")
1617 .await
1618 .expect("serving provider should build");
1619 let error = reg
1620 .register(ConstVecProvider::new("shared", 256))
1621 .expect_err("duplicate registration should fail");
1622 assert!(
1623 matches!(error, RuntimeError::InvalidInput(message) if message.contains("shared") && message.contains("serving"))
1624 );
1625
1626 let provider = reg.get_provider("shared").expect("provider must exist");
1627 assert_eq!(provider.dimensions(), 128);
1628 assert_eq!(reg.names(), vec!["shared".to_owned(), "other".to_owned()]);
1629 }
1630
1631 #[tokio::test]
1632 async fn runtime_try_register_embedder_returns_duplicate_error() {
1633 let runtime = crate::KhiveRuntime::memory().expect("memory runtime");
1634 runtime
1635 .try_register_embedder(ConstVecProvider::new("first-custom", 128))
1636 .expect("first registration should succeed");
1637 runtime
1638 .try_register_embedder(ConstVecProvider::new("second-custom", 256))
1639 .expect("second registration should succeed");
1640
1641 let expected = runtime.registered_embedding_model_names();
1642 assert!(expected.ends_with(&["first-custom".to_owned(), "second-custom".to_owned()]));
1643 for _ in 0..100 {
1644 assert_eq!(runtime.registered_embedding_model_names(), expected);
1645 }
1646 runtime
1647 .embedder("first-custom")
1648 .await
1649 .expect("first custom provider should build");
1650
1651 let error = runtime
1652 .try_register_embedder(ConstVecProvider::new("first-custom", 512))
1653 .expect_err("duplicate registration should fail");
1654 assert!(
1655 matches!(error, RuntimeError::InvalidInput(message) if message.contains("first-custom") && message.contains("serving"))
1656 );
1657 }
1658
1659 #[test]
1660 fn bound_engine_list_is_fixed_once_bound() {
1661 let engine = |name: &str, dims: Option<i64>| crate::engine_config::EngineConfig {
1662 name: name.to_owned(),
1663 weight: 1.0,
1664 dims,
1665 };
1666 let mut reg = EmbedderRegistry::new();
1667 for (name, dimensions) in [("bound-a", 8), ("bound-b", 4)] {
1668 reg.register(ConstVecProvider::new(name, dimensions))
1669 .expect("unique registration should succeed");
1670 }
1671 let bound = vec![engine("bound-a", Some(8)), engine("bound-b", None)];
1672 reg.bind_engines(&bound)
1673 .expect("first binding should succeed");
1674 reg.bind_engines(&bound)
1675 .expect("binding the same ordered list again is idempotent");
1676
1677 for different in [
1679 vec![engine("bound-b", None), engine("bound-a", Some(8))],
1680 vec![engine("bound-a", Some(8))],
1681 vec![engine("bound-a", Some(8)), engine("bound-b", Some(4))],
1682 ] {
1683 let error = reg
1684 .bind_engines(&different)
1685 .expect_err("a bound registry must refuse a different list");
1686 assert!(
1687 matches!(&error, RuntimeError::InvalidInput(message) if message.contains("different ordered list")),
1688 "{error:?}"
1689 );
1690 assert_eq!(reg.bound_engines(), Some(bound.as_slice()));
1691 }
1692 }
1693
1694 #[test]
1695 fn names_preserve_registration_order_across_calls() {
1696 let mut reg = EmbedderRegistry::new();
1697 for (name, dimensions) in [("e", 5), ("b", 4), ("d", 3), ("a", 2), ("c", 1)] {
1698 reg.register(ConstVecProvider::new(name, dimensions))
1699 .expect("unique registration should succeed");
1700 }
1701
1702 let expected = ["e", "b", "d", "a", "c"].map(|name| name.to_owned());
1703 for _ in 0..100 {
1704 assert_eq!(reg.names(), expected);
1705 }
1706 }
1707
1708 #[test]
1709 fn names_returns_all_registered() {
1710 let mut reg = EmbedderRegistry::new();
1711 reg.register(ConstVecProvider::new("model-a", 64))
1712 .expect("unique registration should succeed");
1713 reg.register(ConstVecProvider::new("model-b", 128))
1714 .expect("unique registration should succeed");
1715 reg.register(ConstVecProvider::new("model-c", 256))
1716 .expect("unique registration should succeed");
1717
1718 let mut names = reg.names();
1719 names.sort();
1720 assert_eq!(names, vec!["model-a", "model-b", "model-c"]);
1721 }
1722
1723 #[tokio::test]
1724 async fn get_service_unknown_name_returns_error() {
1725 let reg = EmbedderRegistry::new();
1726 let result = reg.get_service("does-not-exist").await;
1727 let err = result.err().expect("expected Err for unknown name, got Ok");
1728 assert!(
1729 matches!(err, RuntimeError::UnknownModel(ref n) if n == "does-not-exist"),
1730 "expected UnknownModel, got {err:?}"
1731 );
1732 }
1733
1734 #[tokio::test]
1735 async fn get_service_calls_build_once() {
1736 let counter = Arc::new(AtomicUsize::new(0));
1737 let provider = ConstVecProvider {
1738 name: "cached-model".to_owned(),
1739 dims: 32,
1740 build_calls: Arc::clone(&counter),
1741 };
1742 let mut reg = EmbedderRegistry::new();
1743 reg.register(provider)
1744 .expect("unique registration should succeed");
1745
1746 let _ = reg.get_service("cached-model").await.unwrap();
1747 let _ = reg.get_service("cached-model").await.unwrap();
1748 let _ = reg.get_service("cached-model").await.unwrap();
1749
1750 assert_eq!(
1751 counter.load(Ordering::SeqCst),
1752 1,
1753 "build must be called exactly once regardless of get_service call count"
1754 );
1755 }
1756
1757 struct SlowBuildProvider {
1758 name: String,
1759 dims: usize,
1760 build_calls: Arc<AtomicUsize>,
1761 }
1762
1763 #[async_trait]
1764 impl EmbedderProvider for SlowBuildProvider {
1765 fn name(&self) -> &str {
1766 &self.name
1767 }
1768
1769 fn dimensions(&self) -> usize {
1770 self.dims
1771 }
1772
1773 async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
1774 self.build_calls.fetch_add(1, Ordering::SeqCst);
1775 tokio::time::sleep(Duration::from_millis(50)).await;
1776 Ok(Arc::new(ConstVecService { dims: self.dims }))
1777 }
1778 }
1779
1780 #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
1781 async fn concurrent_cold_resolutions_single_flight_one_build() {
1782 const CALLERS: usize = 16;
1783 let counter = Arc::new(AtomicUsize::new(0));
1784 let mut reg = EmbedderRegistry::new();
1785 reg.register(SlowBuildProvider {
1786 name: "cold-model".to_owned(),
1787 dims: 8,
1788 build_calls: Arc::clone(&counter),
1789 })
1790 .expect("unique registration should succeed");
1791 let reg = Arc::new(reg);
1792
1793 let mut callers = Vec::with_capacity(CALLERS);
1794 for _ in 0..CALLERS {
1795 let reg = Arc::clone(®);
1796 callers.push(tokio::spawn(
1797 async move { reg.get_service("cold-model").await },
1798 ));
1799 }
1800
1801 for caller in callers {
1802 let service = caller
1803 .await
1804 .expect("resolution task must not panic")
1805 .expect("every concurrent cold resolution must receive a working service");
1806 assert_eq!(service.name(), "const-vec-service");
1807 }
1808
1809 assert_eq!(
1810 counter.load(Ordering::SeqCst),
1811 1,
1812 "concurrent cold resolutions must share a single in-flight build()"
1813 );
1814 }
1815}