Skip to main content

photon/
run.rs

1use std::sync::Arc;
2use std::thread::JoinHandle;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use lasso::ThreadedRodeo;
6
7use photon_batch::{BatchError, BatchStats};
8use photon_core::types::id::RunId;
9use photon_core::types::metric::{Metric, MetricKey, RawPoint, Step};
10use photon_uplink::{UplinkStats, UplinkThreadError};
11use photon_wal::Wal;
12use tokio::sync::oneshot;
13
14use crate::accumulator::Accumulator;
15use crate::error::{FinishError, LogError};
16
17#[derive(Clone, Debug)]
18pub struct RunStats {
19    pub points: u64,
20    pub points_dropped: u64,
21    pub batches: u64,
22    pub bytes_compressed: u64,
23    pub bytes_uncompressed: u64,
24    pub batches_sent: u64,
25    pub batches_acked: u64,
26    pub batches_rejected: u64,
27}
28
29pub(crate) struct UplinkHandle {
30    pub shutdown_tx: Option<oneshot::Sender<()>>,
31    pub handle: JoinHandle<Result<UplinkStats, UplinkThreadError>>,
32}
33
34pub struct Run {
35    id: RunId,
36    accumulator: Accumulator<RawPoint>,
37    interner: Arc<ThreadedRodeo>,
38    batch_handle: JoinHandle<Result<BatchStats, BatchError>>,
39    uplink_handle: Option<UplinkHandle>,
40    wal: Arc<dyn Wal>,
41    points_logged: u64,
42    last_step: Vec<Option<Step>>,
43}
44
45impl Run {
46    pub(crate) fn new(
47        id: RunId,
48        accumulator: Accumulator<RawPoint>,
49        interner: Arc<ThreadedRodeo>,
50        batch_handle: JoinHandle<Result<BatchStats, BatchError>>,
51        uplink_handle: Option<UplinkHandle>,
52        wal: Arc<dyn Wal>,
53    ) -> Self {
54        Self {
55            id,
56            accumulator,
57            interner,
58            batch_handle,
59            uplink_handle,
60            wal,
61            points_logged: 0,
62            last_step: Vec::new(),
63        }
64    }
65
66    /// Log a single metric data point.
67    ///
68    /// Steps must be monotonically increasing per metric key. Logging a step
69    /// less than or equal to the previous step for the same key returns an error.
70    pub fn log(&mut self, key: &str, value: f64, step: u64) -> Result<(), LogError> {
71        Metric::new(key)?;
72        let spur = self.interner.get_or_intern(key);
73        let metric_key = MetricKey::new(lasso::Key::into_usize(spur));
74        let step = Step::new(step);
75
76        let idx = metric_key.index();
77        if idx >= self.last_step.len() {
78            self.last_step.resize(idx + 1, None);
79        }
80        if let Some(last) = self.last_step[idx]
81            && step <= last
82        {
83            return Err(LogError::StepNotMonotonic {
84                key: key.to_owned(),
85                step: step.as_u64(),
86                last: last.as_u64(),
87            });
88        }
89        self.last_step[idx] = Some(step);
90
91        let now = SystemTime::now()
92            .duration_since(UNIX_EPOCH)
93            .unwrap_or_default()
94            .as_nanos() as u64;
95
96        self.accumulator.push(RawPoint {
97            key: metric_key,
98            value,
99            step,
100            timestamp_ns: now,
101        });
102
103        self.points_logged += 1;
104        Ok(())
105    }
106
107    pub fn points_logged(&self) -> u64 {
108        self.points_logged
109    }
110
111    pub fn points_dropped(&self) -> u64 {
112        self.accumulator.points_dropped()
113    }
114
115    pub fn id(&self) -> RunId {
116        self.id
117    }
118
119    /// Flushes remaining points and waits for the pipeline to drain.
120    pub fn finish(mut self) -> Result<RunStats, FinishError> {
121        let points_logged = self.points_logged;
122        let points_dropped = self.accumulator.points_dropped();
123
124        // Drop the accumulator to close the channel, signaling the batch thread to flush.
125        drop(std::mem::replace(&mut self.accumulator, {
126            let (acc, _rx) = Accumulator::new(1);
127            acc
128        }));
129
130        let batch_stats = self
131            .batch_handle
132            .join()
133            .map_err(|_| FinishError::Panicked)?
134            .map_err(FinishError::Batch)?;
135
136        let (batches_sent, batches_acked, batches_rejected) = match self.uplink_handle {
137            Some(mut ctx) => {
138                drop(ctx.shutdown_tx.take());
139
140                let uplink_stats = ctx
141                    .handle
142                    .join()
143                    .map_err(|_| FinishError::Panicked)?
144                    .map_err(FinishError::Uplink)?;
145
146                (
147                    uplink_stats.batches_sent,
148                    uplink_stats.batches_acked,
149                    uplink_stats.rejections_received,
150                )
151            }
152            None => (0, 0, 0),
153        };
154
155        if batches_sent == 0 || batches_acked >= batches_sent {
156            let _ = self.wal.close();
157        }
158
159        Ok(RunStats {
160            points: points_logged,
161            points_dropped,
162            batches: batch_stats.batches_created,
163            bytes_compressed: batch_stats.bytes_compressed,
164            bytes_uncompressed: batch_stats.bytes_uncompressed,
165            batches_sent,
166            batches_acked,
167            batches_rejected,
168        })
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use crate::Run;
175    use crate::error::LogError;
176
177    fn new_run() -> Run {
178        Run::builder().start().expect("start should succeed")
179    }
180
181    #[test]
182    fn test_monotonic_steps_accepted() {
183        let mut run = new_run();
184        run.log("train/loss", 0.5, 1).unwrap();
185        run.log("train/loss", 0.4, 2).unwrap();
186        run.log("train/loss", 0.3, 3).unwrap();
187    }
188
189    #[test]
190    fn test_duplicate_step_rejected() {
191        let mut run = new_run();
192        run.log("train/loss", 0.5, 1).unwrap();
193        let err = run.log("train/loss", 0.4, 1).unwrap_err();
194        assert!(matches!(err, LogError::StepNotMonotonic { .. }));
195    }
196
197    #[test]
198    fn test_decreasing_step_rejected() {
199        let mut run = new_run();
200        run.log("train/loss", 0.5, 5).unwrap();
201        let err = run.log("train/loss", 0.4, 3).unwrap_err();
202        assert!(matches!(
203            err,
204            LogError::StepNotMonotonic {
205                step: 3,
206                last: 5,
207                ..
208            }
209        ));
210    }
211
212    #[test]
213    fn test_different_metrics_independent() {
214        let mut run = new_run();
215        run.log("train/loss", 0.5, 1).unwrap();
216        run.log("eval/loss", 0.6, 1).unwrap(); // same step, different metric — ok
217        run.log("train/loss", 0.4, 2).unwrap();
218        run.log("eval/loss", 0.5, 2).unwrap();
219    }
220
221    #[test]
222    fn test_large_step_gap_accepted() {
223        let mut run = new_run();
224        run.log("train/loss", 0.5, 1).unwrap();
225        run.log("train/loss", 0.4, 1000).unwrap(); // big jump is fine
226    }
227}