mirror of
https://github.com/ruvnet/RuView
synced 2026-07-28 18:21:42 +00:00
fix(homecore-api): bound multi-entity history responses
This commit is contained in:
@@ -105,6 +105,7 @@ pub struct HistoryQuery {
|
||||
}
|
||||
|
||||
const MAX_HISTORY_ENTITIES: usize = 32;
|
||||
const MAX_API_HISTORY_ROWS: usize = 100_000;
|
||||
|
||||
pub async fn get_history(
|
||||
headers: HeaderMap,
|
||||
@@ -173,11 +174,13 @@ async fn history_response(
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(entity_ids.len());
|
||||
let mut remaining = MAX_API_HISTORY_ROWS;
|
||||
for entity_id in entity_ids {
|
||||
let rows = recorder
|
||||
.get_state_history(&entity_id, start, end)
|
||||
.get_state_history_limited(&entity_id, start, end, remaining)
|
||||
.await
|
||||
.map_err(|error| ApiError::Internal(format!("history query failed: {error}")))?;
|
||||
remaining = remaining.saturating_sub(rows.len());
|
||||
let mut previous_state: Option<String> = None;
|
||||
let states = rows
|
||||
.into_iter()
|
||||
@@ -298,11 +301,13 @@ async fn logbook_response(
|
||||
)));
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
let mut remaining = MAX_API_HISTORY_ROWS;
|
||||
for entity_id in entity_ids {
|
||||
let rows = recorder
|
||||
.get_state_history(&entity_id, start, end)
|
||||
.get_state_history_limited(&entity_id, start, end, remaining)
|
||||
.await
|
||||
.map_err(|error| ApiError::Internal(format!("logbook query failed: {error}")))?;
|
||||
remaining = remaining.saturating_sub(rows.len());
|
||||
for row in rows {
|
||||
entries.push(serde_json::json!({
|
||||
"when": history_timestamp(row.last_updated_ts),
|
||||
|
||||
@@ -429,6 +429,24 @@ impl Recorder {
|
||||
since: DateTime<Utc>,
|
||||
until: DateTime<Utc>,
|
||||
) -> Result<Vec<StateRow>, RecorderError> {
|
||||
self.get_state_history_limited(entity_id, since, until, MAX_HISTORY_ROWS as usize)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Query state history with a caller-selected limit capped by
|
||||
/// [`MAX_HISTORY_ROWS`]. The bound is applied in SQL, before rows are
|
||||
/// materialized.
|
||||
pub async fn get_state_history_limited(
|
||||
&self,
|
||||
entity_id: &EntityId,
|
||||
since: DateTime<Utc>,
|
||||
until: DateTime<Utc>,
|
||||
requested_limit: usize,
|
||||
) -> Result<Vec<StateRow>, RecorderError> {
|
||||
let limit = requested_limit.min(MAX_HISTORY_ROWS as usize);
|
||||
if limit == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let since_ts = since.timestamp_micros() as f64 / 1_000_000.0;
|
||||
let until_ts = until.timestamp_micros() as f64 / 1_000_000.0;
|
||||
|
||||
@@ -446,7 +464,7 @@ impl Recorder {
|
||||
.bind(entity_id.as_str())
|
||||
.bind(since_ts)
|
||||
.bind(until_ts)
|
||||
.bind(MAX_HISTORY_ROWS)
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -957,6 +975,36 @@ mod tests {
|
||||
assert_eq!(rows[2].state, "22.0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn history_caller_limit_is_applied_before_materialization() {
|
||||
let recorder = open_memory().await;
|
||||
let eid = entity("sensor.bounded");
|
||||
for value in ["1", "2", "3"] {
|
||||
recorder
|
||||
.record_state(&make_state_event(
|
||||
"sensor.bounded",
|
||||
value,
|
||||
serde_json::json!({}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let since = Utc::now() - chrono::Duration::seconds(10);
|
||||
let until = Utc::now() + chrono::Duration::seconds(10);
|
||||
let rows = recorder
|
||||
.get_state_history_limited(&eid, since, until, 2)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows.len(), 2);
|
||||
assert_eq!(rows[0].state, "1");
|
||||
assert_eq!(rows[1].state, "2");
|
||||
assert!(recorder
|
||||
.get_state_history_limited(&eid, since, until, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
// ── record_event ──────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user