use crate::identity::ExecutionId;
use crate::bus::error::{BusError, Result};
use crate::bus::session::{
apply_phoxal_transport_policy, client_config, execution_from_zid, zenoh_id_for,
};
#[derive(Debug)]
pub(crate) struct Router {
session: zenoh::Session,
}
impl Router {
pub(crate) async fn open(execution: ExecutionId, listen_endpoints: &[String]) -> Result<Self> {
validate_listen_endpoints(listen_endpoints)?;
let session = zenoh::open(router_config(execution, listen_endpoints)?)
.await
.map_err(|error| BusError::Transport(error.to_string()))?;
let observed = match execution_from_zid(session.zid()) {
Ok(observed) => observed,
Err(error) => {
let _ = session.close().await;
return Err(error);
}
};
if observed != execution {
let _ = session.close().await;
return Err(BusError::ExecutionIdentityMismatch {
expected: execution,
observed,
});
}
Ok(Router { session })
}
pub(crate) async fn close(self) -> Result<()> {
self.session
.close()
.await
.map_err(|error| BusError::Transport(error.to_string()))
}
}
fn validate_listen_endpoints(listen_endpoints: &[String]) -> Result<()> {
if listen_endpoints.is_empty() {
return Err(BusError::Transport(
"a router needs at least one listen endpoint".to_string(),
));
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct RouterWatch {
session: zenoh::Session,
_listener: zenoh::session::LinkEventsListener<()>,
}
impl RouterWatch {
pub(crate) async fn open(
endpoint: &str,
on_lost: impl Fn() + Send + Sync + 'static,
) -> Result<Self> {
let session = zenoh::open(client_config(endpoint)?)
.await
.map_err(|error| BusError::Transport(error.to_string()))?;
let lost = std::sync::atomic::AtomicBool::new(false);
let listener = session
.info()
.link_events_listener()
.history(true)
.callback(move |event| {
if event.kind() == zenoh::sample::SampleKind::Delete
&& !lost.swap(true, std::sync::atomic::Ordering::Relaxed)
{
on_lost();
}
})
.await
.map_err(|error| BusError::Transport(error.to_string()))?;
Ok(RouterWatch {
session,
_listener: listener,
})
}
pub(crate) async fn close(self) -> Result<()> {
self.session
.close()
.await
.map_err(|error| BusError::Transport(error.to_string()))
}
}
fn router_config(execution: ExecutionId, listen_endpoints: &[String]) -> Result<zenoh::Config> {
let mut config = zenoh::Config::default();
apply_phoxal_transport_policy(&mut config)?;
let endpoints = serde_json::to_string(listen_endpoints)
.map_err(|error| BusError::Transport(error.to_string()))?;
let id = serde_json::to_string(&zenoh_id_for(execution)?.to_string())
.map_err(|error| BusError::Transport(error.to_string()))?;
for (key, value) in [
("id", id.as_str()),
("mode", "\"router\""),
("listen/endpoints", endpoints.as_str()),
("listen/timeout_ms", "0"),
("listen/exit_on_failure", "true"),
("scouting/delay", "0"),
] {
config
.insert_json5(key, value)
.map_err(|error| BusError::Transport(error.to_string()))?;
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
const ENDPOINT: &str = "tcp/127.0.0.1:7447";
fn endpoints() -> Vec<String> {
vec![ENDPOINT.to_string()]
}
#[test]
fn router_config_pins_mode_and_listen_endpoints() {
let config = router_config(ExecutionId::mint(), &endpoints()).expect("router config");
assert_eq!(config.get_json("mode").expect("mode is set"), "\"router\"");
assert_eq!(
config
.get_json("listen/endpoints")
.expect("listen endpoints are set"),
"[\"tcp/127.0.0.1:7447\"]"
);
}
#[test]
fn the_router_session_id_is_the_execution() {
let execution = ExecutionId::mint();
let config = router_config(execution, &endpoints()).expect("router config");
assert_eq!(
config.get_json("id").expect("the session id is pinned"),
format!("\"{execution}\""),
);
}
#[test]
fn router_config_carries_the_same_transport_policy_as_a_client() {
let config = router_config(ExecutionId::mint(), &endpoints()).expect("router config");
assert_eq!(
config
.get_json("transport/link/tx/lease")
.expect("lease is set"),
"3000"
);
assert_eq!(
config
.get_json("transport/link/tx/keep_alive")
.expect("keepalive is set"),
"4"
);
assert_eq!(
config
.get_json("scouting/multicast/enabled")
.expect("multicast is set"),
"false"
);
}
#[test]
fn the_bound_on_open_guarantee_is_pinned() {
let config = router_config(ExecutionId::mint(), &endpoints()).expect("router config");
assert_eq!(
config
.get_json("listen/timeout_ms")
.expect("listen timeout is pinned"),
"0",
"a background-retry bind would make `open` succeed with nothing listening"
);
assert_eq!(
config
.get_json("listen/exit_on_failure")
.expect("listen exit_on_failure is pinned"),
"true",
"a bind failure must fail `open`, not be swallowed"
);
}
#[test]
fn opening_without_a_listen_endpoint_is_rejected_before_transport_open() {
let error =
validate_listen_endpoints(&[]).expect_err("a router with nowhere to listen must fail");
assert!(error.to_string().contains("listen endpoint"));
}
}