use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
use std::time::{Duration, Instant};
use crate::output::record::Line;
pub const ANNOUNCE_AFTER: Duration = Duration::from_millis(750);
const FLUSH_EVERY: Duration = Duration::from_millis(250);
const BATCH: usize = 500;
const PREFACE: usize = 2000;
trait Report: Send + 'static {
fn announce(&self, run: &Run, preface: &[Line], dropped: u64) -> Result<(), String>;
fn lines(&self, run: &Run, lines: &[Line]) -> Result<(), String>;
fn finished(&self, run: &Run, exit_code: i32) -> Result<(), String>;
}
struct ToDaemon(crate::daemon::Client);
impl Report for ToDaemon {
fn announce(&self, run: &Run, preface: &[Line], dropped: u64) -> Result<(), String> {
let body = serde_json::json!({
"id": run.id,
"kind": run.kind,
"label": run.label,
"argv": run.argv,
"started_at": run.started_at,
"log_path": run.log_path,
"lines": preface,
"dropped": dropped,
});
let _: serde_json::Value = self.0.worktree_post("/ops/adopt", &body)?;
Ok(())
}
fn lines(&self, run: &Run, lines: &[Line]) -> Result<(), String> {
let body = serde_json::json!({ "lines": lines });
let _: serde_json::Value =
self.0.worktree_post(&format!("/ops/{}/lines", run.id), &body)?;
Ok(())
}
fn finished(&self, run: &Run, exit_code: i32) -> Result<(), String> {
let body = serde_json::json!({ "exit_code": exit_code });
let _: serde_json::Value = self
.0
.worktree_post(&format!("/ops/{}/finished", run.id), &body)?;
Ok(())
}
}
enum Message {
Line(Box<Line>),
Finished(i32, Sender<()>),
}
#[derive(Debug, Clone)]
pub struct Run {
pub id: uuid::Uuid,
pub kind: String,
pub label: String,
pub argv: Vec<String>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub log_path: String,
}
pub struct Reporter {
tx: Sender<Message>,
}
impl Reporter {
pub fn start(
client: crate::daemon::Client,
run: Run,
preface: Vec<Line>,
dropped: u64,
) -> Result<Self, String> {
Self::watching(ToDaemon(client), run, preface, dropped, ANNOUNCE_AFTER)
}
fn watching(
sink: impl Report,
run: Run,
preface: Vec<Line>,
dropped: u64,
announce_after: Duration,
) -> Result<Self, String> {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("wsb-activity".to_string())
.spawn(move || report(sink, run, preface, dropped, announce_after, rx))
.map_err(|e| format!("cannot start the activity reporter: {e}"))?;
Ok(Self { tx })
}
pub fn line(&self, line: &Line) {
let _ = self.tx.send(Message::Line(Box::new(line.clone())));
}
pub fn finished(self, exit_code: i32) {
let (done, wait) = std::sync::mpsc::channel();
if self.tx.send(Message::Finished(exit_code, done)).is_err() {
return;
}
let _ = wait.recv_timeout(Duration::from_millis(1500));
}
}
fn report(
sink: impl Report,
run: Run,
mut held: Vec<Line>,
mut dropped: u64,
announce_after: Duration,
rx: Receiver<Message>,
) {
let announce_at = Instant::now() + announce_after;
loop {
let left = announce_at.saturating_duration_since(Instant::now());
if left.is_zero() {
break;
}
match rx.recv_timeout(left) {
Ok(Message::Line(line)) => {
held.push(*line);
while held.len() > PREFACE {
held.remove(0);
dropped += 1;
}
}
Ok(Message::Finished(_, done)) => {
let _ = done.send(());
return;
}
Err(RecvTimeoutError::Timeout) => break,
Err(RecvTimeoutError::Disconnected) => return,
}
}
if sink.announce(&run, &held, dropped).is_err() {
tracing::debug!("this run is not being shown as activity");
drain(rx);
return;
}
let mut pending: Vec<Line> = Vec::new();
let mut next_flush = Instant::now() + FLUSH_EVERY;
loop {
let left = next_flush.saturating_duration_since(Instant::now());
match rx.recv_timeout(left) {
Ok(Message::Line(line)) => {
pending.push(*line);
if pending.len() >= BATCH {
if flush(&sink, &run, &mut pending).is_err() {
drain(rx);
return;
}
next_flush = Instant::now() + FLUSH_EVERY;
}
}
Ok(Message::Finished(exit_code, done)) => {
let _ = flush(&sink, &run, &mut pending);
let _ = sink.finished(&run, exit_code);
let _ = done.send(());
return;
}
Err(RecvTimeoutError::Timeout) => {
if flush(&sink, &run, &mut pending).is_err() {
drain(rx);
return;
}
next_flush = Instant::now() + FLUSH_EVERY;
}
Err(RecvTimeoutError::Disconnected) => return,
}
}
}
fn drain(rx: Receiver<Message>) {
while let Ok(message) = rx.recv() {
if let Message::Finished(_, done) = message {
let _ = done.send(());
return;
}
}
}
fn flush(sink: &impl Report, run: &Run, pending: &mut Vec<Line>) -> Result<(), String> {
if pending.is_empty() {
return Ok(());
}
for chunk in std::mem::take(pending).chunks(BATCH) {
sink.lines(run, chunk)?;
}
Ok(())
}
#[cfg(test)]
#[path = "activity_tests.rs"]
mod tests;