use std::sync::Arc;
use anyhow::Result;
use super::contract::{EmbedRole, Embedder};
use super::error::EngineError;
use super::retry::{Decision, RetryPolicy};
pub const MAX_INPUTS: usize = 64;
const _: () = assert!(MAX_INPUTS < 100);
pub struct BatchedEmbedder {
inner: Arc<dyn Embedder>,
cap: usize,
}
impl BatchedEmbedder {
pub fn wrap(inner: Arc<dyn Embedder>) -> Arc<dyn Embedder> {
Arc::new(Self {
inner,
cap: MAX_INPUTS,
})
}
#[cfg(test)]
fn with_cap(inner: Arc<dyn Embedder>, cap: usize) -> Self {
Self { inner, cap }
}
async fn part(&self, texts: Vec<String>, role: EmbedRole) -> Result<Vec<Vec<f32>>> {
let asked = texts.len();
let vectors = self.inner.embed(texts, role).await?;
anyhow::ensure!(
vectors.len() == asked,
"the embedder answered {} vectors for {asked} inputs",
vectors.len()
);
Ok(vectors)
}
}
#[async_trait::async_trait]
impl Embedder for BatchedEmbedder {
async fn embed(&self, texts: Vec<String>, role: EmbedRole) -> Result<Vec<Vec<f32>>> {
if texts.len() <= self.cap {
return self.part(texts, role).await;
}
let mut vectors = Vec::with_capacity(texts.len());
let mut rest = texts.into_iter().peekable();
while rest.peek().is_some() {
let part: Vec<String> = rest.by_ref().take(self.cap).collect();
vectors.extend(self.part(part, role).await?);
}
Ok(vectors)
}
}
pub struct RetryEmbedder {
inner: Arc<dyn Embedder>,
policy: RetryPolicy,
}
impl RetryEmbedder {
pub fn wrap(inner: Arc<dyn Embedder>) -> Arc<dyn Embedder> {
Arc::new(Self {
inner,
policy: RetryPolicy::default(),
})
}
#[cfg(test)]
fn with_policy(inner: Arc<dyn Embedder>, policy: RetryPolicy) -> Self {
Self { inner, policy }
}
}
#[async_trait::async_trait]
impl Embedder for RetryEmbedder {
async fn embed(&self, texts: Vec<String>, role: EmbedRole) -> Result<Vec<Vec<f32>>> {
let mut attempt = 1u32;
let mut outcome = self.inner.embed(texts.clone(), role).await;
while let Err(error) = &outcome {
let (transient, retry_after) = match error.downcast_ref::<EngineError>() {
Some(typed) => (typed.is_transient(), typed.retry_after),
None => (false, None),
};
let Decision::Wait(delay) = self.policy.decide(attempt, transient, retry_after) else {
break;
};
tracing::warn!(
attempt,
max = self.policy.max_attempts,
wait_ms = delay.as_millis() as u64,
error = %error,
"the embedding request failed; trying again"
);
tokio::time::sleep(delay).await;
attempt += 1;
outcome = self.inner.embed(texts.clone(), role).await;
}
outcome
}
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::sync::Mutex;
use std::time::Duration;
use super::*;
use crate::shared::api::error::EngineErrorKind;
enum Step {
Answer,
Short,
Fail(EngineError),
Untyped,
}
struct Scripted {
steps: Mutex<VecDeque<Step>>,
asked: Mutex<Vec<Vec<String>>>,
}
impl Scripted {
fn new(steps: Vec<Step>) -> Arc<Self> {
Arc::new(Self {
steps: Mutex::new(steps.into()),
asked: Mutex::new(Vec::new()),
})
}
fn sizes(&self) -> Vec<usize> {
self.asked.lock().unwrap().iter().map(Vec::len).collect()
}
}
#[async_trait::async_trait]
impl Embedder for Scripted {
async fn embed(&self, texts: Vec<String>, _role: EmbedRole) -> Result<Vec<Vec<f32>>> {
self.asked.lock().unwrap().push(texts.clone());
let step = self.steps.lock().unwrap().pop_front();
let vector = |t: &String| vec![t.len() as f32];
match step.unwrap_or(Step::Answer) {
Step::Answer => Ok(texts.iter().map(vector).collect()),
Step::Short => Ok(texts.iter().skip(1).map(vector).collect()),
Step::Fail(error) => Err(error.into()),
Step::Untyped => anyhow::bail!("decoding embeddings response"),
}
}
}
fn status(code: u16, retry_after: Option<Duration>) -> EngineError {
EngineError {
kind: EngineErrorKind::Status,
status: Some(code),
retry_after,
message: format!("embedding server returned status {code}"),
}
}
fn texts(n: usize) -> Vec<String> {
(1..=n).map(|len| "x".repeat(len)).collect()
}
fn lengths(vectors: &[Vec<f32>]) -> Vec<usize> {
vectors.iter().map(|v| v[0] as usize).collect()
}
#[tokio::test]
async fn a_long_input_is_sent_in_parts_and_answered_in_order() {
let inner = Scripted::new(vec![]);
let capped = BatchedEmbedder::with_cap(inner.clone(), 4);
let vectors = capped.embed(texts(10), EmbedRole::Passage).await.unwrap();
assert_eq!(inner.sizes(), [4, 4, 2]);
assert_eq!(lengths(&vectors), (1..=10).collect::<Vec<_>>());
for (n, sizes) in [(4, vec![4]), (5, vec![4, 1]), (8, vec![4, 4]), (1, vec![1])] {
let inner = Scripted::new(vec![]);
let capped = BatchedEmbedder::with_cap(inner.clone(), 4);
let vectors = capped.embed(texts(n), EmbedRole::Query).await.unwrap();
assert_eq!(inner.sizes(), sizes, "{n} texts");
assert_eq!(lengths(&vectors), (1..=n).collect::<Vec<_>>(), "{n} texts");
}
}
#[tokio::test]
async fn an_empty_input_is_passed_on_as_it_is() {
let inner = Scripted::new(vec![]);
let capped = BatchedEmbedder::with_cap(inner.clone(), 4);
assert!(
capped
.embed(vec![], EmbedRole::Passage)
.await
.unwrap()
.is_empty()
);
assert_eq!(inner.sizes(), [0]);
}
#[tokio::test]
async fn every_embedder_gets_the_measured_cap() {
let inner = Scripted::new(vec![]);
let capped = BatchedEmbedder::wrap(inner.clone());
capped
.embed(texts(MAX_INPUTS + 1), EmbedRole::Passage)
.await
.unwrap();
assert_eq!(inner.sizes(), [MAX_INPUTS, 1]);
}
#[tokio::test]
async fn an_answer_with_a_vector_missing_is_refused() {
let inner = Scripted::new(vec![Step::Answer, Step::Short]);
let capped = BatchedEmbedder::with_cap(inner.clone(), 4);
let refused = capped
.embed(texts(10), EmbedRole::Passage)
.await
.unwrap_err();
assert_eq!(
refused.to_string(),
"the embedder answered 3 vectors for 4 inputs"
);
assert_eq!(inner.sizes(), [4, 4], "and the third part is not asked for");
let inner = Scripted::new(vec![Step::Short]);
let capped = BatchedEmbedder::with_cap(inner, 4);
assert!(capped.embed(texts(3), EmbedRole::Passage).await.is_err());
}
#[tokio::test]
async fn a_failed_part_fails_the_request() {
let inner = Scripted::new(vec![Step::Answer, Step::Fail(status(400, None))]);
let capped = BatchedEmbedder::with_cap(inner.clone(), 4);
let failed = capped
.embed(texts(10), EmbedRole::Passage)
.await
.unwrap_err();
assert_eq!(failed.to_string(), "embedding server returned status 400");
assert_eq!(inner.sizes(), [4, 4]);
}
fn quick() -> RetryPolicy {
RetryPolicy {
jitter: 0.0,
..RetryPolicy::default()
}
}
#[tokio::test(start_paused = true)]
async fn a_passing_failure_is_retried_with_the_same_texts() {
for code in [429, 503, 502, 529] {
let inner = Scripted::new(vec![Step::Fail(status(code, None))]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
let started = tokio::time::Instant::now();
let vectors = retrying.embed(texts(3), EmbedRole::Query).await.unwrap();
assert_eq!(lengths(&vectors), [1, 2, 3]);
assert_eq!(inner.sizes(), [3, 3], "{code}");
assert_eq!(started.elapsed(), Duration::from_secs(1), "{code}");
}
}
#[tokio::test(start_paused = true)]
async fn no_answer_is_retried() {
let lost = EngineError {
kind: EngineErrorKind::Transport,
status: None,
retry_after: None,
message: "connection closed".into(),
};
let inner = Scripted::new(vec![Step::Fail(lost)]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
assert!(retrying.embed(texts(2), EmbedRole::Passage).await.is_ok());
assert_eq!(inner.sizes(), [2, 2]);
}
#[tokio::test(start_paused = true)]
async fn the_attempts_are_counted_and_the_last_failure_is_the_one_reported() {
let inner = Scripted::new(vec![
Step::Fail(status(429, None)),
Step::Fail(status(503, None)),
Step::Fail(status(502, None)),
Step::Answer,
]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
let started = tokio::time::Instant::now();
let failed = retrying
.embed(texts(1), EmbedRole::Passage)
.await
.unwrap_err();
assert_eq!(failed.to_string(), "embedding server returned status 502");
assert_eq!(inner.sizes().len(), 3, "and not a fourth");
assert_eq!(started.elapsed(), Duration::from_secs(1 + 2));
}
#[tokio::test(start_paused = true)]
async fn a_refusal_is_not_retried() {
for code in [400, 401, 403, 404, 422] {
let inner = Scripted::new(vec![Step::Fail(status(code, None))]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
assert!(retrying.embed(texts(1), EmbedRole::Passage).await.is_err());
assert_eq!(inner.sizes().len(), 1, "{code}");
}
let inner = Scripted::new(vec![Step::Untyped]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
assert!(retrying.embed(texts(1), EmbedRole::Passage).await.is_err());
assert_eq!(inner.sizes().len(), 1, "a failure without a verdict");
}
#[tokio::test(start_paused = true)]
async fn the_providers_wait_is_honoured_within_reason() {
let asked = Duration::from_secs(7);
let inner = Scripted::new(vec![Step::Fail(status(429, Some(asked)))]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
let started = tokio::time::Instant::now();
assert!(retrying.embed(texts(1), EmbedRole::Passage).await.is_ok());
assert_eq!(started.elapsed(), asked);
let minutes = Duration::from_secs(600);
let inner = Scripted::new(vec![Step::Fail(status(429, Some(minutes)))]);
let retrying = RetryEmbedder::with_policy(inner.clone(), quick());
let started = tokio::time::Instant::now();
assert!(retrying.embed(texts(1), EmbedRole::Passage).await.is_err());
assert_eq!(inner.sizes().len(), 1);
assert_eq!(started.elapsed(), Duration::ZERO);
}
#[tokio::test(start_paused = true)]
async fn a_part_is_retried_by_itself() {
let inner = Scripted::new(vec![Step::Answer, Step::Fail(status(429, None))]);
let retrying: Arc<dyn Embedder> =
Arc::new(RetryEmbedder::with_policy(inner.clone(), quick()));
let stacked = BatchedEmbedder::with_cap(retrying, 4);
let vectors = stacked.embed(texts(10), EmbedRole::Passage).await.unwrap();
assert_eq!(inner.sizes(), [4, 4, 4, 2]);
assert_eq!(lengths(&vectors), (1..=10).collect::<Vec<_>>());
let asked = inner.asked.lock().unwrap();
assert_eq!(asked[1], asked[2], "the second part, twice");
}
}