use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel};
use std::sync::{Arc, Mutex, PoisonError};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use omgbase_search::EmbeddingProvider;
#[derive(Clone)]
pub struct SharedProvider {
model: String,
dim: usize,
max_input_tokens: Option<u32>,
inner: Arc<Mutex<Box<dyn EmbeddingProvider + Send>>>,
}
impl SharedProvider {
pub fn new(provider: Box<dyn EmbeddingProvider + Send>) -> Self {
Self {
model: provider.model().to_owned(),
dim: provider.dim(),
max_input_tokens: provider.max_input_tokens(),
inner: Arc::new(Mutex::new(provider)),
}
}
#[must_use]
pub fn handles(&self) -> usize {
Arc::strong_count(&self.inner)
}
}
impl std::fmt::Debug for SharedProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedProvider")
.field("model", &self.model)
.field("dim", &self.dim)
.field("handles", &self.handles())
.finish_non_exhaustive()
}
}
impl EmbeddingProvider for SharedProvider {
fn model(&self) -> &str {
&self.model
}
fn dim(&self) -> usize {
self.dim
}
fn max_input_tokens(&self) -> Option<u32> {
self.max_input_tokens
}
fn embed(&self, texts: &[String]) -> omgbase_search::Result<Vec<Vec<f32>>> {
let guard = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
guard.embed(texts)
}
}
pub const DEBOUNCE: Duration = Duration::from_millis(500);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DrainReport {
pub embedded: usize,
pub pooled: usize,
}
impl DrainReport {
#[must_use]
pub fn did_work(&self) -> bool {
self.embedded > 0 || self.pooled > 0
}
}
pub enum Msg {
Schedule,
Flush(Sender<()>),
Close,
}
#[derive(Clone)]
pub struct DrainHandle(Sender<Msg>);
impl DrainHandle {
pub fn schedule(&self) {
let _ = self.0.send(Msg::Schedule);
}
}
pub type DrainFn = Box<dyn FnMut() -> Result<DrainReport, String>>;
pub struct Drainer {
tx: Sender<Msg>,
thread: Option<JoinHandle<()>>,
}
impl Drainer {
pub fn spawn<I>(debounce: Duration, init: I) -> Self
where
I: FnOnce() -> Result<DrainFn, String> + Send + 'static,
{
let (tx, rx) = channel();
let thread = std::thread::Builder::new()
.name("omgbase-embed-drain".to_owned())
.spawn(move || {
let mut log = |line: String| eprintln!("{line}");
match init() {
Ok(mut drain) => run_loop(&rx, debounce, &mut *drain, &mut log),
Err(e) => {
log(format!("[mcp] embed drain disabled: {e}"));
idle(&rx);
}
}
})
.expect("spawn the embed drain thread");
Self {
tx,
thread: Some(thread),
}
}
#[must_use]
pub fn handle(&self) -> DrainHandle {
DrainHandle(self.tx.clone())
}
pub fn flush(&self) {
let (ack_tx, ack_rx) = channel();
if self.tx.send(Msg::Flush(ack_tx)).is_ok() {
let _ = ack_rx.recv();
}
}
pub fn close(mut self) {
let _ = self.tx.send(Msg::Close);
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
impl Drop for Drainer {
fn drop(&mut self) {
let _ = self.tx.send(Msg::Close);
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
fn idle(rx: &Receiver<Msg>) {
while let Ok(msg) = rx.recv() {
match msg {
Msg::Schedule => {}
Msg::Flush(ack) => {
let _ = ack.send(());
}
Msg::Close => return,
}
}
}
pub fn run_loop(
rx: &Receiver<Msg>,
debounce: Duration,
drain: &mut dyn FnMut() -> Result<DrainReport, String>,
log: &mut dyn FnMut(String),
) {
let mut dirty = false;
let mut deadline: Option<Instant> = None;
loop {
let msg = match deadline {
Some(dl) => match rx.recv_timeout(dl.saturating_duration_since(Instant::now())) {
Ok(m) => Some(m),
Err(RecvTimeoutError::Timeout) => None,
Err(RecvTimeoutError::Disconnected) => return,
},
None => match rx.recv() {
Ok(m) => Some(m),
Err(_) => return,
},
};
match msg {
Some(Msg::Schedule) => {
dirty = true;
deadline = Some(Instant::now() + debounce);
}
Some(Msg::Flush(ack)) => {
deadline = None;
dirty = true;
let closed = run_until_clean(rx, &mut dirty, drain, log, vec![ack]);
if closed {
return;
}
}
Some(Msg::Close) => return,
None => {
deadline = None;
if run_until_clean(rx, &mut dirty, drain, log, Vec::new()) {
return;
}
}
}
}
}
fn run_until_clean(
rx: &Receiver<Msg>,
dirty: &mut bool,
drain: &mut dyn FnMut() -> Result<DrainReport, String>,
log: &mut dyn FnMut(String),
mut acks: Vec<Sender<()>>,
) -> bool {
let mut closed = false;
while *dirty && !closed {
*dirty = false;
match drain() {
Ok(report) => {
if report.did_work() {
log(format!("[mcp] embedded {} block(s)", report.embedded));
}
}
Err(e) => {
log(format!("[mcp] embed drain failed: {e}"));
break;
}
}
loop {
match rx.try_recv() {
Ok(Msg::Schedule) => *dirty = true,
Ok(Msg::Flush(ack)) => {
*dirty = true;
acks.push(ack);
}
Ok(Msg::Close) => closed = true,
Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
}
}
}
for ack in acks {
let _ = ack.send(());
}
closed
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
const FAST: Duration = Duration::from_millis(40);
#[derive(Default)]
struct Counting {
spawned: Arc<AtomicUsize>,
calls: Arc<AtomicUsize>,
in_flight: Arc<AtomicUsize>,
max_in_flight: Arc<AtomicUsize>,
}
impl Counting {
fn spawn(&self) -> Box<dyn EmbeddingProvider + Send> {
self.spawned.fetch_add(1, Ordering::SeqCst);
Box::new(Counting {
spawned: Arc::clone(&self.spawned),
calls: Arc::clone(&self.calls),
in_flight: Arc::clone(&self.in_flight),
max_in_flight: Arc::clone(&self.max_in_flight),
})
}
}
impl EmbeddingProvider for Counting {
fn model(&self) -> &str {
"counting-2"
}
fn dim(&self) -> usize {
2
}
fn max_input_tokens(&self) -> Option<u32> {
Some(16)
}
fn embed(&self, texts: &[String]) -> omgbase_search::Result<Vec<Vec<f32>>> {
let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
self.max_in_flight.fetch_max(now, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(2));
self.calls.fetch_add(1, Ordering::SeqCst);
self.in_flight.fetch_sub(1, Ordering::SeqCst);
Ok(texts.iter().map(|_| vec![1.0, 0.0]).collect())
}
}
#[test]
fn two_handles_share_one_provider_and_serialize_its_calls() {
let counting = Counting::default();
let shared = SharedProvider::new(counting.spawn());
assert_eq!(
(shared.model(), shared.dim(), shared.max_input_tokens()),
("counting-2", 2, Some(16)),
"the identity is the provider's, served without the lock"
);
let query_path = shared.clone();
let drain_path = shared.clone();
assert_eq!(shared.handles(), 3);
let drain = std::thread::spawn(move || {
for _ in 0..20 {
let batch = vec!["a".to_owned(), "b".to_owned()];
assert_eq!(drain_path.embed(&batch).unwrap().len(), 2);
}
});
for _ in 0..20 {
assert_eq!(query_path.embed_query("q").unwrap(), vec![1.0, 0.0]);
}
drain.join().unwrap();
assert_eq!(counting.spawned.load(Ordering::SeqCst), 1, "one provider");
assert_eq!(counting.calls.load(Ordering::SeqCst), 40);
assert_eq!(
counting.max_in_flight.load(Ordering::SeqCst),
1,
"calls from the two handles never overlap"
);
drop(query_path);
assert_eq!(
shared.handles(),
1,
"the drain thread's clone went with its thread, the query path's with the surface"
);
}
struct Harness {
tx: Sender<Msg>,
calls: Arc<AtomicUsize>,
log: Arc<Mutex<Vec<String>>>,
thread: Option<JoinHandle<()>>,
}
impl Harness {
fn start<F>(on_call: F) -> Self
where
F: Fn(usize, &Sender<Msg>) -> Result<DrainReport, String> + Send + 'static,
{
let (tx, rx) = channel();
let calls = Arc::new(AtomicUsize::new(0));
let log = Arc::new(Mutex::new(Vec::new()));
let (c, l, t) = (Arc::clone(&calls), Arc::clone(&log), tx.clone());
let thread = std::thread::spawn(move || {
let mut drain = move || {
let n = c.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(15));
on_call(n, &t)
};
let mut log = move |line: String| l.lock().unwrap().push(line);
run_loop(&rx, FAST, &mut drain, &mut log);
});
Self {
tx,
calls,
log,
thread: Some(thread),
}
}
fn schedule(&self) {
self.tx.send(Msg::Schedule).unwrap();
}
fn flush(&self) {
let (ack, rx) = channel();
self.tx.send(Msg::Flush(ack)).unwrap();
rx.recv().unwrap();
}
fn calls(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
fn close(mut self) -> Vec<String> {
self.tx.send(Msg::Close).unwrap();
self.thread.take().unwrap().join().unwrap();
self.log.lock().unwrap().clone()
}
}
fn ok(embedded: usize) -> Result<DrainReport, String> {
Ok(DrainReport {
embedded,
pooled: 0,
})
}
#[test]
fn a_burst_of_schedules_is_one_debounced_drain() {
let h = Harness::start(|_, _| ok(3));
for _ in 0..5 {
h.schedule();
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(h.calls(), 0, "nothing runs before the debounce elapses");
std::thread::sleep(FAST * 3);
assert_eq!(h.calls(), 1);
let log = h.close();
assert_eq!(log, ["[mcp] embedded 3 block(s)"]);
}
#[test]
fn a_schedule_during_a_drain_reruns_once_without_debounce() {
let h = Harness::start(|n, tx| {
if n == 0 {
tx.send(Msg::Schedule).unwrap();
}
ok(if n == 0 { 1 } else { 0 })
});
h.schedule();
std::thread::sleep(FAST + Duration::from_millis(60));
assert_eq!(h.calls(), 2, "one re-run, then clean");
std::thread::sleep(FAST * 2);
assert_eq!(h.calls(), 2, "no third pass without a new schedule");
let log = h.close();
assert_eq!(
log,
["[mcp] embedded 1 block(s)"],
"a clean pass logs nothing"
);
}
#[test]
fn flush_drains_now_and_waits() {
let h = Harness::start(|_, _| ok(0));
h.flush();
assert_eq!(h.calls(), 1, "flush always drains, dirty or not");
h.schedule();
h.flush();
assert_eq!(
h.calls(),
2,
"the pending debounce collapses into the flush"
);
std::thread::sleep(FAST * 2);
assert_eq!(h.calls(), 2);
assert!(h.close().is_empty());
}
#[test]
fn a_failing_drain_is_logged_and_retried_on_the_next_schedule() {
let h = Harness::start(|n, _| {
if n == 0 {
Err("provider down".into())
} else {
ok(2)
}
});
h.schedule();
std::thread::sleep(FAST * 3);
assert_eq!(h.calls(), 1);
h.schedule();
std::thread::sleep(FAST * 3);
assert_eq!(h.calls(), 2);
let log = h.close();
assert_eq!(
log,
[
"[mcp] embed drain failed: provider down",
"[mcp] embedded 2 block(s)"
]
);
}
#[test]
fn close_stops_before_a_pending_debounce_fires() {
let h = Harness::start(|_, _| ok(1));
h.schedule();
let log = h.close();
assert!(log.is_empty());
}
#[test]
fn drainer_spawn_runs_init_on_the_thread_and_disables_on_failure() {
let calls = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&calls);
let d = Drainer::spawn(FAST, move || {
let c = Arc::clone(&c);
Ok(Box::new(move || {
c.fetch_add(1, Ordering::SeqCst);
ok(0)
}) as DrainFn)
});
let handle = d.handle();
handle.schedule();
d.flush();
assert_eq!(calls.load(Ordering::SeqCst), 1);
d.close();
handle.schedule();
let broken = Drainer::spawn(FAST, || Err("no embedder".to_owned()));
broken.handle().schedule();
broken.flush(); broken.close();
}
}