use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info, warn};
use super::{AgentRegistry, Liveness, RegistryError};
use crate::registry::CardSource;
pub const DEFAULT_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RefreshReport {
pub live: usize,
pub unreachable: usize,
}
impl RefreshReport {
pub fn probed(&self) -> usize {
self.live + self.unreachable
}
}
pub struct CardRefresher {
registry: Arc<dyn AgentRegistry>,
cards: Arc<dyn CardSource>,
interval: Duration,
}
impl CardRefresher {
pub fn new(registry: Arc<dyn AgentRegistry>, cards: Arc<dyn CardSource>) -> Self {
Self {
registry,
cards,
interval: DEFAULT_REFRESH_INTERVAL,
}
}
pub fn every(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
pub async fn refresh_once(&self) -> Result<RefreshReport, RegistryError> {
let mut report = RefreshReport::default();
for agent in self.registry.list().await? {
let liveness = match self.cards.fetch(&agent.endpoint).await {
Ok(_) => {
report.live += 1;
Liveness::Live
}
Err(e) => {
report.unreachable += 1;
debug!(agent = %agent.id, error = %e, "liveness probe failed");
Liveness::Unreachable
}
};
if agent.liveness != liveness {
match liveness {
Liveness::Unreachable => warn!(
agent = %agent.id,
endpoint = %agent.endpoint,
"agent stopped answering; ranked behind live peers for skill lookups"
),
_ => info!(agent = %agent.id, endpoint = %agent.endpoint, "agent is answering"),
}
}
if let Err(RegistryError::NotFound(id)) = self.registry.mark(&agent.id, liveness).await
{
debug!(agent = %id, "deregistered while its probe was in flight");
}
}
Ok(report)
}
pub async fn run(self) {
let mut ticker = tokio::time::interval(self.interval);
ticker.tick().await;
loop {
ticker.tick().await;
match self.refresh_once().await {
Ok(report) if report.probed() > 0 => {
debug!(
live = report.live,
unreachable = report.unreachable,
"liveness pass"
)
}
Ok(_) => {}
Err(e) => warn!(error = %e, "could not read the registry to probe it"),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::{AgentId, InMemoryAgentRegistry, InMemoryCardSource};
use a2a_rs::domain::{AgentCard, AgentSkill};
fn card(name: &str, skill: &str) -> AgentCard {
let mut card = AgentCard {
name: name.to_string(),
..Default::default()
};
card.skills = vec![AgentSkill::new(
skill.to_string(),
"S".to_string(),
"d".to_string(),
vec![],
)];
card
}
async fn registered(
registry: &Arc<dyn AgentRegistry>,
name: &str,
skill: &str,
endpoint: &str,
) -> AgentId {
registry
.register(card(name, skill), endpoint.to_string())
.await
.unwrap()
}
async fn liveness_of(registry: &Arc<dyn AgentRegistry>, id: &AgentId) -> Liveness {
registry
.get(id)
.await
.unwrap()
.expect("registered")
.liveness
}
#[tokio::test]
async fn a_pass_records_who_answered() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
let up = registered(®istry, "Up", "forecasting", "http://up").await;
let down = registered(®istry, "Down", "forecasting", "http://down").await;
let cards = InMemoryCardSource::new();
cards.insert("http://up", card("Up", "forecasting")).await;
let report = CardRefresher::new(registry.clone(), Arc::new(cards))
.refresh_once()
.await
.unwrap();
assert_eq!(
report,
RefreshReport {
live: 1,
unreachable: 1
}
);
assert_eq!(liveness_of(®istry, &up).await, Liveness::Live);
assert_eq!(liveness_of(®istry, &down).await, Liveness::Unreachable);
}
#[tokio::test]
async fn an_unreachable_agent_is_not_deregistered() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
registered(®istry, "Down", "forecasting", "http://down").await;
CardRefresher::new(registry.clone(), Arc::new(InMemoryCardSource::new()))
.refresh_once()
.await
.unwrap();
assert_eq!(registry.list().await.unwrap().len(), 1);
}
#[tokio::test]
async fn skill_lookups_prefer_the_peer_that_answered() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
registered(®istry, "Down", "forecasting", "http://down").await;
registered(®istry, "Up", "forecasting", "http://up").await;
let cards = InMemoryCardSource::new();
cards.insert("http://up", card("Up", "forecasting")).await;
CardRefresher::new(registry.clone(), Arc::new(cards))
.refresh_once()
.await
.unwrap();
let matches = registry.find_by_skill("forecasting").await.unwrap();
assert_eq!(matches.len(), 2, "the dead peer is ranked, not dropped");
assert_eq!(matches[0].endpoint, "http://up");
}
#[tokio::test]
async fn an_agent_that_comes_back_is_marked_live_again() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
let id = registered(®istry, "Flaky", "forecasting", "http://flaky").await;
let cards = Arc::new(InMemoryCardSource::new());
let refresher = CardRefresher::new(registry.clone(), cards.clone());
refresher.refresh_once().await.unwrap();
assert_eq!(liveness_of(®istry, &id).await, Liveness::Unreachable);
cards
.insert("http://flaky", card("Flaky", "forecasting"))
.await;
refresher.refresh_once().await.unwrap();
assert_eq!(liveness_of(®istry, &id).await, Liveness::Live);
}
#[tokio::test]
async fn re_registering_resets_liveness() {
let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
let id = registered(®istry, "Moved", "forecasting", "http://old").await;
let cards = InMemoryCardSource::new();
cards
.insert("http://old", card("Moved", "forecasting"))
.await;
CardRefresher::new(registry.clone(), Arc::new(cards))
.refresh_once()
.await
.unwrap();
assert_eq!(liveness_of(®istry, &id).await, Liveness::Live);
registered(®istry, "Moved", "forecasting", "http://new").await;
assert_eq!(liveness_of(®istry, &id).await, Liveness::Unknown);
}
}