use std::time::Duration;
use crate::{Error, Result};
use zenkey::{RegistrySlice, parse_slice};
use zenoh::Session;
use zenoh::qos::Priority;
use zenoh::query::{ConsolidationMode, QueryTarget};
use crate::bus::session::Fleet;
use crate::report::ValueSource;
#[derive(Debug, Clone)]
pub enum Answer {
Value(zenoh::bytes::ZBytes),
Error { name: String, message: String },
}
#[derive(Debug, Clone)]
pub struct FleetAnswer {
pub origin: String,
pub key: String,
pub encoding: Option<String>,
pub attachment: Option<zenoh::bytes::ZBytes>,
pub answer: Answer,
}
#[derive(Debug, Clone)]
pub struct GetOpts {
timeout: Duration,
payload: Option<Vec<u8>>,
attachment: Option<Vec<u8>>,
priority: Priority,
accept_any: bool,
max_replies: usize,
elided: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
pub const DEFAULT_MAX_REPLIES: usize = 4096;
impl GetOpts {
pub fn new(timeout: Duration) -> Self {
GetOpts {
timeout,
payload: None,
attachment: None,
priority: Priority::DEFAULT,
accept_any: false,
max_replies: DEFAULT_MAX_REPLIES,
elided: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
}
}
pub fn payload(mut self, payload: Option<Vec<u8>>) -> Self {
self.payload = payload;
self
}
pub fn attachment(mut self, attachment: Option<Vec<u8>>) -> Self {
self.attachment = attachment;
self
}
pub fn priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self
}
pub fn accept_any(mut self) -> Self {
self.accept_any = true;
self
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn max_replies(mut self, max: usize) -> Self {
self.max_replies = max.max(1);
self
}
pub fn reply_bound(&self) -> usize {
self.max_replies
}
pub fn elided(&self) -> u64 {
self.elided.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn reset_elided(&self) {
self.elided.store(0, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn note_elided(&self, n: u64) {
if n > 0 {
self.elided
.fetch_add(n, std::sync::atomic::Ordering::Relaxed);
}
}
}
pub(crate) async fn disciplined_get(
session: &Session,
selector: &str,
opts: &GetOpts,
) -> Result<zenoh::handlers::FifoChannelHandler<zenoh::query::Reply>> {
let mut builder = session
.get(selector)
.target(QueryTarget::All)
.consolidation(ConsolidationMode::None)
.priority(opts.priority)
.timeout(opts.timeout);
if let Some(body) = opts.payload.clone() {
builder = builder.payload(body);
}
if let Some(att) = opts.attachment.clone() {
builder = builder.attachment(att);
}
if opts.accept_any {
builder = builder.accept_replies(zenoh::query::ReplyKeyExpr::Any);
}
builder.await.map_err(|e| Error::bus("get", "", e))
}
pub async fn fleet_get(fleet: &Fleet<'_>, key: &str, opts: &GetOpts) -> Result<Vec<FleetAnswer>> {
let replies = disciplined_get(fleet.session(), key, opts)
.await
.map_err(|e| Error::bus("query", key.to_string(), e))?;
let (answers, elided) = collect_answers(fleet.base(), replies, opts.max_replies).await;
opts.note_elided(elided);
Ok(answers)
}
async fn collect_answers(
base: &str,
replies: zenoh::handlers::FifoChannelHandler<zenoh::query::Reply>,
max: usize,
) -> (Vec<FleetAnswer>, u64) {
let mut out = Vec::new();
let mut elided = 0u64;
while let Ok(reply) = replies.recv_async().await {
if out.len() >= max {
elided += 1;
continue;
}
out.push(answer_of(base, reply));
}
(out, elided)
}
fn answer_of(base: &str, reply: zenoh::query::Reply) -> FleetAnswer {
match reply.result() {
Ok(sample) => FleetAnswer {
origin: origin_of(base, sample.key_expr().as_str()),
key: sample.key_expr().as_str().to_string(),
encoding: Some(sample.encoding().to_string()),
attachment: sample.attachment().cloned(),
answer: Answer::Value(sample.payload().clone()),
},
Err(err) => {
let bytes = err.payload().to_bytes();
let (name, message) = match serde_json::from_slice::<serde_json::Value>(&bytes) {
Ok(v) => (
v.get("error")
.and_then(|e| e.as_str())
.unwrap_or("error/unparsed")
.to_string(),
v.get("message")
.and_then(|m| m.as_str())
.unwrap_or_default()
.to_string(),
),
Err(_) => (
"error/unparsed".to_string(),
String::from_utf8_lossy(&bytes).to_string(),
),
};
FleetAnswer {
origin: "?".to_string(),
key: String::new(),
encoding: None,
attachment: None,
answer: Answer::Error { name, message },
}
}
}
}
pub struct RepeatingQuery {
querier: zenoh::query::Querier<'static>,
base: String,
max_replies: usize,
elided: std::sync::atomic::AtomicU64,
}
pub async fn declare_repeating(
fleet: &Fleet<'_>,
key: &str,
timeout: Duration,
) -> Result<RepeatingQuery> {
declare(fleet, key, timeout, false).await
}
pub async fn declare_repeating_any(
fleet: &Fleet<'_>,
key: &str,
timeout: Duration,
) -> Result<RepeatingQuery> {
declare(fleet, key, timeout, true).await
}
async fn declare(
fleet: &Fleet<'_>,
key: &str,
timeout: Duration,
accept_any: bool,
) -> Result<RepeatingQuery> {
let mut builder = fleet
.session()
.declare_querier(key.to_string())
.target(QueryTarget::All)
.consolidation(ConsolidationMode::None)
.timeout(timeout);
if accept_any {
builder = builder.accept_replies(zenoh::query::ReplyKeyExpr::Any);
}
let querier = crate::bus::teardown::declared("declare querier", &key, builder).await?;
Ok(RepeatingQuery {
querier,
base: fleet.base().to_string(),
max_replies: DEFAULT_MAX_REPLIES,
elided: std::sync::atomic::AtomicU64::new(0),
})
}
impl RepeatingQuery {
pub fn key(&self) -> &str {
self.querier.key_expr().as_str()
}
pub async fn fetch(&self) -> Result<Vec<FleetAnswer>> {
self.fetch_with("", None).await
}
pub async fn fetch_with(
&self,
params: &str,
payload: Option<Vec<u8>>,
) -> Result<Vec<FleetAnswer>> {
let mut builder = self.querier.get();
if !params.is_empty() {
builder = builder.parameters(params);
}
if let Some(body) = payload {
builder = builder.payload(body);
}
let replies = builder
.await
.map_err(|e| Error::bus("query", self.key(), e))?;
let (answers, elided) = collect_answers(&self.base, replies, self.max_replies).await;
self.note_elided(elided);
Ok(answers)
}
pub fn max_replies(mut self, max: usize) -> Self {
self.max_replies = max.max(1);
self
}
pub fn reply_bound(&self) -> usize {
self.max_replies
}
pub fn elided(&self) -> u64 {
self.elided.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn reset_elided(&self) {
self.elided.store(0, std::sync::atomic::Ordering::Relaxed);
}
fn note_elided(&self, n: u64) {
if n > 0 {
self.elided
.fetch_add(n, std::sync::atomic::Ordering::Relaxed);
}
}
pub async fn fetch_timed(&self) -> Result<Vec<(FleetAnswer, Duration)>> {
let started = std::time::Instant::now();
let replies = self
.querier
.get()
.await
.map_err(|e| Error::bus("query", self.key(), e))?;
let mut out = Vec::new();
let mut elided = 0u64;
while let Ok(reply) = replies.recv_async().await {
let at = started.elapsed();
if out.len() >= self.max_replies {
elided += 1;
continue;
}
out.push((answer_of(&self.base, reply), at));
}
self.note_elided(elided);
Ok(out)
}
pub async fn undeclare(self) -> Result<()> {
self.querier
.undeclare()
.await
.map_err(|e| Error::bus("undeclare querier", "", e))
}
pub async fn matching_status(&self) -> Result<bool> {
self.querier
.matching_status()
.await
.map(|s| s.matching())
.map_err(|e| Error::bus("matching status", "", e))
}
pub async fn matching_events(&self) -> Result<crate::bus::write::MatchingEvents> {
crate::bus::write::MatchingEvents::for_querier(&self.querier).await
}
}
fn origin_of(base: &str, key: &str) -> String {
zenkey::grammar::parse_full(base, key)
.map(|k| k.origin.chunk().to_string())
.unwrap_or_else(|| "?".to_string())
}
pub async fn fleet_registry(
fleet: &Fleet<'_>,
timeout: Duration,
) -> Result<Vec<(String, RegistrySlice)>> {
Ok(fleet_registry_by_origin(fleet, timeout)
.await?
.into_iter()
.map(|served| (served.slice.name.clone(), served.slice))
.collect())
}
pub async fn fleet_registry_raw(
fleet: &Fleet<'_>,
timeout: Duration,
) -> Result<Vec<(RegistrySlice, String)>> {
Ok(fleet_registry_by_origin(fleet, timeout)
.await?
.into_iter()
.map(|served| (served.slice, served.raw))
.collect())
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ServedSlice {
pub origin: String,
pub slice: RegistrySlice,
pub raw: String,
}
pub async fn fleet_registry_by_origin(
fleet: &Fleet<'_>,
timeout: Duration,
) -> Result<Vec<ServedSlice>> {
let repeating = RepeatingRegistry::declare(fleet, timeout).await?;
let slices = repeating.fetch_by_origin().await?;
repeating.undeclare().await?;
Ok(slices)
}
pub struct RepeatingRegistry {
wildcard: RepeatingQuery,
catalog: RepeatingQuery,
}
impl RepeatingRegistry {
pub async fn declare(fleet: &Fleet<'_>, timeout: Duration) -> Result<Self> {
let wildcard = fleet.wire(zenkey::selector::rpc(
zenkey::selector::Scope::fleet(),
zenkey::selector::Producers::all(),
&["introspect"],
));
let catalog = fleet.wire(zenkey::selector::service_rpc(
&zenkey::ServiceOrigin::catalog(),
&["introspect"],
));
Ok(RepeatingRegistry {
wildcard: declare_repeating(fleet, &wildcard, timeout).await?,
catalog: declare_repeating(fleet, &catalog, timeout).await?,
})
}
pub async fn fetch(&self) -> Result<Vec<(RegistrySlice, String)>> {
Ok(self
.fetch_by_origin()
.await?
.into_iter()
.map(|served| (served.slice, served.raw))
.collect())
}
pub async fn fetch_by_origin(&self) -> Result<Vec<ServedSlice>> {
let mut slices = Vec::new();
for q in [&self.wildcard, &self.catalog] {
for answer in q.fetch().await? {
let origin = answer.origin;
let Answer::Value(bytes) = answer.answer else {
continue;
};
let served_toml = String::from_utf8_lossy(&bytes.to_bytes()).to_string();
match parse_slice(&served_toml) {
Ok(slice) => slices.push(ServedSlice {
origin,
slice,
raw: served_toml,
}),
Err(e) => tracing::warn!(
origin = %origin,
"introspect reply did not parse, skipping: {e}"
),
}
}
}
Ok(slices)
}
pub async fn undeclare(self) -> Result<()> {
crate::bus::teardown::drain_undeclare(
vec![
("wildcard introspect".to_string(), self.wildcard),
("@catalog introspect".to_string(), self.catalog),
],
RepeatingQuery::undeclare,
)
.await
}
}
#[derive(Debug, Clone)]
pub struct StateSample {
pub key: String,
pub timestamp: Option<zenoh::time::Timestamp>,
pub payload_len: usize,
}
pub async fn state_snapshot(
session: &Session,
selector: &str,
timeout: Duration,
max: Option<usize>,
) -> Result<Vec<StateSample>> {
let replies = disciplined_get(session, selector, &GetOpts::new(timeout))
.await
.map_err(|e| Error::bus("state snapshot", selector, e))?;
let mut out = Vec::new();
while let Ok(reply) = replies.recv_async().await {
if max.is_some_and(|m| out.len() >= m) {
break;
}
let Ok(sample) = reply.result() else { continue };
out.push(StateSample {
key: sample.key_expr().as_str().to_string(),
timestamp: sample.timestamp().copied(),
payload_len: sample.payload().len(),
});
}
Ok(out)
}
#[derive(Debug, Clone)]
pub struct FetchedValue {
pub key: String,
pub payload: zenoh::bytes::ZBytes,
pub encoding: String,
pub timestamp: Option<zenoh::time::Timestamp>,
pub attachment: Option<zenoh::bytes::ZBytes>,
pub source: ValueSource,
}
#[derive(Debug, Clone)]
pub enum FetchOutcome {
Value(FetchedValue),
None {
attempted: [&'static str; 3],
},
}
#[derive(Debug, Clone, Copy)]
pub struct FetchSpec {
pub get_timeout: Duration,
pub window: Duration,
}
impl Default for FetchSpec {
fn default() -> Self {
FetchSpec {
get_timeout: Duration::from_secs(2),
window: Duration::from_millis(1500),
}
}
}
pub async fn fetch_value(session: &Session, key: &str, spec: FetchSpec) -> Result<FetchOutcome> {
if let Some(v) = fetch_stored(session, key, spec.get_timeout).await? {
return Ok(FetchOutcome::Value(v));
}
let (tx, rx) = tokio::sync::oneshot::channel::<FetchedValue>();
let tx = std::sync::Mutex::new(Some(tx));
let subscriber = crate::bus::teardown::declared(
"window subscribe",
key,
session.declare_subscriber(key).callback(move |sample| {
if let Some(tx) = tx.lock().expect("fetch window lock").take() {
let _ = tx.send(FetchedValue {
key: sample.key_expr().as_str().to_string(),
payload: sample.payload().clone(),
encoding: sample.encoding().to_string(),
timestamp: sample.timestamp().copied(),
attachment: sample.attachment().cloned(),
source: ValueSource::Window,
});
}
}),
)
.await?;
let caught = tokio::time::timeout(spec.window, rx).await;
subscriber
.undeclare()
.await
.map_err(|e| Error::bus("window undeclare", key, e))?;
if let Ok(Ok(v)) = caught {
return Ok(FetchOutcome::Value(v));
}
Ok(FetchOutcome::None {
attempted: ["get", "@adv cache", "subscribe window"],
})
}
pub async fn fetch_stored(
session: &Session,
key: &str,
get_timeout: Duration,
) -> Result<Option<FetchedValue>> {
for (selector, source) in [
(key.to_string(), ValueSource::Storage),
(format!("{key}/@adv/**?_max=1"), ValueSource::Cache),
] {
if let Some(v) = get_latest(session, &selector, source, get_timeout).await? {
return Ok(Some(v));
}
}
Ok(None)
}
async fn get_latest(
session: &Session,
selector: &str,
source: ValueSource,
timeout: Duration,
) -> Result<Option<FetchedValue>> {
let replies = disciplined_get(session, selector, &GetOpts::new(timeout).accept_any())
.await
.map_err(|e| Error::bus("get", selector, e))?;
let mut candidates = Vec::new();
while let Ok(reply) = replies.recv_async().await {
let Ok(sample) = reply.result() else { continue };
candidates.push(FetchedValue {
key: sample.key_expr().as_str().to_string(),
payload: sample.payload().clone(),
encoding: sample.encoding().to_string(),
timestamp: sample.timestamp().copied(),
attachment: sample.attachment().cloned(),
source,
});
}
Ok(pick_latest(candidates))
}
fn pick_latest(candidates: impl IntoIterator<Item = FetchedValue>) -> Option<FetchedValue> {
let mut best: Option<FetchedValue> = None;
for candidate in candidates {
best = Some(match best.take() {
None => candidate,
Some(cur) => match (cur.timestamp, candidate.timestamp) {
(Some(a), Some(b)) if b > a => candidate,
(None, Some(_)) => candidate,
_ => cur,
},
});
}
best
}
#[cfg(test)]
mod tests {
use super::*;
fn stamp(secs: u64) -> zenoh::time::Timestamp {
zenoh::time::Timestamp::new(
zenoh::time::NTP64::from(Duration::from_secs(secs)),
zenoh::time::TimestampId::rand(),
)
}
fn value(key: &str, timestamp: Option<zenoh::time::Timestamp>) -> FetchedValue {
FetchedValue {
key: key.to_string(),
payload: zenoh::bytes::ZBytes::from(vec![0u8]),
encoding: "application/json".to_string(),
timestamp,
attachment: None,
source: ValueSource::Storage,
}
}
#[test]
fn the_latest_hlc_wins_whatever_order_the_replies_arrived_in() {
let pick = |order: [u64; 3]| {
pick_latest(order.map(|s| value(&format!("k/{s}"), Some(stamp(s)))))
.expect("three candidates")
.key
};
assert_eq!(pick([1, 2, 3]), "k/3");
assert_eq!(pick([3, 2, 1]), "k/3", "arrival order is not the rule");
assert_eq!(pick([2, 3, 1]), "k/3");
}
#[test]
fn a_stamped_answer_beats_an_unstamped_one_both_ways_round() {
let stamped = || value("stamped", Some(stamp(7)));
let bare = || value("bare", None);
assert_eq!(pick_latest([bare(), stamped()]).unwrap().key, "stamped");
assert_eq!(pick_latest([stamped(), bare()]).unwrap().key, "stamped");
}
#[test]
fn nothing_answered_is_nothing_picked_and_a_tie_keeps_the_first() {
assert!(
pick_latest(Vec::new()).is_none(),
"silence is not a value (RFC 05 §3.1)"
);
let ts = stamp(4);
assert_eq!(
pick_latest([value("first", Some(ts)), value("second", Some(ts))])
.unwrap()
.key,
"first",
"equal stamps keep arrival order — arbitrary, but stated"
);
assert_eq!(
pick_latest([value("first", None), value("second", None)])
.unwrap()
.key,
"first",
"two unstamped answers cannot be ordered; the first stands"
);
}
}