1use std::collections::BTreeMap;
4use std::time::Duration;
5
6use anyhow::Result;
7use zenkey::grammar::with_base;
8use zenoh::Session;
9
10pub async fn roster(
16 session: &Session,
17 base: &str,
18 timeout: Duration,
19) -> Result<BTreeMap<String, Vec<String>>> {
20 let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
21
22 let catalog_alive = zenkey::selector::service_alive(&zenkey::ServiceOrigin::catalog());
23 for expr in [
26 with_base(
27 base,
28 zenkey::selector::all_liveliness(zenkey::selector::Scope::fleet()),
29 ),
30 with_base(base, catalog_alive),
31 ] {
32 let Ok(replies) = session.liveliness().get(&expr).timeout(timeout).await else {
33 continue;
34 };
35 while let Ok(reply) = replies.recv_async().await {
36 let Ok(sample) = reply.result() else { continue };
37 let key = sample.key_expr().as_str();
38 let Some(parsed) = zenkey::grammar::parse_full(base, key) else {
39 continue;
40 };
41 let origin = parsed.origin.chunk().to_string();
42 let producer = parsed
45 .producer
46 .as_ref()
47 .map(|p| p.chunk())
48 .unwrap_or_else(|| origin.trim_start_matches('@').to_string());
49 out.entry(origin).or_default().push(producer);
50 }
51 }
52 for producers in out.values_mut() {
53 producers.sort();
54 producers.dedup();
55 }
56 Ok(out)
57}
58
59#[derive(Debug, Clone, serde::Serialize)]
63pub struct ProducerInfo {
64 pub name: String,
65 pub alive: bool,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub app: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub registry_version: Option<String>,
72 pub subjects: usize,
73 pub procedures: usize,
74 #[serde(skip_serializing_if = "Vec::is_empty", default)]
75 pub blob_tiers: Vec<String>,
76 #[serde(skip_serializing_if = "Vec::is_empty", default)]
80 pub media: Vec<MediaStreamInfo>,
81 pub deprecated_served: usize,
84}
85
86#[derive(Debug, Clone, serde::Serialize)]
88pub struct MediaStreamInfo {
89 pub path: String,
91 pub encoding: String,
93}
94
95#[derive(Debug, Clone, serde::Serialize)]
97pub struct Freshness {
98 pub producer: String,
99 pub path: String,
100 pub ttl_s: i64,
101 #[serde(skip_serializing_if = "Option::is_none")]
104 pub age_s: Option<i64>,
105 pub stale: bool,
107}
108
109#[derive(Debug, Clone, serde::Serialize)]
111pub struct NodeInfo {
112 pub origin: String,
113 pub producers: Vec<ProducerInfo>,
114 #[serde(skip_serializing_if = "Vec::is_empty", default)]
115 pub freshness: Vec<Freshness>,
116}
117
118enum Node {
124 Host(zenkey::origin::RemoteOrigin),
125 Service(zenkey::ServiceOrigin),
126}
127
128impl Node {
129 fn parse(origin: &str) -> Result<Node> {
130 if origin.starts_with('@') {
131 zenkey::ServiceOrigin::new(origin)
132 .map(Node::Service)
133 .map_err(|e| anyhow::anyhow!("{e}"))
134 } else {
135 zenkey::origin::RemoteOrigin::parse(origin)
136 .map(Node::Host)
137 .map_err(|e| anyhow::anyhow!("{e} — a hostname is not an origin (RFC 06 §6)"))
138 }
139 }
140
141 fn alive_selector(&self) -> String {
143 match self {
144 Node::Host(o) => {
145 zenkey::selector::all_liveliness(zenkey::selector::Scope::origin(o)).to_string()
146 }
147 Node::Service(o) => zenkey::selector::service_alive(o).to_string(),
148 }
149 }
150
151 fn introspect_selector(&self) -> String {
153 match self {
154 Node::Host(o) => {
155 zenkey::selector::rpc(zenkey::selector::Scope::origin(o), "*", &["introspect"])
156 .to_string()
157 }
158 Node::Service(o) => zenkey::selector::service_rpc(o, &["introspect"]).to_string(),
159 }
160 }
161
162 fn state_selector(&self) -> String {
165 let scope = match self {
166 Node::Host(o) => zenkey::selector::Scope::origin(o),
167 Node::Service(o) => zenkey::selector::Scope::origin(o),
168 };
169 zenkey::selector::all_state(scope).to_string()
170 }
171}
172
173pub async fn node_info(
187 session: &Session,
188 base: &str,
189 origin: &str,
190 timeout: Duration,
191 with_freshness: bool,
192) -> Result<NodeInfo> {
193 let node = Node::parse(origin)?;
194
195 let mut alive: Vec<String> = Vec::new();
198 let alive_expr = with_base(base, node.alive_selector());
199 if let Ok(replies) = session.liveliness().get(&alive_expr).timeout(timeout).await {
200 while let Ok(reply) = replies.recv_async().await {
201 let Ok(sample) = reply.result() else { continue };
202 let Some(parsed) = zenkey::grammar::parse_full(base, sample.key_expr().as_str()) else {
203 continue;
204 };
205 alive.push(
206 parsed
207 .producer
208 .as_ref()
209 .map(|p| p.chunk())
210 .unwrap_or_else(|| parsed.origin.chunk().trim_start_matches('@').to_string()),
211 );
212 }
213 }
214 alive.sort();
215 alive.dedup();
216
217 let introspect = with_base(base, node.introspect_selector());
221 let answers = crate::query::fleet_get(session, base, &introspect, None, timeout)
222 .await
223 .unwrap_or_default();
224 let served: Vec<zenkey::slice::RegistrySlice> = answers
225 .into_iter()
226 .filter(|a| a.origin == origin)
227 .filter_map(|a| {
228 let crate::query::Answer::Value(bytes) = a.answer else {
229 return None;
230 };
231 let toml = String::from_utf8_lossy(&bytes.to_bytes()).to_string();
232 match zenkey::parse_slice(&toml) {
233 Ok(slice) => Some(slice),
234 Err(e) => {
235 tracing::warn!(origin, "introspect reply did not parse, skipping: {e}");
236 None
237 }
238 }
239 })
240 .collect();
241 let mine: Vec<&zenkey::slice::RegistrySlice> = served.iter().collect();
242
243 let mut names: Vec<String> = alive.clone();
244 names.extend(mine.iter().map(|s| s.name.clone()));
245 names.sort();
246 names.dedup();
247
248 let producers: Vec<ProducerInfo> = names
249 .iter()
250 .map(|name| {
251 let slice = mine.iter().find(|s| &s.name == name);
252 ProducerInfo {
253 name: name.clone(),
254 alive: alive.iter().any(|a| a == name),
255 app: slice.map(|s| s.app.clone()),
256 registry_version: slice.map(|s| s.version.clone()),
257 subjects: slice.map(|s| s.subjects.len()).unwrap_or(0),
258 procedures: slice.map(|s| s.procedures.len()).unwrap_or(0),
259 blob_tiers: slice
260 .map(|s| s.blob.iter().map(|b| b.tier.clone()).collect())
261 .unwrap_or_default(),
262 media: slice
263 .map(|s| {
264 s.media
265 .iter()
266 .map(|m| MediaStreamInfo {
267 path: m.path.clone(),
268 encoding: m.encoding.clone(),
269 })
270 .collect()
271 })
272 .unwrap_or_default(),
273 deprecated_served: slice.map(|s| s.deprecated.len()).unwrap_or(0),
274 }
275 })
276 .collect();
277
278 let mut freshness = Vec::new();
279 if with_freshness && !mine.is_empty() {
280 let selector = with_base(base, node.state_selector());
282 let samples = crate::query::state_snapshot(session, &selector, timeout, None)
283 .await
284 .unwrap_or_default();
285 let now = std::time::SystemTime::now();
286 for slice in &mine {
287 for subject in &slice.subjects {
288 let Some(ttl) = subject.ttl_s else { continue };
289 if subject.class != "state" {
290 continue;
291 }
292 let age = samples
294 .iter()
295 .filter_map(|s| {
296 let parsed = zenkey::grammar::parse_full(base, &s.key)?;
297 let p = parsed.producer.as_ref()?.name().to_string();
298 if p != slice.name {
299 return None;
300 }
301 let tail: Vec<&str> = parsed.subject.clone();
302 let pattern = zenkey::pattern::SubjectPattern::parse(&subject.path).ok()?;
303 pattern.matches(&tail)?;
304 s.timestamp.map(|t| {
305 now.duration_since(t.get_time().to_system_time())
306 .map(|d| d.as_secs() as i64)
307 .unwrap_or(0)
308 })
309 })
310 .min();
311 freshness.push(Freshness {
312 producer: slice.name.clone(),
313 path: subject.path.clone(),
314 ttl_s: ttl,
315 age_s: age,
316 stale: match age {
317 Some(a) => a > ttl,
318 None => true,
321 },
322 });
323 }
324 }
325 }
326
327 Ok(NodeInfo {
328 origin: origin.to_string(),
329 producers,
330 freshness,
331 })
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct BridgeMatch {
338 pub host_id: zenkey::origin::HostId,
340 pub source: String,
342 pub key: String,
345}
346
347pub async fn bridge_resolve(
359 session: &zenoh::Session,
360 base: &str,
361 producer: &str,
362 label: &str,
363 timeout: std::time::Duration,
364) -> Result<(Vec<BridgeMatch>, usize)> {
365 let relative =
366 zenkey::selector::producer_state(zenkey::selector::Scope::fleet(), producer, &["health"])
367 .to_string();
368 let key = zenkey::grammar::with_base(base, relative);
369 let answers = crate::query::fleet_get(session, base, &key, None, timeout).await?;
370 let mut matches = Vec::new();
371 let seen = answers.len();
372 for a in &answers {
373 let crate::query::Answer::Value(bytes) = &a.answer else {
374 continue;
375 };
376 let Ok(doc) = serde_json::from_slice::<serde_json::Value>(&bytes.to_bytes()) else {
377 continue;
378 };
379 let (Some(host_id), Some(source)) = (
380 doc.get("host_id").and_then(|v| v.as_str()),
381 doc.get("source").and_then(|v| v.as_str()),
382 ) else {
383 continue;
384 };
385 if source == label
386 && let Ok(id) = zenkey::origin::HostId::parse(host_id)
387 {
388 matches.push(BridgeMatch {
389 host_id: id,
390 source: source.to_string(),
391 key: a.key.clone(),
392 });
393 }
394 }
395 matches.dedup_by(|a, b| a.host_id == b.host_id);
396 Ok((matches, seen))
397}