use std::num::NonZeroUsize;
use std::thread;
use super::job::Embed;
use super::{Embedded, Embedder, Settings};
use crate::serve::storage::Failure;
pub struct Workers<E = Embedder> {
workers: Vec<E>,
}
impl Workers<Embedder> {
#[must_use]
pub fn new(settings: Option<&Settings>, count: NonZeroUsize) -> Self {
let workers = (0..count.get())
.map(|_worker| Embedder::new(settings.cloned()))
.collect();
Self { workers }
}
}
impl<E: Embed + Sync> Workers<E> {
pub fn of(workers: Vec<E>) -> Result<Self, Failure> {
if workers.is_empty() {
return Err(Failure::new(
"embed_workers_none",
"at least one worker is needed",
));
}
Ok(Self { workers })
}
}
impl<E: Embed + Sync> Embed for Workers<E> {
fn documents(&self, texts: &[String]) -> Result<Embedded, Failure> {
let total = self.workers.len();
let Some(first) = self.workers.first() else {
return Err(Failure::new(
"embed_workers_none",
"at least one worker is needed",
));
};
if texts.is_empty() || total == 1 {
return first.documents(texts);
}
let shares = total.min(texts.len());
let answers: Vec<Result<Embedded, Failure>> = thread::scope(|scope| {
let mut running = Vec::with_capacity(shares);
for (share, worker) in shares_of(texts, shares).into_iter().zip(&self.workers) {
running.push(scope.spawn(move || worker.documents(share)));
}
running
.into_iter()
.map(|handle| handle.join().unwrap_or_else(|_panic| Err(panicked())))
.collect()
});
let mut model = None;
let mut answered = Vec::with_capacity(texts.len());
for (index, answer) in answers.into_iter().enumerate() {
let made = answer.map_err(|failure| Failure {
message: format!("worker {} of {total}: {}", index + 1, failure.message),
..failure
})?;
if model.as_ref().is_some_and(|known| *known != made.model) {
return Err(Failure::new(
"embed_model_mixed",
format!("worker {} of {total} answered another model", index + 1),
));
}
if model.is_none() {
model = Some(made.model);
}
answered.extend(made.answers);
}
let model = model.ok_or_else(panicked)?;
Ok(Embedded {
model,
answers: answered,
})
}
}
fn shares_of(texts: &[String], shares: usize) -> Vec<&[String]> {
let base = texts.len() / shares;
let extra = texts.len() % shares;
let mut cut = Vec::with_capacity(shares);
let mut rest = texts;
for index in 0..shares {
let size = base + usize::from(index < extra);
let (share, after) = rest.split_at(size.min(rest.len()));
cut.push(share);
rest = after;
}
cut
}
fn panicked() -> Failure {
Failure::new(
"embed_worker_owner_failed",
"a share's thread panicked before it answered",
)
}
#[cfg(test)]
#[path = "workers_tests.rs"]
mod tests;