use std::path::PathBuf;
use std::time::Duration;
use anyhow::Result;
use crate::daemon::{self, DaemonCommand, DaemonResponse};
use crate::metering::store::TimeWindow;
use crate::output::reports::requests::{DaemonView, RequestsReport};
use crate::paths::ConfigSource;
pub const MAX_REQUEST_ROWS: u64 = 500;
const DAEMON_PROBE_TIMEOUT: Duration = Duration::from_millis(750);
pub struct RequestsAction {
source: ConfigSource,
socket: PathBuf,
}
impl RequestsAction {
pub fn new(source: ConfigSource, socket: PathBuf) -> Self {
Self { source, socket }
}
pub async fn report(&self, limit: u64) -> Result<RequestsReport> {
if !(1..=MAX_REQUEST_ROWS).contains(&limit) {
anyhow::bail!("request limit must be between 1 and {MAX_REQUEST_ROWS}");
}
let window = TimeWindow::Today;
let daemon = daemon_view(&self.socket).await;
let Some(store) = crate::metering::reader::open_readonly(&self.source).await else {
return Ok(RequestsReport::new(
daemon,
window,
Default::default(),
Default::default(),
Vec::new(),
));
};
let summary = match store.spend_summary(window).await {
Ok(summary) => summary,
Err(error) => {
tracing::debug!(%error, "request-summary read failed");
Default::default()
}
};
let rate = match store.get_total_rate().await {
Ok(rate) => rate,
Err(error) => {
tracing::debug!(%error, "request-rate read failed");
Default::default()
}
};
let rows = match store.recent_requests(window, limit, None).await {
Ok(rows) => rows,
Err(error) => {
tracing::debug!(%error, "recent-request read failed");
Vec::new()
}
};
Ok(RequestsReport::new(daemon, window, summary, rate, rows))
}
}
async fn daemon_view(socket: &std::path::Path) -> Option<DaemonView> {
let probe = daemon::send_command(socket, &DaemonCommand::Status);
let Ok(response) = tokio::time::timeout(DAEMON_PROBE_TIMEOUT, probe).await else {
return None;
};
match response {
Ok(DaemonResponse::Status {
pid,
listen,
models,
..
}) => Some(DaemonView {
pid,
listen,
models,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn empty_store_still_returns_an_empty_snapshot() -> anyhow::Result<()> {
let directory = tempfile::tempdir()?;
let action = RequestsAction::new(
ConfigSource::Default {
home: directory.path().to_path_buf(),
},
directory.path().join("missing.sock"),
);
let report = action.report(10).await?;
assert!(report.rows.is_empty());
assert!(report.daemon.is_none());
Ok(())
}
#[tokio::test]
async fn row_limit_is_bounded() -> anyhow::Result<()> {
let directory = tempfile::tempdir()?;
let action = RequestsAction::new(
ConfigSource::Default {
home: directory.path().to_path_buf(),
},
directory.path().join("missing.sock"),
);
assert!(action.report(0).await.is_err());
assert!(action.report(MAX_REQUEST_ROWS + 1).await.is_err());
Ok(())
}
}