Skip to main content

khive_runtime/
embedder_registry.rs

1//! EmbedderRegistry — pack-extensible embedding provider surface.
2//!
3//! Packs implement [`EmbedderProvider`] and register custom models via
4//! [`crate::KhiveRuntime::register_embedder`]. Built-in lattice models are pre-registered
5//! during runtime construction and require no opt-in.
6
7use 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
41/// Bound only the built-in worker's pre-admission wait, above the lattice error
42/// boundary. Providers that never enter that wait retain their own semantics.
43pub(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                    // A ready cache hit precedes admission, including an expired bound.
72                    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                    // A provider may enter the owned adapter later. Keep the
89                    // original expired bound without timing out its other work.
90                    _ => {}
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;
106// 32 queue slots × the normal 128-text batch × 32 KiB/text = 128 MiB in flight.
107const 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
156/// Bounds non-cancellable native inference to one worker and a fixed queue.
157/// Callers can detach safely because closed queued jobs are skipped before inference.
158pub(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            // Direct trait callers have no RuntimeResult boundary at which to
278            // report a typed admission timeout, so retain finite fail-fast admission.
279            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        // Capacity and expiry can become ready in one poll. Do not publish a
289        // job after the original bound, even when reserve() returned a permit.
290        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        // The outer cache delegates raw caller text here; preparation belongs
340        // to the native service, after the caller-input admission checks.
341        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/// A source that can produce an [`EmbeddingService`] by name.
384///
385/// Packs implement this trait to register custom embedding backends.
386/// The runtime calls [`build`](EmbedderProvider::build) lazily — once per
387/// process per model — and caches the result. Subsequent calls to
388/// `KhiveRuntime::embedder(name)` are cheap.
389///
390/// Built-in lattice models are registered automatically via
391/// [`LatticeEmbedderProvider`]; packs need not re-register them.
392#[async_trait]
393pub trait EmbedderProvider: Send + Sync {
394    /// Stable, case-sensitive name for this embedder.
395    ///
396    /// Must be unique across all registered providers. The name is used as
397    /// the key in `KhiveRuntime::embedder(name)` lookups and as the storage
398    /// table suffix for vector indices. Use the model's canonical short form
399    /// (e.g. `"all-minilm-l6-v2"`, `"my-custom-encoder"`).
400    fn name(&self) -> &str;
401
402    /// Output vector dimension for this embedder.
403    ///
404    /// Must be consistent with what [`build`](Self::build) produces.
405    /// The runtime uses this to pre-register the vector store columns.
406    fn dimensions(&self) -> usize;
407
408    /// Construct the underlying [`EmbeddingService`].
409    ///
410    /// Called at most once per process. The result is cached in a
411    /// [`OnceCell`]; concurrent callers block on the first call and share
412    /// the result thereafter.
413    async fn build(&self) -> RuntimeResult<Arc<dyn EmbeddingService>>;
414}
415
416/// An entry in the [`EmbedderRegistry`] combining a provider with its
417/// lazy-initialized service.
418pub(crate) struct EmbedderEntry {
419    provider: Arc<dyn EmbedderProvider>,
420    cell: Arc<OnceCell<Arc<dyn EmbeddingService>>>,
421    serving: Arc<AtomicBool>,
422    /// Only the runtime's built-in lattice provider has an audited document
423    /// preparation path. Pack replacements, even under a built-in name, do not.
424    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/// Registry of named [`EmbedderProvider`] instances.
439///
440/// Built during `KhiveRuntime` construction and optionally extended by packs
441/// via [`crate::KhiveRuntime::register_embedder`]. The registry is internally
442/// reference-counted so `KhiveRuntime::clone()` shares the same providers
443/// and cached service instances.
444#[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    /// Create an empty registry.
454    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    /// Register a provider, preserving its position for later name enumeration.
464    ///
465    /// Before a provider is configured-bound or selected for resolution,
466    /// registration with the same name replaces it in place. Afterwards,
467    /// duplicate registration leaves the bound or serving provider unchanged.
468    pub fn register<P: EmbedderProvider + 'static>(&mut self, provider: P) -> RuntimeResult<()> {
469        self.insert(provider, false)
470    }
471
472    /// Register the runtime-owned lattice adapter whose passage preparation is
473    /// audited. This is deliberately not available to pack providers.
474    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    /// Test-only attested provider. The wrapper below owns passage preparation,
480    /// so a fake backend cannot change the bytes whose digest is recorded.
481    #[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    /// Test-only replacement of a provider whose name is already bound.
493    ///
494    /// A test that builds its host through the production startup path finds
495    /// the configured names bound before it can register a deterministic
496    /// backend, and [`Self::register`] refuses a bound name. This swaps the
497    /// provider under such a name and leaves the bound engine list unchanged.
498    /// It refuses a name that is not bound, a provider that is already
499    /// serving, and dimensions that differ from the bound provider's.
500    #[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    /// Look up a provider by name.
582    pub fn get_provider(&self, name: &str) -> Option<&dyn EmbedderProvider> {
583        self.entries.get(name).map(|e| e.provider.as_ref())
584    }
585
586    /// Returns `true` if a provider with this name is registered.
587    pub fn contains(&self, name: &str) -> bool {
588        self.entries.contains_key(name)
589    }
590
591    /// Names of all registered providers, in registration order.
592    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    /// Return a cloned entry for `name` without holding any lock.
650    ///
651    /// The caller can then call [`EmbedderEntry::resolve`] without holding
652    /// a lock — this avoids holding a `RwLockGuard` across `await` points.
653    /// It marks the provider as selected for serving so registration cannot
654    /// replace it while a request is resolving the service. Returns `None` if
655    /// `name` is not registered.
656    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    /// Lazily resolve a registered provider to its live [`EmbeddingService`].
663    ///
664    /// Returns [`RuntimeError::UnknownModel`] if `name` is not registered.
665    /// The first call for a given name triggers [`EmbedderProvider::build`];
666    /// subsequent calls return the cached `Arc`.
667    ///
668    /// Prefer [`crate::KhiveRuntime::embedder`] over calling this directly from pack
669    /// handlers — the runtime method handles alias resolution and error mapping.
670    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/// Deliberately does not delegate `embed_passage`: the lattice trait default
704/// applies the model's document instruction before calling `embed`, exactly
705/// as the audited built-in path does. Test providers supply only fake vectors.
706#[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    /// Lazily initialise and return the embedding service for this entry.
739    ///
740    /// `OnceCell::get_or_try_init` single-flights concurrent cold callers: only
741    /// one of them runs [`EmbedderProvider::build`], and every other waiter
742    /// receives the same result once it completes. Only the caller whose task
743    /// actually ran `build()` gets back `Some(duration)`; every other caller
744    /// (cache hit or wait-for-in-flight-build) gets `None`.
745    ///
746    /// Returns `RuntimeError` if `build()` fails, rather than panicking. On
747    /// failure the cell stays unset, so a later call retries the build.
748    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
770// ── LatticeEmbedderProvider ───────────────────────────────────────────────────
771
772/// Adapter that wraps a [`lattice_embed::EmbeddingModel`] as an
773/// [`EmbedderProvider`].
774///
775/// All built-in models (MiniLM, paraphrase-multilingual, BGE variants, etc.)
776/// are registered as `LatticeEmbedderProvider` instances during
777/// `KhiveRuntime` construction. External callers do not need to use this type
778/// unless they are constructing a custom registry from scratch.
779pub struct LatticeEmbedderProvider {
780    model: EmbeddingModel,
781    /// Cached `to_string()` result so `name()` can return `&str`.
782    name: String,
783}
784
785impl LatticeEmbedderProvider {
786    /// Create a new provider wrapping the given lattice model.
787    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        // Embedder loading frees transient heap that glibc would otherwise retain.
807        #[cfg(all(target_os = "linux", target_env = "gnu"))]
808        // SAFETY: malloc_trim has no preconditions. Its 0/1 release status is intentionally ignored.
809        unsafe {
810            libc::malloc_trim(0);
811        }
812        Ok(cached_blocking_service(native))
813    }
814}
815
816/// Keep result-cache lookup outside worker admission for every retrieval role.
817fn 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// ── Unit tests ────────────────────────────────────────────────────────────────
829
830#[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    /// A trivial embedding service that returns a constant vector of `1.0`s.
856    /// The `model` parameter is ignored — this service always returns the
857    /// same synthetic vector regardless of which model is requested.
858    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            // This custom service owns its inputs; a built-in instruction must
1005            // not rewrite the sentinel that releases its synthetic worker.
1006            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        // Another order, a shorter list and a changed field are each a different list.
1678        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(&reg);
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}