use std::fmt;
use std::sync::Arc;
use tokio::sync::broadcast;
use crate::exec::JobStatus;
use crate::llm::Usage;
use crate::pricing::Price;
use crate::runpod::PodStatus;
use crate::system::SystemSample;
use crate::train::{Mark, TrainMetric};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
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>,
cost: Option<f64>,
excluded: bool,
},
ItemFailed {
stage: Stage,
id: String,
error: String,
retryable: bool,
},
StageModel {
stage: Stage,
model: String,
},
StageFinished {
stage: Stage,
stats: StageStats,
},
RunWatched {
run_id: String,
job: Option<crate::runs::JobKind>,
},
Metric(TrainMetric),
Mark(Mark),
JobStatus(JobStatus),
PodStatus(PodStatus),
System(SystemSample),
Push {
run_id: String,
done: u64,
total: u64,
},
Judged {
compare_id: String,
done: u64,
total: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemResult {
Done,
Skipped,
Excluded,
Failed,
}
impl ItemResult {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Done => "done",
Self::Skipped => "skipped",
Self::Excluded => "excluded",
Self::Failed => "failed",
}
}
}
impl Event {
#[must_use]
pub fn item_result(&self) -> Option<(Stage, ItemResult)> {
match self {
Self::ItemDone {
stage,
usage: Some(_),
excluded: true,
..
} => Some((*stage, ItemResult::Excluded)),
Self::ItemDone {
stage,
usage: Some(_),
..
} => Some((*stage, ItemResult::Done)),
Self::ItemDone {
stage, usage: None, ..
} => Some((*stage, ItemResult::Skipped)),
Self::ItemFailed {
stage,
retryable: false,
..
} => Some((*stage, ItemResult::Failed)),
_ => None,
}
}
}
pub trait Observer: Send + Sync {
fn event(&self, bus: usize, event: &Event);
fn closed(&self, bus: usize);
}
#[derive(Debug, Clone)]
pub struct EventBus {
sender: broadcast::Sender<Event>,
id: Arc<BusId>,
}
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 {
Self::observed(capacity, None)
}
#[must_use]
pub fn observed(capacity: usize, observer: Option<Arc<dyn Observer>>) -> Self {
let (sender, _) = broadcast::channel(capacity.max(1));
Self {
sender,
id: Arc::new(BusId { observer }),
}
}
pub fn publish(&self, event: Event) {
if let Some(observer) = &self.id.observer {
observer.event(self.id.key(), &event);
}
self.sender.send(event).ok();
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.sender.subscribe()
}
#[cfg(test)]
pub(crate) fn receiver_count(&self) -> usize {
self.sender.receiver_count()
}
}
struct BusId {
observer: Option<Arc<dyn Observer>>,
}
impl BusId {
fn key(&self) -> usize {
std::ptr::from_ref(self).addr()
}
}
impl fmt::Debug for BusId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BusId")
.field("observed", &self.observer.is_some())
.finish()
}
}
impl Drop for BusId {
fn drop(&mut self) {
if let Some(observer) = &self.observer {
observer.closed(self.key());
}
}
}
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(())
}
#[derive(Default)]
struct Seen(std::sync::Mutex<Vec<Result<(usize, Event), usize>>>);
impl Observer for Seen {
fn event(&self, bus: usize, event: &Event) {
if let Ok(mut seen) = self.0.lock() {
seen.push(Ok((bus, event.clone())));
}
}
fn closed(&self, bus: usize) {
if let Ok(mut seen) = self.0.lock() {
seen.push(Err(bus));
}
}
}
#[test]
fn an_observer_sees_every_event_of_its_buses_and_their_end() -> Result<(), String> {
let seen = Arc::new(Seen::default());
let observer: Arc<dyn Observer> = seen.clone();
let first = EventBus::observed(1, Some(Arc::clone(&observer)));
let second = EventBus::observed(1, Some(observer));
let clone = first.clone();
let (a, b) = (first.id.key(), second.id.key());
assert_ne!(a, b);
let started = |total| Event::StageStarted {
stage: Stage::Answers,
total,
};
for total in 0..3 {
clone.publish(started(total));
}
second.publish(started(9));
EventBus::new().publish(started(5));
drop(first);
drop(clone);
let seen = seen.0.lock().map_err(|e| e.to_string())?;
assert_eq!(
*seen,
[
Ok((a, started(0))),
Ok((a, started(1))),
Ok((a, started(2))),
Ok((b, started(9))),
Err(a),
]
);
Ok(())
}
#[test]
fn item_events_say_how_their_item_ended() {
let done = |usage| Event::ItemDone {
stage: Stage::Answers,
id: "a".into(),
usage,
cost: None,
excluded: false,
};
let failed = |retryable| Event::ItemFailed {
stage: Stage::Questions,
id: "q".into(),
error: "no".into(),
retryable,
};
assert_eq!(
done(Some(Usage::default())).item_result(),
Some((Stage::Answers, ItemResult::Done))
);
assert_eq!(
done(None).item_result(),
Some((Stage::Answers, ItemResult::Skipped))
);
assert_eq!(
failed(false).item_result(),
Some((Stage::Questions, ItemResult::Failed))
);
assert_eq!(
failed(true).item_result(),
None,
"a retried attempt ends nothing"
);
assert_eq!(
Event::StageStarted {
stage: Stage::Split,
total: 1
}
.item_result(),
None
);
assert_eq!(
[
ItemResult::Done,
ItemResult::Skipped,
ItemResult::Excluded,
ItemResult::Failed
]
.map(ItemResult::name),
["done", "skipped", "excluded", "failed"]
);
}
#[test]
fn stage_serializes_as_its_name() -> Result<(), serde_json::Error> {
for stage in [
Stage::Subtopics,
Stage::Questions,
Stage::Answers,
Stage::Split,
] {
assert_eq!(
serde_json::to_string(&stage)?,
format!("\"{}\"", stage.name())
);
assert_eq!(
serde_json::from_str::<Stage>(&format!("\"{}\"", stage.name()))?,
stage
);
}
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(())
}
}