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);
}
}
pub(crate) fn merge(&mut self, other: &Self) {
self.done += other.done;
self.skipped += other.skipped;
self.failed += other.failed;
self.excluded += other.excluded;
self.usage += other.usage;
if let Some(cost) = other.cost {
*self.cost.get_or_insert(0.0) += cost;
}
}
}
#[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));
}
#[test]
fn merge_adds_counts_tokens_and_cost() {
let mut total = StageStats {
done: 1,
cost: None,
..StageStats::default()
};
let other = StageStats {
done: 2,
skipped: 1,
failed: 1,
excluded: 1,
usage: Usage {
input_tokens: 3,
output_tokens: 4,
},
cost: Some(0.5),
};
total.merge(&other);
total.merge(&other);
assert_eq!(
(total.done, total.skipped, total.failed, total.excluded),
(5, 2, 2, 2)
);
assert_eq!(
total.usage,
Usage {
input_tokens: 6,
output_tokens: 8
}
);
assert_eq!(total.cost, Some(1.0));
}
#[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(())
}
}