use std::fmt;
use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Sender, SyncSender, TrySendError, sync_channel};
use std::thread::JoinHandle;
use crate::error::Result;
pub(crate) const DEFAULT_DRAIN_CAPACITY: usize = 1024;
enum Msg<T> {
Item(T),
Flush(Sender<()>),
}
pub(crate) struct AppendWorker<T: Send + 'static> {
tx: Option<SyncSender<Msg<T>>>,
dropped: Arc<AtomicU64>,
handle: Option<JoinHandle<()>>,
name: &'static str,
}
impl<T: Send + 'static> AppendWorker<T> {
pub(crate) fn spawn<F, Fut>(name: &'static str, capacity: usize, append: F) -> Self
where
F: Fn(T) -> Fut + Send + 'static,
Fut: Future<Output = Result<()>>,
{
let (tx, rx) = sync_channel::<Msg<T>>(capacity.max(1));
let handle = std::thread::Builder::new()
.name(format!("tinyagents-{name}-drain"))
.spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => {
while rx.recv().is_ok() {}
return;
}
};
rt.block_on(async move {
while let Ok(msg) = rx.recv() {
match msg {
Msg::Item(item) => {
if let Err(e) = append(item).await {
eprintln!("tinyagents: {name} durable append failed: {e}");
}
}
Msg::Flush(ack) => {
let _ = ack.send(());
}
}
}
});
})
.expect("spawn durable-drain thread");
Self {
tx: Some(tx),
dropped: Arc::new(AtomicU64::new(0)),
handle: Some(handle),
name,
}
}
pub(crate) fn submit(&self, item: T) {
let Some(tx) = self.tx.as_ref() else {
return;
};
match tx.try_send(Msg::Item(item)) {
Ok(()) => {}
Err(TrySendError::Full(_)) | Err(TrySendError::Disconnected(_)) => {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub(crate) fn flush(&self) {
let Some(tx) = self.tx.as_ref() else {
return;
};
let (ack_tx, ack_rx) = std::sync::mpsc::channel();
if tx.send(Msg::Flush(ack_tx)).is_ok() {
let _ = ack_rx.recv();
}
}
}
impl<T: Send + 'static> fmt::Debug for AppendWorker<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppendWorker")
.field("name", &self.name)
.field("dropped", &self.dropped.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
impl<T: Send + 'static> Drop for AppendWorker<T> {
fn drop(&mut self) {
self.flush();
drop(self.tx.take());
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}