use std::fmt;
use tokio::sync::broadcast;
use crate::exec::JobStatus;
use crate::llm::Usage;
use crate::pricing::Price;
use crate::runpod::PodStatus;
use crate::train::TrainMetric;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Stage {
Subtopics,
Questions,
Answers,
Split,
}
impl Stage {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Subtopics => "subtopics",
Self::Questions => "questions",
Self::Answers => "answers",
Self::Split => "split",
}
}
}
impl fmt::Display for Stage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StageStats {
pub done: usize,
pub skipped: usize,
pub failed: usize,
pub excluded: usize,
pub usage: Usage,
pub cost: Option<f64>,
}
impl StageStats {
pub fn add_usage(&mut self, usage: Usage, price: Option<&Price>) {
self.usage += usage;
if let Some(price) = price {
*self.cost.get_or_insert(0.0) += price.cost(usage);
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Event {
StageStarted {
stage: Stage,
total: usize,
},
ItemDone {
stage: Stage,
id: String,
usage: Option<Usage>,
},
ItemFailed {
stage: Stage,
id: String,
error: String,
retryable: bool,
},
StageFinished {
stage: Stage,
stats: StageStats,
},
Metric(TrainMetric),
JobStatus(JobStatus),
PodStatus(PodStatus),
}
#[derive(Debug, Clone)]
pub struct EventBus {
sender: broadcast::Sender<Event>,
}
impl EventBus {
pub const CAPACITY: usize = 1024;
#[must_use]
pub fn new() -> Self {
Self::with_capacity(Self::CAPACITY)
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
let (sender, _) = broadcast::channel(capacity.max(1));
Self { sender }
}
pub fn publish(&self, event: Event) {
self.sender.send(event).ok();
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.sender.subscribe()
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn subscribers_receive_published_events() -> Result<(), broadcast::error::RecvError> {
let bus = EventBus::new();
bus.publish(Event::StageStarted {
stage: Stage::Split,
total: 0,
});
let mut receiver = bus.subscribe();
bus.publish(Event::StageStarted {
stage: Stage::Answers,
total: 3,
});
assert_eq!(
receiver.recv().await?,
Event::StageStarted {
stage: Stage::Answers,
total: 3
}
);
Ok(())
}
#[test]
fn cost_is_known_only_with_a_price() {
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 0,
};
let mut stats = StageStats::default();
stats.add_usage(usage, None);
assert_eq!(stats.cost, None);
let price = Price {
input_per_million: 3.0,
output_per_million: 15.0,
};
stats.add_usage(usage, Some(&price));
assert_eq!(stats.usage.input_tokens, 2_000_000);
assert!(stats.cost.is_some_and(|cost| (cost - 3.0).abs() < 1e-9));
}
#[tokio::test]
async fn a_larger_bus_keeps_more_events_for_a_slow_subscriber()
-> Result<(), broadcast::error::RecvError> {
let small = EventBus::new();
let large = EventBus::with_capacity(EventBus::CAPACITY * 2);
let (mut behind, mut kept) = (small.subscribe(), large.subscribe());
for total in 0..EventBus::CAPACITY * 2 {
let event = Event::StageStarted {
stage: Stage::Answers,
total,
};
small.publish(event.clone());
large.publish(event);
}
assert!(matches!(
behind.recv().await,
Err(broadcast::error::RecvError::Lagged(1024))
));
assert_eq!(
kept.recv().await?,
Event::StageStarted {
stage: Stage::Answers,
total: 0
}
);
Ok(())
}
}