1use std::sync::Arc;
20use std::time::Duration;
21
22use tracing::{debug, info, warn};
23
24use super::{AgentRegistry, Liveness, RegistryError};
25use crate::registry::CardSource;
26
27pub const DEFAULT_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct RefreshReport {
36 pub live: usize,
38 pub unreachable: usize,
40}
41
42impl RefreshReport {
43 pub fn probed(&self) -> usize {
45 self.live + self.unreachable
46 }
47}
48
49pub struct CardRefresher {
51 registry: Arc<dyn AgentRegistry>,
52 cards: Arc<dyn CardSource>,
53 interval: Duration,
54}
55
56impl CardRefresher {
57 pub fn new(registry: Arc<dyn AgentRegistry>, cards: Arc<dyn CardSource>) -> Self {
59 Self {
60 registry,
61 cards,
62 interval: DEFAULT_REFRESH_INTERVAL,
63 }
64 }
65
66 pub fn every(mut self, interval: Duration) -> Self {
68 self.interval = interval;
69 self
70 }
71
72 pub async fn refresh_once(&self) -> Result<RefreshReport, RegistryError> {
78 let mut report = RefreshReport::default();
79 for agent in self.registry.list().await? {
80 let liveness = match self.cards.fetch(&agent.endpoint).await {
81 Ok(_) => {
82 report.live += 1;
83 Liveness::Live
84 }
85 Err(e) => {
86 report.unreachable += 1;
87 debug!(agent = %agent.id, error = %e, "liveness probe failed");
91 Liveness::Unreachable
92 }
93 };
94
95 if agent.liveness != liveness {
98 match liveness {
99 Liveness::Unreachable => warn!(
100 agent = %agent.id,
101 endpoint = %agent.endpoint,
102 "agent stopped answering; ranked behind live peers for skill lookups"
103 ),
104 _ => info!(agent = %agent.id, endpoint = %agent.endpoint, "agent is answering"),
105 }
106 }
107
108 if let Err(RegistryError::NotFound(id)) = self.registry.mark(&agent.id, liveness).await
110 {
111 debug!(agent = %id, "deregistered while its probe was in flight");
112 }
113 }
114 Ok(report)
115 }
116
117 pub async fn run(self) {
123 let mut ticker = tokio::time::interval(self.interval);
124 ticker.tick().await;
132 loop {
133 ticker.tick().await;
134 match self.refresh_once().await {
135 Ok(report) if report.probed() > 0 => {
136 debug!(
137 live = report.live,
138 unreachable = report.unreachable,
139 "liveness pass"
140 )
141 }
142 Ok(_) => {}
143 Err(e) => warn!(error = %e, "could not read the registry to probe it"),
144 }
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::registry::{AgentId, InMemoryAgentRegistry, InMemoryCardSource};
153 use a2a_rs::domain::{AgentCard, AgentSkill};
154
155 fn card(name: &str, skill: &str) -> AgentCard {
156 let mut card = AgentCard {
157 name: name.to_string(),
158 ..Default::default()
159 };
160 card.skills = vec![AgentSkill::new(
161 skill.to_string(),
162 "S".to_string(),
163 "d".to_string(),
164 vec![],
165 )];
166 card
167 }
168
169 async fn registered(
170 registry: &Arc<dyn AgentRegistry>,
171 name: &str,
172 skill: &str,
173 endpoint: &str,
174 ) -> AgentId {
175 registry
176 .register(card(name, skill), endpoint.to_string())
177 .await
178 .unwrap()
179 }
180
181 async fn liveness_of(registry: &Arc<dyn AgentRegistry>, id: &AgentId) -> Liveness {
182 registry
183 .get(id)
184 .await
185 .unwrap()
186 .expect("registered")
187 .liveness
188 }
189
190 #[tokio::test]
191 async fn a_pass_records_who_answered() {
192 let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
193 let up = registered(®istry, "Up", "forecasting", "http://up").await;
194 let down = registered(®istry, "Down", "forecasting", "http://down").await;
195
196 let cards = InMemoryCardSource::new();
197 cards.insert("http://up", card("Up", "forecasting")).await;
198
199 let report = CardRefresher::new(registry.clone(), Arc::new(cards))
200 .refresh_once()
201 .await
202 .unwrap();
203
204 assert_eq!(
205 report,
206 RefreshReport {
207 live: 1,
208 unreachable: 1
209 }
210 );
211 assert_eq!(liveness_of(®istry, &up).await, Liveness::Live);
212 assert_eq!(liveness_of(®istry, &down).await, Liveness::Unreachable);
213 }
214
215 #[tokio::test]
218 async fn an_unreachable_agent_is_not_deregistered() {
219 let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
220 registered(®istry, "Down", "forecasting", "http://down").await;
221
222 CardRefresher::new(registry.clone(), Arc::new(InMemoryCardSource::new()))
223 .refresh_once()
224 .await
225 .unwrap();
226
227 assert_eq!(registry.list().await.unwrap().len(), 1);
228 }
229
230 #[tokio::test]
235 async fn skill_lookups_prefer_the_peer_that_answered() {
236 let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
237 registered(®istry, "Down", "forecasting", "http://down").await;
239 registered(®istry, "Up", "forecasting", "http://up").await;
240
241 let cards = InMemoryCardSource::new();
242 cards.insert("http://up", card("Up", "forecasting")).await;
243 CardRefresher::new(registry.clone(), Arc::new(cards))
244 .refresh_once()
245 .await
246 .unwrap();
247
248 let matches = registry.find_by_skill("forecasting").await.unwrap();
249 assert_eq!(matches.len(), 2, "the dead peer is ranked, not dropped");
250 assert_eq!(matches[0].endpoint, "http://up");
251 }
252
253 #[tokio::test]
256 async fn an_agent_that_comes_back_is_marked_live_again() {
257 let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
258 let id = registered(®istry, "Flaky", "forecasting", "http://flaky").await;
259 let cards = Arc::new(InMemoryCardSource::new());
260 let refresher = CardRefresher::new(registry.clone(), cards.clone());
261
262 refresher.refresh_once().await.unwrap();
263 assert_eq!(liveness_of(®istry, &id).await, Liveness::Unreachable);
264
265 cards
266 .insert("http://flaky", card("Flaky", "forecasting"))
267 .await;
268 refresher.refresh_once().await.unwrap();
269 assert_eq!(liveness_of(®istry, &id).await, Liveness::Live);
270 }
271
272 #[tokio::test]
275 async fn re_registering_resets_liveness() {
276 let registry: Arc<dyn AgentRegistry> = Arc::new(InMemoryAgentRegistry::new());
277 let id = registered(®istry, "Moved", "forecasting", "http://old").await;
278 let cards = InMemoryCardSource::new();
279 cards
280 .insert("http://old", card("Moved", "forecasting"))
281 .await;
282 CardRefresher::new(registry.clone(), Arc::new(cards))
283 .refresh_once()
284 .await
285 .unwrap();
286 assert_eq!(liveness_of(®istry, &id).await, Liveness::Live);
287
288 registered(®istry, "Moved", "forecasting", "http://new").await;
289 assert_eq!(liveness_of(®istry, &id).await, Liveness::Unknown);
290 }
291}