use std::collections::BinaryHeap;
use std::sync::Arc;
use tokio::sync::mpsc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Priority {
EmbedWrite = 0,
ToolWrite = 1,
}
pub struct WriteJob {
pub priority: Priority,
pub kind: &'static str,
pub run: Box<dyn FnOnce() -> Result<(), String> + Send + 'static>,
}
struct QueuedJob {
seq: u64,
priority: Priority,
kind: &'static str,
run: Option<Box<dyn FnOnce() -> Result<(), String> + Send + 'static>>,
}
impl PartialEq for QueuedJob {
fn eq(&self, other: &Self) -> bool {
self.priority == other.priority && self.seq == other.seq
}
}
impl Eq for QueuedJob {}
impl PartialOrd for QueuedJob {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for QueuedJob {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.priority
.cmp(&other.priority)
.then_with(|| other.seq.cmp(&self.seq))
}
}
pub trait WriteBus: Send + Sync {
fn submit(&self, job: WriteJob) -> Result<(), String>;
fn flush(&self) -> Result<(), String>;
}
pub struct InProcessWriteBus {
tx: mpsc::Sender<QueuedJob>,
rx: std::sync::Mutex<Option<mpsc::Receiver<QueuedJob>>>,
spawned: std::sync::OnceLock<()>,
seq: std::sync::atomic::AtomicU64,
shutdown: Arc<tokio::sync::Notify>,
}
impl InProcessWriteBus {
pub fn new(queue_len: usize) -> Self {
let (tx, rx) = mpsc::channel::<QueuedJob>(queue_len.max(1));
let shutdown = Arc::new(tokio::sync::Notify::new());
Self {
tx,
rx: std::sync::Mutex::new(Some(rx)),
spawned: std::sync::OnceLock::new(),
seq: std::sync::atomic::AtomicU64::new(0),
shutdown,
}
}
fn ensure_worker(&self) {
self.spawned.get_or_init(|| {
let rx = self.rx.lock().unwrap().take().expect("worker started once");
let shutdown_task = self.shutdown.clone();
tokio::spawn(async move {
let mut rx = rx;
let mut heap: BinaryHeap<QueuedJob> = BinaryHeap::new();
loop {
tokio::select! {
biased;
maybe = rx.recv() => {
match maybe {
Some(first) => {
heap.push(first);
while let Ok(next) = rx.try_recv() {
heap.push(next);
}
}
None => {
while let Some(job) = heap.pop() {
if let Some(run) = job.run {
let _ = run();
}
}
break;
}
}
if let Some(job) = heap.pop() {
if let Some(run) = job.run {
let _ = run();
}
}
}
_ = shutdown_task.notified() => break,
}
if rx.is_empty() {
while let Some(job) = heap.pop() {
if let Some(run) = job.run {
let _ = run();
}
}
}
}
});
});
}
pub fn shutdown(&self) {
self.shutdown.notify_one();
}
}
impl Default for InProcessWriteBus {
fn default() -> Self {
Self::new(1024)
}
}
impl WriteBus for InProcessWriteBus {
fn submit(&self, job: WriteJob) -> Result<(), String> {
self.ensure_worker();
let seq = self.seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let queued = QueuedJob {
seq,
priority: job.priority,
kind: job.kind,
run: Some(job.run),
};
self.tx
.try_send(queued)
.map_err(|e| format!("write bus submit failed ({})", e))
}
fn flush(&self) -> Result<(), String> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn priority_orders_tool_before_embed() {
let mut heap: BinaryHeap<QueuedJob> = BinaryHeap::new();
let mk = |seq: u64, p: Priority| QueuedJob {
seq,
priority: p,
kind: "t",
run: None,
};
heap.push(mk(0, Priority::EmbedWrite));
heap.push(mk(1, Priority::ToolWrite));
heap.push(mk(2, Priority::EmbedWrite));
assert_eq!(heap.pop().unwrap().priority, Priority::ToolWrite);
assert_eq!(heap.pop().unwrap().priority, Priority::EmbedWrite);
assert_eq!(heap.pop().unwrap().priority, Priority::EmbedWrite);
}
#[test]
fn fifo_within_same_priority() {
let mut heap: BinaryHeap<QueuedJob> = BinaryHeap::new();
let mk = |seq: u64| QueuedJob {
seq,
priority: Priority::ToolWrite,
kind: "t",
run: None,
};
heap.push(mk(5));
heap.push(mk(2));
heap.push(mk(9));
assert_eq!(heap.pop().unwrap().seq, 2);
assert_eq!(heap.pop().unwrap().seq, 5);
assert_eq!(heap.pop().unwrap().seq, 9);
}
#[tokio::test(flavor = "multi_thread")]
async fn executes_all_jobs_serially() {
let bus = InProcessWriteBus::default();
let order = Arc::new(std::sync::Mutex::new(Vec::new()));
for i in 0..5 {
let order = order.clone();
bus.submit(WriteJob {
priority: Priority::EmbedWrite,
kind: "embed",
run: Box::new(move || {
order.lock().unwrap().push(i);
Ok(())
}),
})
.unwrap();
}
let tool_order = order.clone();
bus.submit(WriteJob {
priority: Priority::ToolWrite,
kind: "tool",
run: Box::new(move || {
tool_order.lock().unwrap().push(99);
Ok(())
}),
})
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
let got = order.lock().unwrap().clone();
assert_eq!(got.len(), 6, "all jobs must run exactly once, got {got:?}");
assert!(
got.contains(&99),
"tool write must be executed, got {got:?}"
);
bus.shutdown();
}
#[tokio::test(flavor = "multi_thread")]
async fn priority_job_queued_last_still_jumps_embeds() {
let bus = InProcessWriteBus::default();
let order = Arc::new(std::sync::Mutex::new(Vec::new()));
let mut release_txs = Vec::new();
for i in 0..4 {
let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
release_txs.push(release_tx);
let order = order.clone();
bus.submit(WriteJob {
priority: Priority::EmbedWrite,
kind: "embed",
run: Box::new(move || {
let _ = release_rx.recv();
order.lock().unwrap().push(i);
Ok(())
}),
})
.unwrap();
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let tool_order = order.clone();
bus.submit(WriteJob {
priority: Priority::ToolWrite,
kind: "tool",
run: Box::new(move || {
tool_order.lock().unwrap().push(99);
Ok(())
}),
})
.unwrap();
for tx in release_txs {
tx.send(()).unwrap();
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
let got = order.lock().unwrap().clone();
assert_eq!(got.len(), 5, "all jobs must run, got {got:?}");
assert_eq!(
got[1], 99,
"tool write queued last must jump the buffered embeds, got {got:?}"
);
bus.shutdown();
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_accepts_job_on_fresh_bus() {
let bus = InProcessWriteBus::new(1);
let res = bus.submit(WriteJob {
priority: Priority::ToolWrite,
kind: "tool",
run: Box::new(|| Ok(())),
});
assert!(res.is_ok(), "fresh bus must accept a job");
bus.shutdown();
}
}