Watch
0
0
Fork
You've already forked hyperhive
0

the agent state stream sends a new client the latest header first

The stream was a live tail with no replay, and the agent re-sends its header
once a minute, so a card opened mid-idle read "no report yet" for up to a
minute. The controller already follows every agent's header for the inference
route; it now keeps the newest one as published, and the stream sends it
first when it is under 150 seconds old. An agent that stopped keeps no state
older than that.
This commit is contained in:
iris 2026-11-01 01:37:49 +01:00 • committed by mara
commit ff3d119c63

View file

@ -12,10 +12,15 @@
//! Agents publish a header when it changes and re-send it once a minute, and
//! the queue keeps none, so an agent has no entry for up to a minute after
//! this daemon starts.
//!
//! [`Reported`] also keeps each agent's newest header as it arrived, so the
//! state stream (`crate::agent_state_stream`) can hand a client that attaches
//! mid-idle the last header at once, rather than leaving it waiting for the
//! next re-send.
use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use std::time::{Duration, Instant};
use axum::Json;
use axum::extract::{Path, State};
@ -108,6 +113,8 @@ fn effective_view(v: Effective) -> EffectiveInference {
#[derive(Debug, Default)]
pub(crate) struct Reported {
latest: Mutex<HashMap<String, Effective>>,
/// Each agent's newest header exactly as published, with when it came.
raw: Mutex<HashMap<String, (Instant, Vec<u8>)>>,
}
impl Reported {
@ -139,12 +146,24 @@ impl Reported {
}
}
/// `agent`'s newest header, as published, when it arrived within `max_age`.
/// An older one describes an agent that may have stopped since.
pub(crate) fn header_within(&self, agent: &str, max_age: Duration) -> Option<Vec<u8>> {
let raw = self.raw.lock().unwrap_or_else(PoisonError::into_inner);
let (at, payload) = raw.get(agent)?;
(at.elapsed() <= max_age).then(|| payload.clone())
}
/// Keep one header's inference fields. A header without them, from an
/// agent that does not report them, removes what was kept.
fn record(&self, subject: &str, payload: &[u8]) {
let Some(agent) = swarm_queue_client::agent_from_state_subject(subject) else {
return;
};
self.raw
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(agent.to_owned(), (Instant::now(), payload.to_vec()));
let mut latest = self.latest.lock().unwrap_or_else(PoisonError::into_inner);
match serde_json::from_slice::<Effective>(payload) {
Ok(effective) => {
@ -436,6 +455,38 @@ mod tests {
assert_eq!(reported.get("atlas"), None);
}
#[test]
fn the_newest_header_is_kept_as_published_while_it_is_fresh() {
let reported = Reported::default();
reported.record(&iris(), OLD_HEADER);
reported.record(&iris(), NEW_HEADER);
assert_eq!(
reported.header_within("iris", Duration::from_mins(1)),
Some(NEW_HEADER.to_vec())
);
assert_eq!(
reported.header_within("atlas", Duration::from_mins(1)),
None
);
// A header with no inference fields still counts as the newest one.
reported.record(&iris(), OLD_HEADER);
assert_eq!(
reported.header_within("iris", Duration::from_mins(1)),
Some(OLD_HEADER.to_vec())
);
}
#[test]
fn a_header_older_than_the_limit_is_not_handed_out() {
let reported = Reported::default();
reported.record(&iris(), NEW_HEADER);
std::thread::sleep(Duration::from_millis(20));
assert_eq!(
reported.header_within("iris", Duration::from_millis(1)),
None
);
}
#[test]
fn a_header_without_the_inference_fields_leaves_no_entry() {
let reported = Reported::default();