From 09aef56c8a63831267a170bba7e044bcdfffaabc Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 13 Nov 2024 11:47:32 +0100 Subject: [PATCH 01/30] add querier --- commons/zenoh-config/src/lib.rs | 2 + examples/examples/z_querier.rs | 127 ++++++++ zenoh/src/api/builders/mod.rs | 1 + zenoh/src/api/builders/publisher.rs | 31 +- zenoh/src/api/builders/querier.rs | 448 ++++++++++++++++++++++++++++ zenoh/src/api/builders/session.rs | 9 + zenoh/src/api/mod.rs | 1 + zenoh/src/api/querier.rs | 221 ++++++++++++++ zenoh/src/api/session.rs | 155 +++++++++- zenoh/src/lib.rs | 3 + 10 files changed, 968 insertions(+), 30 deletions(-) create mode 100644 examples/examples/z_querier.rs create mode 100644 zenoh/src/api/builders/querier.rs create mode 100644 zenoh/src/api/querier.rs diff --git a/commons/zenoh-config/src/lib.rs b/commons/zenoh-config/src/lib.rs index b3df5e3cb5..ef09f6b3e9 100644 --- a/commons/zenoh-config/src/lib.rs +++ b/commons/zenoh-config/src/lib.rs @@ -356,6 +356,8 @@ validated_struct::validator! { subscribers: Vec, /// A list of key-expressions for which all included publishers will be aggregated into. publishers: Vec, + /// A list of key-expressions for which all included queriers will be aggregated into. + queriers: Vec, }, pub transport: #[derive(Default)] TransportConf { diff --git a/examples/examples/z_querier.rs b/examples/examples/z_querier.rs new file mode 100644 index 0000000000..fe21e9801a --- /dev/null +++ b/examples/examples/z_querier.rs @@ -0,0 +1,127 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use std::time::Duration; + +use clap::Parser; +use zenoh::{key_expr::KeyExpr, query::QueryTarget, Config}; +use zenoh_examples::CommonArgs; + +#[tokio::main] +async fn main() { + // initiate logging + zenoh::init_log_from_env_or("error"); + + let (config, keyexpr, payload, target, timeout) = parse_args(); + + println!("Opening session..."); + let session = zenoh::open(config).await.unwrap(); + + println!("Declaring Querier on '{keyexpr}'..."); + let querier = session + .declare_querier(keyexpr) + .target(target) + .timeout(timeout) + .await + .unwrap(); + println!("Press CTRL-C to quit..."); + for idx in 0..u32::MAX { + tokio::time::sleep(Duration::from_secs(1)).await; + let buf = format!("[{idx:4}] {}", payload.clone().unwrap_or_default()); + println!( + "Querying '{}' with payload: '{}')...", + &querier.key_expr(), + buf + ); + let replies = querier + .get() + // // By default get receives replies from a FIFO. + // // Uncomment this line to use a ring channel instead. + // // More information on the ring channel are available in the z_pull example. + // .with(zenoh::handlers::RingChannel::default()) + // Refer to z_bytes.rs to see how to serialize different types of message + .payload(buf) + .await + .unwrap(); + while let Ok(reply) = replies.recv_async().await { + match reply.result() { + Ok(sample) => { + // Refer to z_bytes.rs to see how to deserialize different types of message + let payload = sample + .payload() + .try_to_string() + .unwrap_or_else(|e| e.to_string().into()); + println!( + ">> Received ('{}': '{}')", + sample.key_expr().as_str(), + payload, + ); + } + Err(err) => { + let payload = err + .payload() + .try_to_string() + .unwrap_or_else(|e| e.to_string().into()); + println!(">> Received (ERROR: '{}')", payload); + } + } + } + } +} + +#[derive(clap::ValueEnum, Clone, Copy, Debug)] +#[value(rename_all = "SCREAMING_SNAKE_CASE")] +enum Qt { + BestMatching, + All, + AllComplete, +} + +#[derive(Parser, Clone, Debug)] +struct Args { + #[arg(short, long, default_value = "demo/example/**")] + /// The selection of resources to query + key_expr: KeyExpr<'static>, + #[arg(short, long)] + /// An optional payload to put in the query. + payload: Option, + #[arg(short, long, default_value = "BEST_MATCHING")] + /// The target queryables of the query. + target: Qt, + #[arg(short = 'o', long, default_value = "10000")] + /// The query timeout in milliseconds. + timeout: u64, + #[command(flatten)] + common: CommonArgs, +} + +fn parse_args() -> ( + Config, + KeyExpr<'static>, + Option, + QueryTarget, + Duration, +) { + let args = Args::parse(); + ( + args.common.into(), + args.key_expr, + args.payload, + match args.target { + Qt::BestMatching => QueryTarget::BestMatching, + Qt::All => QueryTarget::All, + Qt::AllComplete => QueryTarget::AllComplete, + }, + Duration::from_millis(args.timeout), + ) +} diff --git a/zenoh/src/api/builders/mod.rs b/zenoh/src/api/builders/mod.rs index a9cfcab630..a5ac61e1f3 100644 --- a/zenoh/src/api/builders/mod.rs +++ b/zenoh/src/api/builders/mod.rs @@ -15,6 +15,7 @@ pub(crate) mod info; pub(crate) mod matching_listener; pub(crate) mod publisher; +pub(crate) mod querier; pub(crate) mod query; pub(crate) mod queryable; pub(crate) mod reply; diff --git a/zenoh/src/api/builders/publisher.rs b/zenoh/src/api/builders/publisher.rs index ac6565cd27..cb439f0556 100644 --- a/zenoh/src/api/builders/publisher.rs +++ b/zenoh/src/api/builders/publisher.rs @@ -14,9 +14,9 @@ use std::future::{IntoFuture, Ready}; use zenoh_core::{Resolvable, Result as ZResult, Wait}; +use zenoh_protocol::core::CongestionControl; #[cfg(feature = "unstable")] use zenoh_protocol::core::Reliability; -use zenoh_protocol::{core::CongestionControl, network::Mapping}; #[cfg(feature = "unstable")] use crate::api::sample::SourceInfo; @@ -375,34 +375,7 @@ impl<'a, 'b> Wait for PublisherBuilder<'a, 'b> { fn wait(self) -> ::To { let mut key_expr = self.key_expr?; if !key_expr.is_fully_optimized(&self.session.0) { - let session_id = self.session.0.id; - let expr_id = self.session.0.declare_prefix(key_expr.as_str()).wait()?; - let prefix_len = key_expr - .len() - .try_into() - .expect("How did you get a key expression with a length over 2^32!?"); - key_expr = match key_expr.0 { - crate::api::key_expr::KeyExprInner::Borrowed(key_expr) - | crate::api::key_expr::KeyExprInner::BorrowedWire { key_expr, .. } => { - KeyExpr(crate::api::key_expr::KeyExprInner::BorrowedWire { - key_expr, - expr_id, - mapping: Mapping::Sender, - prefix_len, - session_id, - }) - } - crate::api::key_expr::KeyExprInner::Owned(key_expr) - | crate::api::key_expr::KeyExprInner::Wire { key_expr, .. } => { - KeyExpr(crate::api::key_expr::KeyExprInner::Wire { - key_expr, - expr_id, - mapping: Mapping::Sender, - prefix_len, - session_id, - }) - } - } + key_expr = self.session.declare_keyexpr(key_expr).wait()?; } let id = self .session diff --git a/zenoh/src/api/builders/querier.rs b/zenoh/src/api/builders/querier.rs new file mode 100644 index 0000000000..52ccc0fb97 --- /dev/null +++ b/zenoh/src/api/builders/querier.rs @@ -0,0 +1,448 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use std::{ + future::{IntoFuture, Ready}, + sync::Arc, + time::Duration, +}; + +use zenoh_core::{Resolvable, Wait}; +use zenoh_protocol::{ + core::{CongestionControl, Parameters}, + network::request::ext::QueryTarget, +}; +use zenoh_result::ZResult; + +use super::sample::QoSBuilderTrait; +#[cfg(feature = "unstable")] +use crate::api::query::ReplyKeyExpr; +#[cfg(feature = "unstable")] +use crate::api::sample::SourceInfo; +#[cfg(feature = "unstable")] +use crate::query::ZenohParameters; +use crate::{ + api::{ + builders::sample::{EncodingBuilderTrait, SampleBuilderTrait}, + bytes::ZBytes, + encoding::Encoding, + handlers::{locked, Callback, DefaultHandler, IntoHandler}, + querier::Querier, + sample::{Locality, QoSBuilder}, + }, + bytes::OptionZBytes, + key_expr::KeyExpr, + qos::Priority, + query::{QueryConsolidation, Reply}, + Session, +}; + +/// A builder for initializing a `querier`. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// use zenoh::{query::{ConsolidationMode, QueryTarget}}; +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.declare_querier("key/expression") +/// .target(QueryTarget::All) +/// .consolidation(ConsolidationMode::None) +/// .await +/// .unwrap(); +/// let replies = querier.get() +/// .parameters("value>1") +/// .await +/// .unwrap(); +/// while let Ok(reply) = replies.recv_async().await { +/// println!("Received {:?}", reply.result()) +/// } +/// # } +/// ``` +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[derive(Debug)] +pub struct QuerierBuilder<'a, 'b> { + pub(crate) session: &'a Session, + pub(crate) key_expr: ZResult>, + pub(crate) target: QueryTarget, + pub(crate) consolidation: QueryConsolidation, + pub(crate) qos: QoSBuilder, + pub(crate) destination: Locality, + pub(crate) timeout: Duration, + #[cfg(feature = "unstable")] + pub(crate) accept_replies: ReplyKeyExpr, +} + +#[zenoh_macros::internal_trait] +impl QoSBuilderTrait for QuerierBuilder<'_, '_> { + fn congestion_control(self, congestion_control: CongestionControl) -> Self { + let qos = self.qos.congestion_control(congestion_control); + Self { qos, ..self } + } + + fn priority(self, priority: Priority) -> Self { + let qos = self.qos.priority(priority); + Self { qos, ..self } + } + + fn express(self, is_express: bool) -> Self { + let qos = self.qos.express(is_express); + Self { qos, ..self } + } +} + +impl<'a, 'b> QuerierBuilder<'a, 'b> { + /// Change the target of the querier queries. + #[inline] + pub fn target(self, target: QueryTarget) -> Self { + Self { target, ..self } + } + + /// Change the consolidation mode of the querier queries. + #[inline] + pub fn consolidation>(self, consolidation: QC) -> Self { + Self { + consolidation: consolidation.into(), + ..self + } + } + + /// Restrict the matching queryables that will receive the queries + /// to the ones that have the given [`Locality`](Locality). + #[zenoh_macros::unstable] + #[inline] + pub fn allowed_destination(self, destination: Locality) -> Self { + Self { + destination, + ..self + } + } + + /// Set queries timeout. + #[inline] + pub fn timeout(self, timeout: Duration) -> Self { + Self { timeout, ..self } + } + + /// By default, only replies whose key expressions intersect + /// with the querier key expression will be received by calls to [`Querier::get`](crate::query::Querier::get) method. + /// + /// If allowed to through `accept_replies(ReplyKeyExpr::Any)`, queryables may also reply on key + /// expressions that don't intersect with the querier's queries. + #[zenoh_macros::unstable] + pub fn accept_replies(self, accept: ReplyKeyExpr) -> Self { + Self { + accept_replies: accept, + ..self + } + } +} + +impl<'a, 'b> Resolvable for QuerierBuilder<'a, 'b> { + type To = ZResult>; +} + +impl<'a, 'b> Wait for QuerierBuilder<'a, 'b> { + fn wait(self) -> ::To { + let mut key_expr = self.key_expr?; + if !key_expr.is_fully_optimized(&self.session.0) { + key_expr = self.session.declare_keyexpr(key_expr).wait()?; + } + let id = self + .session + .0 + .declare_querier_inner(key_expr.clone(), self.destination)?; + Ok(Querier { + session: self.session.downgrade(), + id, + key_expr, + qos: self.qos.into(), + destination: self.destination, + undeclare_on_drop: true, + target: self.target, + consolidation: self.consolidation, + timeout: self.timeout, + #[cfg(feature = "unstable")] + accept_replies: self.accept_replies, + }) + } +} + +impl<'a, 'b> IntoFuture for QuerierBuilder<'a, 'b> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +/// A builder for initializing a `query` to be sent from the querier. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// use zenoh::{query::{ConsolidationMode, QueryTarget}}; +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.declare_querier("key/expression") +/// .target(QueryTarget::All) +/// .consolidation(ConsolidationMode::None) +/// .await +/// .unwrap(); +/// let replies = querier +/// .get() +/// .parameters("value>1") +/// .await +/// .unwrap(); +/// while let Ok(reply) = replies.recv_async().await { +/// println!("Received {:?}", reply.result()) +/// } +/// # } +/// ``` +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[derive(Debug)] +pub struct QuerierGetBuilder<'a, 'b, Handler> { + pub(crate) querier: &'a Querier<'a>, + pub(crate) parameters: Parameters<'b>, + pub(crate) handler: Handler, + pub(crate) value: Option<(ZBytes, Encoding)>, + pub(crate) attachment: Option, + #[cfg(feature = "unstable")] + pub(crate) source_info: SourceInfo, +} + +#[zenoh_macros::internal_trait] +impl SampleBuilderTrait for QuerierGetBuilder<'_, '_, Handler> { + #[zenoh_macros::unstable] + fn source_info(self, source_info: SourceInfo) -> Self { + Self { + source_info, + ..self + } + } + + fn attachment>(self, attachment: T) -> Self { + let attachment: OptionZBytes = attachment.into(); + Self { + attachment: attachment.into(), + ..self + } + } +} + +#[zenoh_macros::internal_trait] +impl EncodingBuilderTrait for QuerierGetBuilder<'_, '_, Handler> { + fn encoding>(self, encoding: T) -> Self { + let mut value = self.value.unwrap_or_default(); + value.1 = encoding.into(); + Self { + value: Some(value), + ..self + } + } +} + +impl<'a, 'b> QuerierGetBuilder<'a, 'b, DefaultHandler> { + /// Receive the replies for this query with a callback. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh::{query::{ConsolidationMode, QueryTarget}}; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression") + /// .target(QueryTarget::All) + /// .consolidation(ConsolidationMode::None) + /// .await + /// .unwrap(); + /// let _ = querier + /// .get() + /// .callback(|reply| {println!("Received {:?}", reply.result());}) + /// .await + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn callback(self, callback: F) -> QuerierGetBuilder<'a, 'b, Callback> + where + F: Fn(Reply) + Send + Sync + 'static, + { + self.with(Callback::new(Arc::new(callback))) + } + + /// Receive the replies for this query with a mutable callback. + /// + /// Using this guarantees that your callback will never be called concurrently. + /// If your callback is also accepted by the [`callback`](crate::query::QuerierGetBuilder::callback) method, we suggest you use it instead of `callback_mut`. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh::{query::{ConsolidationMode, QueryTarget}}; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression") + /// .target(QueryTarget::All) + /// .consolidation(ConsolidationMode::None) + /// .await + /// .unwrap(); + /// let mut n = 0; + /// let _ = querier + /// .get() + /// .callback_mut(move |reply| {n += 1;}) + /// .await + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn callback_mut(self, callback: F) -> QuerierGetBuilder<'a, 'b, Callback> + where + F: FnMut(Reply) + Send + Sync + 'static, + { + self.callback(locked(callback)) + } + + /// Receive the replies for this query with a [`Handler`](crate::handlers::IntoHandler). + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh::{query::{ConsolidationMode, QueryTarget}}; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression") + /// .target(QueryTarget::All) + /// .consolidation(ConsolidationMode::None) + /// .await + /// .unwrap(); + /// let replies = querier + /// .get() + /// .with(flume::bounded(32)) + /// .await + /// .unwrap(); + /// while let Ok(reply) = replies.recv_async().await { + /// println!("Received {:?}", reply.result()); + /// } + /// # } + /// ``` + #[inline] + pub fn with(self, handler: Handler) -> QuerierGetBuilder<'a, 'b, Handler> + where + Handler: IntoHandler, + { + let QuerierGetBuilder { + querier, + parameters, + value, + attachment, + #[cfg(feature = "unstable")] + source_info, + handler: _, + } = self; + QuerierGetBuilder { + querier, + parameters, + value, + attachment, + #[cfg(feature = "unstable")] + source_info, + handler, + } + } +} +impl<'a, 'b, Handler> QuerierGetBuilder<'a, 'b, Handler> { + /// Set the query payload. + #[inline] + pub fn payload(mut self, payload: IntoZBytes) -> Self + where + IntoZBytes: Into, + { + let mut value = self.value.unwrap_or_default(); + value.0 = payload.into(); + self.value = Some(value); + self + } + + /// Set the query selector parameters. + #[inline] + pub fn parameters

(mut self, parameters: P) -> Self + where + P: Into>, + { + self.parameters = parameters.into(); + self + } +} + +impl Resolvable for QuerierGetBuilder<'_, '_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type To = ZResult; +} + +impl Wait for QuerierGetBuilder<'_, '_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + fn wait(self) -> ::To { + let (callback, receiver) = self.handler.into_handler(); + + #[allow(unused_mut)] + let mut parameters = self.parameters.clone(); + #[cfg(feature = "unstable")] + if self.querier.accept_replies() == ReplyKeyExpr::Any { + parameters.set_reply_key_expr_any(); + } + self.querier + .session + .query( + &self.querier.key_expr, + ¶meters, + self.querier.target, + self.querier.consolidation, + self.querier.qos, + self.querier.destination, + self.querier.timeout, + self.value, + self.attachment, + #[cfg(feature = "unstable")] + self.source_info, + callback, + ) + .map(|_| receiver) + } +} + +impl IntoFuture for QuerierGetBuilder<'_, '_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} diff --git a/zenoh/src/api/builders/session.rs b/zenoh/src/api/builders/session.rs index 0ada1f4a67..a694a860f9 100644 --- a/zenoh/src/api/builders/session.rs +++ b/zenoh/src/api/builders/session.rs @@ -122,6 +122,7 @@ pub fn init(runtime: Runtime) -> InitBuilder { runtime, aggregated_subscribers: vec![], aggregated_publishers: vec![], + aggregated_queriers: vec![], } } @@ -133,6 +134,7 @@ pub struct InitBuilder { runtime: Runtime, aggregated_subscribers: Vec, aggregated_publishers: Vec, + aggregated_queriers: Vec, } #[zenoh_macros::internal] @@ -148,6 +150,12 @@ impl InitBuilder { self.aggregated_publishers = exprs; self } + + #[inline] + pub fn aggregated_queriers(mut self, exprs: Vec) -> Self { + self.aggregated_queriers = exprs; + self + } } #[zenoh_macros::internal] @@ -162,6 +170,7 @@ impl Wait for InitBuilder { self.runtime, self.aggregated_subscribers, self.aggregated_publishers, + self.aggregated_queriers, false, ) .wait()) diff --git a/zenoh/src/api/mod.rs b/zenoh/src/api/mod.rs index 30268a3eb9..95c1ea7e52 100644 --- a/zenoh/src/api/mod.rs +++ b/zenoh/src/api/mod.rs @@ -29,6 +29,7 @@ pub(crate) mod loader; #[cfg(feature = "plugins")] pub(crate) mod plugins; pub(crate) mod publisher; +pub(crate) mod querier; pub(crate) mod query; pub(crate) mod queryable; pub(crate) mod sample; diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs new file mode 100644 index 0000000000..b9a25a5d6d --- /dev/null +++ b/zenoh/src/api/querier.rs @@ -0,0 +1,221 @@ +use core::fmt; +use std::{ + future::{IntoFuture, Ready}, + time::Duration, +}; + +use tracing::error; +use zenoh_core::{Resolvable, Resolve, Wait}; +use zenoh_protocol::{ + core::{CongestionControl, Parameters}, + network::request::ext::QueryTarget, +}; +use zenoh_result::ZResult; +#[cfg(feature = "unstable")] +use { + crate::api::sample::SourceInfo, crate::query::ReplyKeyExpr, + zenoh_config::wrappers::EntityGlobalId, zenoh_protocol::core::EntityGlobalIdProto, +}; + +use super::{ + builders::querier::QuerierGetBuilder, + key_expr::KeyExpr, + query::QueryConsolidation, + sample::{Locality, QoS}, + session::{UndeclarableSealed, WeakSession}, + Id, +}; +use crate::{api::handlers::DefaultHandler, qos::Priority}; + +pub(crate) struct QuerierState { + pub(crate) id: Id, + pub(crate) remote_id: Id, + pub(crate) key_expr: KeyExpr<'static>, + pub(crate) destination: Locality, +} + +/// A querier that allows to send queries to a queryable. +/// +/// Queriers are automatically undeclared when dropped. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.declare_querier("key/expression").await.unwrap(); +/// let replies = querier.get().await.unwrap(); +/// # } +/// ``` +#[derive(Debug)] +pub struct Querier<'a> { + pub(crate) session: WeakSession, + pub(crate) id: Id, + pub(crate) key_expr: KeyExpr<'a>, + pub(crate) qos: QoS, + pub(crate) destination: Locality, + pub(crate) target: QueryTarget, + pub(crate) consolidation: QueryConsolidation, + pub(crate) timeout: Duration, + #[cfg(feature = "unstable")] + pub(crate) accept_replies: ReplyKeyExpr, + pub(crate) undeclare_on_drop: bool, +} + +impl fmt::Debug for QuerierState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Querier") + .field("id", &self.id) + .field("key_expr", &self.key_expr) + .finish() + } +} + +impl<'a> Querier<'a> { + /// Returns the [`EntityGlobalId`] of this Querier. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression") + /// .await + /// .unwrap(); + /// let querier_id = querier.id(); + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn id(&self) -> EntityGlobalId { + EntityGlobalIdProto { + zid: self.session.zid().into(), + eid: self.id, + } + .into() + } + + #[inline] + pub fn key_expr(&self) -> &KeyExpr<'a> { + &self.key_expr + } + + /// Get the `congestion_control` applied when routing the data. + #[inline] + pub fn congestion_control(&self) -> CongestionControl { + self.qos.congestion_control() + } + + /// Get the priority of the written data. + #[inline] + pub fn priority(&self) -> Priority { + self.qos.priority() + } + + /// Get type of queryables that can reply to this querier + #[zenoh_macros::unstable] + #[inline] + pub fn accept_replies(&self) -> ReplyKeyExpr { + self.accept_replies + } + + /// Send a query. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression").await.unwrap(); + /// let replies = querier.get(); + /// # } + /// ``` + #[inline] + pub fn get(&self) -> QuerierGetBuilder<'_, '_, DefaultHandler> { + QuerierGetBuilder { + querier: self, + #[cfg(feature = "unstable")] + source_info: SourceInfo::empty(), + value: None, + attachment: None, + parameters: Parameters::empty(), + handler: DefaultHandler::default(), + } + } + + /// Undeclare the [`Querier`], informing the network that it needn't optimize publications for its key expression anymore. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression").await.unwrap(); + /// querier.undeclare().await.unwrap(); + /// # } + /// ``` + pub fn undeclare(self) -> impl Resolve> + 'a { + UndeclarableSealed::undeclare_inner(self, ()) + } + + fn undeclare_impl(&mut self) -> ZResult<()> { + // set the flag first to avoid double panic if this function panic + self.undeclare_on_drop = false; + self.session.undeclare_querier_inner(self.id) + } +} + +impl<'a> UndeclarableSealed<()> for Querier<'a> { + type Undeclaration = QuerierUndeclaration<'a>; + + fn undeclare_inner(self, _: ()) -> Self::Undeclaration { + QuerierUndeclaration(self) + } +} + +/// A [`Resolvable`] returned when undeclaring a publisher. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.declare_querier("key/expression").await.unwrap(); +/// querier.undeclare().await.unwrap(); +/// # } +/// ``` +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +pub struct QuerierUndeclaration<'a>(Querier<'a>); + +impl Resolvable for QuerierUndeclaration<'_> { + type To = ZResult<()>; +} + +impl Wait for QuerierUndeclaration<'_> { + fn wait(mut self) -> ::To { + self.0.undeclare_impl() + } +} + +impl IntoFuture for QuerierUndeclaration<'_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +impl Drop for Querier<'_> { + fn drop(&mut self) { + if self.undeclare_on_drop { + if let Err(error) = self.undeclare_impl() { + error!(error); + } + } + } +} diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 4771bce622..8f2513a7d2 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -74,7 +74,7 @@ use crate::api::{ liveliness::{Liveliness, LivelinessTokenState}, publisher::Publisher, publisher::{MatchingListenerState, MatchingStatus}, - query::LivelinessQueryState, + query::{LivelinessQueryState, ReplyKeyExpr}, sample::SourceInfo, }; use crate::{ @@ -85,6 +85,7 @@ use crate::{ PublicationBuilderDelete, PublicationBuilderPut, PublisherBuilder, SessionDeleteBuilder, SessionPutBuilder, }, + querier::QuerierBuilder, query::SessionGetBuilder, queryable::QueryableBuilder, session::OpenBuilder, @@ -96,6 +97,7 @@ use crate::{ info::SessionInfo, key_expr::{KeyExpr, KeyExprInner}, publisher::{Priority, PublisherState}, + querier::QuerierState, query::{ConsolidationMode, QueryConsolidation, QueryState, QueryTarget, Reply}, queryable::{Query, QueryInner, QueryableState}, sample::{DataInfo, DataInfoIntoSample, Locality, QoS, Sample, SampleKind}, @@ -130,6 +132,7 @@ pub(crate) struct SessionState { #[cfg(feature = "unstable")] pub(crate) remote_subscribers: HashMap>, pub(crate) publishers: HashMap, + pub(crate) queriers: HashMap, #[cfg(feature = "unstable")] pub(crate) remote_tokens: HashMap>, //pub(crate) publications: Vec, @@ -145,12 +148,14 @@ pub(crate) struct SessionState { pub(crate) liveliness_queries: HashMap, pub(crate) aggregated_subscribers: Vec, pub(crate) aggregated_publishers: Vec, + pub(crate) aggregated_queriers: Vec, } impl SessionState { pub(crate) fn new( aggregated_subscribers: Vec, aggregated_publishers: Vec, + aggregated_queriers: Vec, ) -> SessionState { SessionState { primitives: None, @@ -163,6 +168,7 @@ impl SessionState { #[cfg(feature = "unstable")] remote_subscribers: HashMap::new(), publishers: HashMap::new(), + queriers: HashMap::new(), #[cfg(feature = "unstable")] remote_tokens: HashMap::new(), //publications: Vec::new(), @@ -178,6 +184,7 @@ impl SessionState { liveliness_queries: HashMap::new(), aggregated_subscribers, aggregated_publishers, + aggregated_queriers, } } } @@ -540,6 +547,7 @@ impl Session { runtime: Runtime, aggregated_subscribers: Vec, aggregated_publishers: Vec, + aggregated_queriers: Vec, owns_runtime: bool, ) -> impl Resolve { ResolveClosure::new(move || { @@ -547,6 +555,7 @@ impl Session { let state = RwLock::new(SessionState::new( aggregated_subscribers, aggregated_publishers, + aggregated_queriers, )); let session = Session(Arc::new(SessionInner { weak_counter: Mutex::new(0), @@ -839,6 +848,50 @@ impl Session { } } + /// Create a [`Querier`](crate::query::Querier) for the given key expression. + /// + /// # Arguments + /// + /// * `key_expr` - The key expression matching resources to query + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression") + /// .await + /// .unwrap(); + /// let replies = querier.get().await.unwrap(); + /// # } + /// ``` + pub fn declare_querier<'b, TryIntoKeyExpr>( + &self, + key_expr: TryIntoKeyExpr, + ) -> QuerierBuilder<'_, 'b> + where + TryIntoKeyExpr: TryInto>, + >>::Error: Into, + { + let timeout = { + let conf = &self.0.runtime.config().lock().0; + Duration::from_millis(unwrap_or_default!(conf.queries_default_timeout())) + }; + let qos: QoS = request::ext::QoSType::REQUEST.into(); + QuerierBuilder { + session: self, + key_expr: key_expr.try_into().map_err(Into::into), + qos: qos.into(), + destination: Locality::default(), + target: QueryTarget::default(), + consolidation: QueryConsolidation::default(), + timeout, + #[cfg(feature = "unstable")] + accept_replies: ReplyKeyExpr::default(), + } + } + /// Obtain a [`Liveliness`] struct tied to this Zenoh [`Session`]. /// /// # Examples @@ -1053,6 +1106,7 @@ impl Session { tracing::debug!("Config: {:?}", &config); let aggregated_subscribers = config.0.aggregation().subscribers().clone(); let aggregated_publishers = config.0.aggregation().publishers().clone(); + let aggregated_queriers = config.0.aggregation().queriers().clone(); #[allow(unused_mut)] // Required for shared-memory let mut runtime = RuntimeBuilder::new(config); #[cfg(feature = "shared-memory")] @@ -1065,6 +1119,7 @@ impl Session { runtime.clone(), aggregated_subscribers, aggregated_publishers, + aggregated_queriers, true, ) .await; @@ -1266,6 +1321,104 @@ impl SessionInner { } } + pub(crate) fn declare_querier_inner( + &self, + key_expr: KeyExpr, + destination: Locality, + ) -> ZResult { + let mut state = zwrite!(self.state); + tracing::trace!("declare_querier({:?})", key_expr); + let id = self.runtime.next_id(); + + let mut querier_state = QuerierState { + id, + remote_id: id, + key_expr: key_expr.clone().into_owned(), + destination, + }; + + let declared_querier = (destination != Locality::SessionLocal) + .then(|| { + match state + .aggregated_queriers + .iter() + .find(|s| s.includes(&key_expr)) + { + Some(join_querier) => { + if let Some(joined_querier) = state.queriers.values().find(|q| { + q.destination != Locality::SessionLocal + && join_querier.includes(&q.key_expr) + }) { + querier_state.remote_id = joined_querier.remote_id; + None + } else { + Some(join_querier.clone().into()) + } + } + None => { + if let Some(twin_querier) = state.queriers.values().find(|p| { + p.destination != Locality::SessionLocal && p.key_expr == key_expr + }) { + querier_state.remote_id = twin_querier.remote_id; + None + } else { + Some(key_expr.clone()) + } + } + } + }) + .flatten(); + + state.queriers.insert(id, querier_state); + + if let Some(res) = declared_querier { + let primitives = state.primitives()?; + drop(state); + primitives.send_interest(Interest { + id, + mode: InterestMode::CurrentFuture, + options: InterestOptions::KEYEXPRS + InterestOptions::QUERYABLES, + wire_expr: Some(res.to_wire(self).to_owned()), + ext_qos: network::ext::QoSType::DEFAULT, + ext_tstamp: None, + ext_nodeid: network::ext::NodeIdType::DEFAULT, + }); + } + Ok(id) + } + + pub(crate) fn undeclare_querier_inner(&self, pid: Id) -> ZResult<()> { + let mut state = zwrite!(self.state); + let Ok(primitives) = state.primitives() else { + return Ok(()); + }; + if let Some(querier_state) = state.queriers.remove(&pid) { + trace!("undeclare_querier({:?})", querier_state); + if querier_state.destination != Locality::SessionLocal { + // Note: there might be several queriers on the same KeyExpr. + // Before calling forget_queriers(key_expr), check if this was the last one. + if !state.queriers.values().any(|p| { + p.destination != Locality::SessionLocal + && p.remote_id == querier_state.remote_id + }) { + drop(state); + primitives.send_interest(Interest { + id: querier_state.remote_id, + mode: InterestMode::Final, + options: InterestOptions::empty(), + wire_expr: None, + ext_qos: declare::ext::QoSType::DEFAULT, + ext_tstamp: None, + ext_nodeid: declare::ext::NodeIdType::DEFAULT, + }); + } + } + Ok(()) + } else { + Err(zerror!("Unable to find querier").into()) + } + } + pub(crate) fn declare_subscriber_inner( self: &Arc, key_expr: &KeyExpr, diff --git a/zenoh/src/lib.rs b/zenoh/src/lib.rs index 7974dbad3e..e43f46770e 100644 --- a/zenoh/src/lib.rs +++ b/zenoh/src/lib.rs @@ -195,6 +195,7 @@ pub mod session { builders::{ info::{PeersZenohIdBuilder, RoutersZenohIdBuilder, ZenohIdBuilder}, publisher::{SessionDeleteBuilder, SessionPutBuilder}, + querier::QuerierBuilder, query::SessionGetBuilder, session::OpenBuilder, }, @@ -282,9 +283,11 @@ pub mod query { pub use crate::api::queryable::ReplySample; pub use crate::api::{ builders::{ + querier::QuerierGetBuilder, queryable::QueryableBuilder, reply::{ReplyBuilder, ReplyBuilderDelete, ReplyBuilderPut, ReplyErrBuilder}, }, + querier::Querier, query::{ConsolidationMode, QueryConsolidation, QueryTarget, Reply, ReplyError}, queryable::{Query, Queryable, QueryableUndeclaration}, selector::Selector, From 9a8e89489e2631a31c551e1f4dfdfc6d101720d1 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 13 Nov 2024 21:43:02 +0100 Subject: [PATCH 02/30] add LivelinessQuerier --- examples/examples/z_querier_liveliness.rs | 75 ++++ zenoh/src/api/liveliness.rs | 408 ++++++++++++++++++++++ zenoh/src/api/querier.rs | 2 +- zenoh/src/api/session.rs | 75 ++++ zenoh/src/lib.rs | 3 +- 5 files changed, 561 insertions(+), 2 deletions(-) create mode 100644 examples/examples/z_querier_liveliness.rs diff --git a/examples/examples/z_querier_liveliness.rs b/examples/examples/z_querier_liveliness.rs new file mode 100644 index 0000000000..c892b052b1 --- /dev/null +++ b/examples/examples/z_querier_liveliness.rs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use std::time::Duration; + +use clap::Parser; +use tokio::time::sleep; +use zenoh::{key_expr::KeyExpr, Config}; +use zenoh_examples::CommonArgs; + +#[tokio::main] +async fn main() { + // initiate logging + zenoh::init_log_from_env_or("error"); + + let (config, key_expr, timeout) = parse_args(); + + println!("Opening session..."); + let session = zenoh::open(config).await.unwrap(); + + println!("Declaring Liveliness Querier on '{}'...", &key_expr); + let querier = session + .liveliness() + .declare_querier(key_expr) + .timeout(timeout) + .await + .unwrap(); + + println!("Press CTRL-C to quit..."); + loop { + println!("Sending Liveliness Query '{}'...", querier.key_expr()); + let replies = querier.get().await.unwrap(); + while let Ok(reply) = replies.recv_async().await { + match reply.result() { + Ok(sample) => println!(">> Alive token ('{}')", sample.key_expr().as_str(),), + Err(err) => { + let payload = err + .payload() + .try_to_string() + .unwrap_or_else(|e| e.to_string().into()); + println!(">> Received (ERROR: '{}')", payload); + } + } + } + sleep(Duration::from_secs(1)).await; + } +} + +#[derive(Parser, Clone, Debug)] +struct Args { + #[arg(short, long, default_value = "group1/**")] + /// The key expression matching liveliness tokens to query. + key_expr: KeyExpr<'static>, + #[arg(short = 'o', long, default_value = "10000")] + /// The query timeout in milliseconds. + timeout: u64, + #[command(flatten)] + common: CommonArgs, +} + +fn parse_args() -> (Config, KeyExpr<'static>, Duration) { + let args = Args::parse(); + let timeout = Duration::from_millis(args.timeout); + (args.common.into(), args.key_expr, timeout) +} diff --git a/zenoh/src/api/liveliness.rs b/zenoh/src/api/liveliness.rs index 20dd30b4b0..65dc337c88 100644 --- a/zenoh/src/api/liveliness.rs +++ b/zenoh/src/api/liveliness.rs @@ -21,6 +21,8 @@ use std::{ use tracing::error; use zenoh_config::unwrap_or_default; +#[cfg(feature = "unstable")] +use zenoh_config::wrappers::EntityGlobalId; use zenoh_core::{Resolvable, Resolve, Result as ZResult, Wait}; use crate::{ @@ -174,6 +176,47 @@ impl<'a> Liveliness<'a> { } } + /// Create a [`Querier`](LivelinessQuerier) for to perform queries on liveliness tokens matching the given key expression. + /// + /// # Arguments + /// + /// * `key_expr` - The key expression to perform queries on + /// + /// # Examples + /// ```no_run + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); + /// let tokens = querier.get().await.unwrap(); + /// while let Ok(reply) = tokens.recv_async().await { + /// if let Ok(sample) = reply.result() { + /// println!(">> Liveliness token {}", sample.key_expr()); + /// } + /// } + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn declare_querier<'b, TryIntoKeyExpr>( + &self, + key_expr: TryIntoKeyExpr, + ) -> LivelinessQuerierBuilder<'a, 'b> + where + TryIntoKeyExpr: TryInto>, + >>::Error: Into, + { + let timeout = { + let conf = &self.session.0.runtime.config().lock().0; + Duration::from_millis(unwrap_or_default!(conf.queries_default_timeout())) + }; + LivelinessQuerierBuilder { + session: self.session, + key_expr: TryIntoKeyExpr::try_into(key_expr).map_err(Into::into), + timeout, + } + } + /// Query liveliness tokens with matching key expressions. /// /// # Arguments @@ -658,6 +701,371 @@ impl IntoFuture for LivelinessSubscriberBuilder<'_, '_, Callback, true> } } +/// A querier that allows to perform livleiness queries. +/// +/// Queriers are automatically undeclared when dropped. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); +/// let tokens = querier.get().await.unwrap(); +/// # } +/// ``` +#[derive(Debug)] +pub struct LivelinessQuerier<'a> { + pub(crate) session: WeakSession, + pub(crate) id: Id, + pub(crate) key_expr: KeyExpr<'a>, + pub(crate) timeout: Duration, + pub(crate) undeclare_on_drop: bool, +} + +/// A builder for initializing a liveliness querier. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session +/// .liveliness() +/// .declare_querier("key/expression") +/// .await +/// .unwrap(); +/// # } +/// ``` +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[zenoh_macros::unstable] +#[derive(Debug)] +pub struct LivelinessQuerierBuilder<'a, 'b> { + session: &'a Session, + key_expr: ZResult>, + timeout: Duration, +} + +impl<'a, 'b> LivelinessQuerierBuilder<'a, 'b> { + /// Set querier's queries timeout. + #[inline] + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } +} + +#[zenoh_macros::unstable] +impl<'a, 'b> Resolvable for LivelinessQuerierBuilder<'a, 'b> { + type To = ZResult>; +} + +#[zenoh_macros::unstable] +impl<'a> Wait for LivelinessQuerierBuilder<'a, '_> { + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + let mut key_expr = self.key_expr?; + if !key_expr.is_fully_optimized(&self.session.0) { + key_expr = self.session.declare_keyexpr(key_expr).wait()?; + } + let id = self.session.0.declare_liveliness_querier_inner(&key_expr)?; + Ok(LivelinessQuerier { + session: self.session.downgrade(), + id, + key_expr, + timeout: self.timeout, + undeclare_on_drop: true, + }) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for LivelinessQuerierBuilder<'_, '_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +impl<'a> LivelinessQuerier<'a> { + /// Returns the [`EntityGlobalId`] of this Querier. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression") + /// .await + /// .unwrap(); + /// let querier_id = querier.id(); + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn id(&self) -> EntityGlobalId { + use zenoh_protocol::core::EntityGlobalIdProto; + EntityGlobalIdProto { + zid: self.session.zid().into(), + eid: self.id, + } + .into() + } + + #[inline] + pub fn key_expr(&self) -> &KeyExpr<'a> { + &self.key_expr + } + + /// Perform a liveliness a query. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); + /// let tokens = querier.get(); + /// # } + /// ``` + #[inline] + pub fn get(&self) -> LivelinessQuerierGetBuilder<'_, DefaultHandler> { + LivelinessQuerierGetBuilder { + querier: self, + handler: DefaultHandler::default(), + } + } + + /// Undeclare the [`LivelinessQuerier`], informing the network that it needn't optimize queries for its key expression anymore. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); + /// querier.undeclare().await.unwrap(); + /// # } + /// ``` + pub fn undeclare(self) -> impl Resolve> + 'a { + UndeclarableSealed::undeclare_inner(self, ()) + } + + fn undeclare_impl(&mut self) -> ZResult<()> { + // set the flag first to avoid double panic if this function panic + self.undeclare_on_drop = false; + self.session.undeclare_liveliness_querier_inner(self.id) + } +} + +impl<'a> UndeclarableSealed<()> for LivelinessQuerier<'a> { + type Undeclaration = LivelinessQuerierUndeclaration<'a>; + + fn undeclare_inner(self, _: ()) -> Self::Undeclaration { + LivelinessQuerierUndeclaration(self) + } +} + +/// A [`Resolvable`] returned when undeclaring a publisher. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.declare_querier("key/expression").await.unwrap(); +/// querier.undeclare().await.unwrap(); +/// # } +/// ``` +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +pub struct LivelinessQuerierUndeclaration<'a>(LivelinessQuerier<'a>); + +impl Resolvable for LivelinessQuerierUndeclaration<'_> { + type To = ZResult<()>; +} + +impl Wait for LivelinessQuerierUndeclaration<'_> { + fn wait(mut self) -> ::To { + self.0.undeclare_impl() + } +} + +impl IntoFuture for LivelinessQuerierUndeclaration<'_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +impl Drop for LivelinessQuerier<'_> { + fn drop(&mut self) { + if self.undeclare_on_drop { + if let Err(error) = self.undeclare_impl() { + error!(error); + } + } + } +} + +/// A builder for initializing a liveliness querier's `query`. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// # use std::convert::TryFrom; +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); +/// let tokens = querier +/// .get() +/// .await +/// .unwrap(); +/// while let Ok(token) = tokens.recv_async().await { +/// match token.result() { +/// Ok(sample) => println!("Alive token ('{}')", sample.key_expr().as_str()), +/// Err(err) => println!("Received (ERROR: '{:?}')", err.payload()), +/// } +/// } +/// # } +/// ``` +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[derive(Debug)] +pub struct LivelinessQuerierGetBuilder<'a, Handler> { + pub(crate) querier: &'a LivelinessQuerier<'a>, + pub(crate) handler: Handler, +} + +impl<'a> LivelinessQuerierGetBuilder<'a, DefaultHandler> { + /// Receive the replies for this livliness querier's query with a callback. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); + /// let _ = querier + /// .get() + /// .callback(|reply| { println!("Received {:?}", reply.result()); }) + /// .await + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn callback(self, callback: F) -> LivelinessQuerierGetBuilder<'a, Callback> + where + F: Fn(Reply) + Send + Sync + 'static, + { + self.with(Callback::new(Arc::new(callback))) + } + + /// Receive the replies for this liveliness querier's query with a mutable callback. + /// + /// Using this guarantees that your callback will never be called concurrently. + /// If your callback is also accepted by the [`callback`](LivelinessQuerierGetBuilder::callback) method, we suggest you use it instead of `callback_mut`. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let mut n = 0; + /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); + /// let _ = querier + /// .get() + /// .callback_mut(move |reply| {n += 1;}) + /// .await + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn callback_mut(self, callback: F) -> LivelinessQuerierGetBuilder<'a, Callback> + where + F: FnMut(Reply) + Send + Sync + 'static, + { + self.callback(locked(callback)) + } + + /// Receive the replies for this liveliness querier's query with a [`Handler`](crate::handlers::IntoHandler). + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); + /// let replies = querier + /// .get() + /// .with(flume::bounded(32)) + /// .await + /// .unwrap(); + /// while let Ok(reply) = replies.recv_async().await { + /// println!("Received {:?}", reply.result()); + /// } + /// # } + /// ``` + #[inline] + pub fn with(self, handler: Handler) -> LivelinessQuerierGetBuilder<'a, Handler> + where + Handler: IntoHandler, + { + let LivelinessQuerierGetBuilder { + querier, + handler: _, + } = self; + LivelinessQuerierGetBuilder { querier, handler } + } +} + +impl Resolvable for LivelinessQuerierGetBuilder<'_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type To = ZResult; +} + +impl Wait for LivelinessQuerierGetBuilder<'_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + fn wait(self) -> ::To { + let (callback, receiver) = self.handler.into_handler(); + self.querier + .session + .liveliness_query(&self.querier.key_expr, self.querier.timeout, callback) + .map(|_| receiver) + } +} + +impl IntoFuture for LivelinessQuerierGetBuilder<'_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + /// A builder for initializing a liveliness `query`. /// /// # Examples diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index b9a25a5d6d..6f45124669 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -145,7 +145,7 @@ impl<'a> Querier<'a> { } } - /// Undeclare the [`Querier`], informing the network that it needn't optimize publications for its key expression anymore. + /// Undeclare the [`Querier`], informing the network that it needn't optimize queries for its key expression anymore. /// /// # Examples /// ``` diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 8f2513a7d2..7819eb8e9d 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -134,6 +134,8 @@ pub(crate) struct SessionState { pub(crate) publishers: HashMap, pub(crate) queriers: HashMap, #[cfg(feature = "unstable")] + pub(crate) liveliness_queriers: HashMap, + #[cfg(feature = "unstable")] pub(crate) remote_tokens: HashMap>, //pub(crate) publications: Vec, pub(crate) subscribers: HashMap>, @@ -170,6 +172,8 @@ impl SessionState { publishers: HashMap::new(), queriers: HashMap::new(), #[cfg(feature = "unstable")] + liveliness_queriers: HashMap::new(), + #[cfg(feature = "unstable")] remote_tokens: HashMap::new(), //publications: Vec::new(), subscribers: HashMap::new(), @@ -1826,6 +1830,77 @@ impl SessionInner { Ok(sub_state) } + #[cfg(feature = "unstable")] + pub(crate) fn declare_liveliness_querier_inner(&self, key_expr: &KeyExpr) -> ZResult { + let mut state = zwrite!(self.state); + trace!("declare_liveliness_querier({:?})", key_expr); + let id = self.runtime.next_id(); + + let mut querier_state = QuerierState { + id, + remote_id: id, + key_expr: key_expr.clone().into_owned(), + destination: Locality::default(), + }; + + let primitives = state.primitives()?; + let declared_querier = + if let Some(twin_querier) = state.queriers.values().find(|p| &p.key_expr == key_expr) { + querier_state.remote_id = twin_querier.remote_id; + None + } else { + Some(key_expr.clone()) + }; + state.liveliness_queriers.insert(id, querier_state); + drop(state); + + if let Some(res) = declared_querier { + primitives.send_interest(Interest { + id, + mode: InterestMode::CurrentFuture, + options: InterestOptions::KEYEXPRS + InterestOptions::TOKENS, + wire_expr: Some(res.to_wire(self).to_owned()), + ext_qos: declare::ext::QoSType::DECLARE, + ext_tstamp: None, + ext_nodeid: declare::ext::NodeIdType::DEFAULT, + }); + } + + Ok(id) + } + + #[cfg(feature = "unstable")] + pub(crate) fn undeclare_liveliness_querier_inner(&self, pid: Id) -> ZResult<()> { + let mut state = zwrite!(self.state); + let Ok(primitives) = state.primitives() else { + return Ok(()); + }; + if let Some(querier_state) = state.liveliness_queriers.remove(&pid) { + trace!("undeclare_liveliness_querier({:?})", querier_state); + // Note: there might be several queriers on the same KeyExpr. + // Before calling forget_queriers(key_expr), check if this was the last one. + if !state + .liveliness_queriers + .values() + .any(|p| p.remote_id == querier_state.remote_id) + { + drop(state); + primitives.send_interest(Interest { + id: querier_state.remote_id, + mode: InterestMode::Final, + options: InterestOptions::empty(), + wire_expr: None, + ext_qos: declare::ext::QoSType::DEFAULT, + ext_tstamp: None, + ext_nodeid: declare::ext::NodeIdType::DEFAULT, + }); + } + Ok(()) + } else { + Err(zerror!("Unable to find liveliness querier").into()) + } + } + #[zenoh_macros::unstable] pub(crate) fn undeclare_liveliness(&self, tid: Id) -> ZResult<()> { let mut state = zwrite!(self.state); diff --git a/zenoh/src/lib.rs b/zenoh/src/lib.rs index e43f46770e..f940e9cbe9 100644 --- a/zenoh/src/lib.rs +++ b/zenoh/src/lib.rs @@ -417,7 +417,8 @@ pub mod scouting { #[zenoh_macros::unstable] pub mod liveliness { pub use crate::api::liveliness::{ - Liveliness, LivelinessGetBuilder, LivelinessSubscriberBuilder, LivelinessToken, + Liveliness, LivelinessGetBuilder, LivelinessQuerier, LivelinessQuerierBuilder, + LivelinessQuerierGetBuilder, LivelinessSubscriberBuilder, LivelinessToken, LivelinessTokenBuilder, LivelinessTokenUndeclaration, }; } From 800993bf4776f8ded03f4b09cd239a12ac889b13 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 14 Nov 2024 15:04:01 +0100 Subject: [PATCH 03/30] code clean up --- zenoh/src/api/querier.rs | 7 ++ zenoh/src/api/session.rs | 211 ++++++++++++++++++--------------------- 2 files changed, 105 insertions(+), 113 deletions(-) diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 6f45124669..12bde1935c 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -219,3 +219,10 @@ impl Drop for Querier<'_> { } } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum QueriersKind { + Querier, + #[allow(dead_code)] + LivelinessQuerier, +} diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 7819eb8e9d..b6714c9120 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -67,6 +67,7 @@ use zenoh_result::ZResult; use zenoh_shm::api::client_storage::ShmClientStorage; use zenoh_task::TaskController; +use super::querier::QueriersKind; #[cfg(feature = "unstable")] use crate::api::selector::ZenohParameters; #[cfg(feature = "unstable")] @@ -133,7 +134,6 @@ pub(crate) struct SessionState { pub(crate) remote_subscribers: HashMap>, pub(crate) publishers: HashMap, pub(crate) queriers: HashMap, - #[cfg(feature = "unstable")] pub(crate) liveliness_queriers: HashMap, #[cfg(feature = "unstable")] pub(crate) remote_tokens: HashMap>, @@ -171,7 +171,6 @@ impl SessionState { remote_subscribers: HashMap::new(), publishers: HashMap::new(), queriers: HashMap::new(), - #[cfg(feature = "unstable")] liveliness_queriers: HashMap::new(), #[cfg(feature = "unstable")] remote_tokens: HashMap::new(), @@ -311,6 +310,69 @@ impl SessionState { SubscriberKind::LivelinessSubscriber => &mut self.liveliness_subscribers, } } + + pub(crate) fn queriers(&self, kind: QueriersKind) -> &HashMap { + match kind { + QueriersKind::Querier => &self.queriers, + QueriersKind::LivelinessQuerier => &self.liveliness_queriers, + } + } + + pub(crate) fn queriers_mut(&mut self, kind: QueriersKind) -> &mut HashMap { + match kind { + QueriersKind::Querier => &mut self.queriers, + QueriersKind::LivelinessQuerier => &mut self.liveliness_queriers, + } + } + + fn register_querier<'a>( + &mut self, + id: EntityId, + key_expr: &'a KeyExpr, + destination: Locality, + kind: QueriersKind, + ) -> Option> { + let mut querier_state = QuerierState { + id, + remote_id: id, + key_expr: key_expr.clone().into_owned(), + destination, + }; + let aggregated_queriers: &[OwnedKeyExpr] = match kind { + QueriersKind::Querier => self.aggregated_queriers.as_slice(), + QueriersKind::LivelinessQuerier => &[] as &[OwnedKeyExpr; 0], + }; + + let declared_querier = (destination != Locality::SessionLocal) + .then( + || match aggregated_queriers.iter().find(|s| s.includes(key_expr)) { + Some(join_querier) => { + if let Some(joined_querier) = self.queriers(kind).values().find(|q| { + q.destination != Locality::SessionLocal + && join_querier.includes(&q.key_expr) + }) { + querier_state.remote_id = joined_querier.remote_id; + None + } else { + Some(join_querier.clone().into()) + } + } + None => { + if let Some(twin_querier) = self.queriers(kind).values().find(|p| { + p.destination != Locality::SessionLocal && &p.key_expr == key_expr + }) { + querier_state.remote_id = twin_querier.remote_id; + None + } else { + Some(key_expr.clone()) + } + } + }, + ) + .flatten(); + self.queriers_mut(kind).insert(id, querier_state); + declared_querier + } } impl fmt::Debug for SessionState { @@ -1325,63 +1387,28 @@ impl SessionInner { } } - pub(crate) fn declare_querier_inner( + fn _declare_querier_inner( &self, - key_expr: KeyExpr, + key_expr: &KeyExpr, destination: Locality, + kind: QueriersKind, ) -> ZResult { let mut state = zwrite!(self.state); - tracing::trace!("declare_querier({:?})", key_expr); let id = self.runtime.next_id(); - - let mut querier_state = QuerierState { - id, - remote_id: id, - key_expr: key_expr.clone().into_owned(), - destination, - }; - - let declared_querier = (destination != Locality::SessionLocal) - .then(|| { - match state - .aggregated_queriers - .iter() - .find(|s| s.includes(&key_expr)) - { - Some(join_querier) => { - if let Some(joined_querier) = state.queriers.values().find(|q| { - q.destination != Locality::SessionLocal - && join_querier.includes(&q.key_expr) - }) { - querier_state.remote_id = joined_querier.remote_id; - None - } else { - Some(join_querier.clone().into()) - } - } - None => { - if let Some(twin_querier) = state.queriers.values().find(|p| { - p.destination != Locality::SessionLocal && p.key_expr == key_expr - }) { - querier_state.remote_id = twin_querier.remote_id; - None - } else { - Some(key_expr.clone()) - } - } - } - }) - .flatten(); - - state.queriers.insert(id, querier_state); - + let declared_querier = state.register_querier(id, key_expr, destination, kind); if let Some(res) = declared_querier { let primitives = state.primitives()?; drop(state); + let interest_options = match kind { + QueriersKind::Querier => InterestOptions::KEYEXPRS + InterestOptions::QUERYABLES, + QueriersKind::LivelinessQuerier => { + InterestOptions::KEYEXPRS + InterestOptions::TOKENS + } + }; primitives.send_interest(Interest { id, mode: InterestMode::CurrentFuture, - options: InterestOptions::KEYEXPRS + InterestOptions::QUERYABLES, + options: interest_options, wire_expr: Some(res.to_wire(self).to_owned()), ext_qos: network::ext::QoSType::DEFAULT, ext_tstamp: None, @@ -1391,17 +1418,27 @@ impl SessionInner { Ok(id) } - pub(crate) fn undeclare_querier_inner(&self, pid: Id) -> ZResult<()> { + pub(crate) fn declare_querier_inner( + &self, + key_expr: KeyExpr, + destination: Locality, + ) -> ZResult { + tracing::trace!("declare_querier({:?})", key_expr); + self._declare_querier_inner(&key_expr, destination, QueriersKind::Querier) + } + + fn _undeclare_querier_inner(&self, pid: Id, kind: QueriersKind) -> ZResult<()> { let mut state = zwrite!(self.state); let Ok(primitives) = state.primitives() else { return Ok(()); }; - if let Some(querier_state) = state.queriers.remove(&pid) { + let queriers = state.queriers_mut(kind); + if let Some(querier_state) = queriers.remove(&pid) { trace!("undeclare_querier({:?})", querier_state); if querier_state.destination != Locality::SessionLocal { // Note: there might be several queriers on the same KeyExpr. // Before calling forget_queriers(key_expr), check if this was the last one. - if !state.queriers.values().any(|p| { + if !queriers.values().any(|p| { p.destination != Locality::SessionLocal && p.remote_id == querier_state.remote_id }) { @@ -1423,6 +1460,10 @@ impl SessionInner { } } + pub(crate) fn undeclare_querier_inner(&self, pid: Id) -> ZResult<()> { + self._undeclare_querier_inner(pid, QueriersKind::Querier) + } + pub(crate) fn declare_subscriber_inner( self: &Arc, key_expr: &KeyExpr, @@ -1832,73 +1873,17 @@ impl SessionInner { #[cfg(feature = "unstable")] pub(crate) fn declare_liveliness_querier_inner(&self, key_expr: &KeyExpr) -> ZResult { - let mut state = zwrite!(self.state); trace!("declare_liveliness_querier({:?})", key_expr); - let id = self.runtime.next_id(); - - let mut querier_state = QuerierState { - id, - remote_id: id, - key_expr: key_expr.clone().into_owned(), - destination: Locality::default(), - }; - - let primitives = state.primitives()?; - let declared_querier = - if let Some(twin_querier) = state.queriers.values().find(|p| &p.key_expr == key_expr) { - querier_state.remote_id = twin_querier.remote_id; - None - } else { - Some(key_expr.clone()) - }; - state.liveliness_queriers.insert(id, querier_state); - drop(state); - - if let Some(res) = declared_querier { - primitives.send_interest(Interest { - id, - mode: InterestMode::CurrentFuture, - options: InterestOptions::KEYEXPRS + InterestOptions::TOKENS, - wire_expr: Some(res.to_wire(self).to_owned()), - ext_qos: declare::ext::QoSType::DECLARE, - ext_tstamp: None, - ext_nodeid: declare::ext::NodeIdType::DEFAULT, - }); - } - - Ok(id) + self._declare_querier_inner( + key_expr, + Locality::default(), + QueriersKind::LivelinessQuerier, + ) } #[cfg(feature = "unstable")] pub(crate) fn undeclare_liveliness_querier_inner(&self, pid: Id) -> ZResult<()> { - let mut state = zwrite!(self.state); - let Ok(primitives) = state.primitives() else { - return Ok(()); - }; - if let Some(querier_state) = state.liveliness_queriers.remove(&pid) { - trace!("undeclare_liveliness_querier({:?})", querier_state); - // Note: there might be several queriers on the same KeyExpr. - // Before calling forget_queriers(key_expr), check if this was the last one. - if !state - .liveliness_queriers - .values() - .any(|p| p.remote_id == querier_state.remote_id) - { - drop(state); - primitives.send_interest(Interest { - id: querier_state.remote_id, - mode: InterestMode::Final, - options: InterestOptions::empty(), - wire_expr: None, - ext_qos: declare::ext::QoSType::DEFAULT, - ext_tstamp: None, - ext_nodeid: declare::ext::NodeIdType::DEFAULT, - }); - } - Ok(()) - } else { - Err(zerror!("Unable to find liveliness querier").into()) - } + self._undeclare_querier_inner(pid, QueriersKind::LivelinessQuerier) } #[zenoh_macros::unstable] From 07795fa1e52d3c0a19232c42ff9d3c6d4e9ca5fa Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 18 Nov 2024 19:48:18 +0100 Subject: [PATCH 04/30] interest support --- zenoh/src/api/key_expr.rs | 16 ++++++ zenoh/src/net/routing/hat/client/pubsub.rs | 32 +++-------- zenoh/src/net/routing/hat/client/queries.rs | 51 +++++++++++++----- zenoh/src/net/routing/hat/p2p_peer/pubsub.rs | 32 +++-------- zenoh/src/net/routing/hat/p2p_peer/queries.rs | 53 +++++++++++++------ 5 files changed, 105 insertions(+), 79 deletions(-) diff --git a/zenoh/src/api/key_expr.rs b/zenoh/src/api/key_expr.rs index 2a3c775bfc..6f4cd1ef73 100644 --- a/zenoh/src/api/key_expr.rs +++ b/zenoh/src/api/key_expr.rs @@ -88,6 +88,22 @@ impl KeyExpr<'static> { s, ))) } + + pub(crate) fn string_intersects(left: &str, right: &str) -> bool { + if let (Ok(l), Ok(r)) = (KeyExpr::try_from(left), KeyExpr::try_from(right)) { + l.intersects(&r) + } else { + false + } + } + + pub(crate) fn string_includes(left: &str, right: &str) -> bool { + if let (Ok(l), Ok(r)) = (KeyExpr::try_from(left), KeyExpr::try_from(right)) { + l.includes(&r) + } else { + false + } + } } impl<'a> KeyExpr<'a> { /// Equivalent to `::try_from(t)`. diff --git a/zenoh/src/net/routing/hat/client/pubsub.rs b/zenoh/src/net/routing/hat/client/pubsub.rs index 96a128b75d..9d8fcf77c0 100644 --- a/zenoh/src/net/routing/hat/client/pubsub.rs +++ b/zenoh/src/net/routing/hat/client/pubsub.rs @@ -339,37 +339,19 @@ impl HatPubSubTrait for HatCode { .values() .filter(|f| f.whatami != WhatAmI::Client) { - if face.local_interests.values().any(|interest| { + if !face.local_interests.values().any(|interest| { interest.finalized && interest.options.subscribers() && interest .res .as_ref() - .map(|res| { - KeyExpr::try_from(res.expr()) - .and_then(|intres| { - KeyExpr::try_from(expr.full_expr()) - .map(|putres| intres.includes(&putres)) - }) - .unwrap_or(false) - }) + .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) .unwrap_or(true) - }) { - if face_hat!(face).remote_subs.values().any(|sub| { - KeyExpr::try_from(sub.expr()) - .and_then(|subres| { - KeyExpr::try_from(expr.full_expr()) - .map(|putres| subres.intersects(&putres)) - }) - .unwrap_or(false) - }) { - let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); - route.insert( - face.id, - (face.clone(), key_expr.to_owned(), NodeId::default()), - ); - } - } else { + }) || face_hat!(face) + .remote_subs + .values() + .any(|sub| KeyExpr::string_intersects(&sub.expr(), expr.full_expr())) + { let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); route.insert( face.id, diff --git a/zenoh/src/net/routing/hat/client/queries.rs b/zenoh/src/net/routing/hat/client/queries.rs index cd417ef84b..c37ec6869f 100644 --- a/zenoh/src/net/routing/hat/client/queries.rs +++ b/zenoh/src/net/routing/hat/client/queries.rs @@ -33,15 +33,18 @@ use zenoh_protocol::{ use zenoh_sync::get_mut_unchecked; use super::{face_hat, face_hat_mut, get_routes_entries, HatCode, HatFace}; -use crate::net::routing::{ - dispatcher::{ - face::FaceState, - resource::{NodeId, Resource, SessionContext}, - tables::{QueryTargetQabl, QueryTargetQablSet, RoutingExpr, Tables}, +use crate::{ + key_expr::KeyExpr, + net::routing::{ + dispatcher::{ + face::FaceState, + resource::{NodeId, Resource, SessionContext}, + tables::{QueryTargetQabl, QueryTargetQablSet, RoutingExpr, Tables}, + }, + hat::{HatQueriesTrait, SendDeclare, Sources}, + router::{update_query_routes_from, RoutesIndexes}, + RoutingContext, }, - hat::{HatQueriesTrait, SendDeclare, Sources}, - router::RoutesIndexes, - RoutingContext, }; #[inline] @@ -272,6 +275,8 @@ pub(super) fn queries_new_face( propagate_simple_queryable(tables, qabl, Some(&mut face.clone()), send_declare); } } + // recompute routes + update_query_routes_from(tables, &mut tables.root_res.clone()); } lazy_static::lazy_static! { @@ -349,12 +354,30 @@ impl HatQueriesTrait for HatCode { }; if source_type == WhatAmI::Client { - if let Some(face) = tables.faces.values().find(|f| f.whatami != WhatAmI::Client) { - let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); - route.push(QueryTargetQabl { - direction: (face.clone(), key_expr.to_owned(), NodeId::default()), - info: None, - }); + for face in tables + .faces + .values() + .filter(|f| f.whatami != WhatAmI::Client) + { + if !face.local_interests.values().any(|interest| { + interest.finalized + && interest.options.queryables() + && interest + .res + .as_ref() + .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) + .unwrap_or(true) + }) || face_hat!(face) + .remote_qabls + .values() + .any(|qbl| KeyExpr::string_intersects(&qbl.expr(), expr.full_expr())) + { + let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); + route.push(QueryTargetQabl { + direction: (face.clone(), key_expr.to_owned(), NodeId::default()), + info: None, + }); + } } } diff --git a/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs b/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs index 776b472c87..7d33bcef43 100644 --- a/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs +++ b/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs @@ -603,37 +603,19 @@ impl HatPubSubTrait for HatCode { .values() .filter(|f| f.whatami == WhatAmI::Router) { - if face.local_interests.values().any(|interest| { + if !face.local_interests.values().any(|interest| { interest.finalized && interest.options.subscribers() && interest .res .as_ref() - .map(|res| { - KeyExpr::try_from(res.expr()) - .and_then(|intres| { - KeyExpr::try_from(expr.full_expr()) - .map(|putres| intres.includes(&putres)) - }) - .unwrap_or(false) - }) + .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) .unwrap_or(true) - }) { - if face_hat!(face).remote_subs.values().any(|sub| { - KeyExpr::try_from(sub.expr()) - .and_then(|subres| { - KeyExpr::try_from(expr.full_expr()) - .map(|putres| subres.intersects(&putres)) - }) - .unwrap_or(false) - }) { - let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); - route.insert( - face.id, - (face.clone(), key_expr.to_owned(), NodeId::default()), - ); - } - } else { + }) || face_hat!(face) + .remote_subs + .values() + .any(|sub| KeyExpr::string_intersects(&sub.expr(), expr.full_expr())) + { let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); route.insert( face.id, diff --git a/zenoh/src/net/routing/hat/p2p_peer/queries.rs b/zenoh/src/net/routing/hat/p2p_peer/queries.rs index 052d401690..8bdc1ee62b 100644 --- a/zenoh/src/net/routing/hat/p2p_peer/queries.rs +++ b/zenoh/src/net/routing/hat/p2p_peer/queries.rs @@ -36,15 +36,20 @@ use zenoh_protocol::{ use zenoh_sync::get_mut_unchecked; use super::{face_hat, face_hat_mut, get_routes_entries, HatCode, HatFace}; -use crate::net::routing::{ - dispatcher::{ - face::FaceState, - resource::{NodeId, Resource, SessionContext}, - tables::{QueryTargetQabl, QueryTargetQablSet, RoutingExpr, Tables}, +use crate::{ + key_expr::KeyExpr, + net::routing::{ + dispatcher::{ + face::FaceState, + resource::{NodeId, Resource, SessionContext}, + tables::{QueryTargetQabl, QueryTargetQablSet, RoutingExpr, Tables}, + }, + hat::{ + p2p_peer::initial_interest, CurrentFutureTrait, HatQueriesTrait, SendDeclare, Sources, + }, + router::{update_query_routes_from, RoutesIndexes}, + RoutingContext, }, - hat::{p2p_peer::initial_interest, CurrentFutureTrait, HatQueriesTrait, SendDeclare, Sources}, - router::{update_query_routes_from, RoutesIndexes}, - RoutingContext, }; #[inline] @@ -589,13 +594,31 @@ impl HatQueriesTrait for HatCode { }; if source_type == WhatAmI::Client { - // TODO: BNestMatching: What if there is a local compete ? - if let Some(face) = tables.faces.values().find(|f| f.whatami == WhatAmI::Router) { - let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); - route.push(QueryTargetQabl { - direction: (face.clone(), key_expr.to_owned(), NodeId::default()), - info: None, - }); + // TODO: BestMatching: What if there is a local compete ? + for face in tables + .faces + .values() + .filter(|f| f.whatami == WhatAmI::Router) + { + if !face.local_interests.values().any(|interest| { + interest.finalized + && interest.options.queryables() + && interest + .res + .as_ref() + .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) + .unwrap_or(true) + }) || face_hat!(face) + .remote_qabls + .values() + .any(|sub| KeyExpr::string_intersects(&sub.expr(), expr.full_expr())) + { + let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); + route.push(QueryTargetQabl { + direction: (face.clone(), key_expr.to_owned(), NodeId::default()), + info: None, + }); + } } for face in tables.faces.values().filter(|f| { From f21964081e8edd9f566caef1e65541d893ba0c2b Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 19 Nov 2024 10:38:51 +0100 Subject: [PATCH 05/30] make keyexpr include/intersect checking functions generic --- zenoh/src/api/key_expr.rs | 40 +++++++++++-------- zenoh/src/net/routing/hat/client/pubsub.rs | 4 +- zenoh/src/net/routing/hat/client/queries.rs | 4 +- zenoh/src/net/routing/hat/p2p_peer/pubsub.rs | 4 +- zenoh/src/net/routing/hat/p2p_peer/queries.rs | 4 +- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/zenoh/src/api/key_expr.rs b/zenoh/src/api/key_expr.rs index 6f4cd1ef73..64703d77fa 100644 --- a/zenoh/src/api/key_expr.rs +++ b/zenoh/src/api/key_expr.rs @@ -88,22 +88,6 @@ impl KeyExpr<'static> { s, ))) } - - pub(crate) fn string_intersects(left: &str, right: &str) -> bool { - if let (Ok(l), Ok(r)) = (KeyExpr::try_from(left), KeyExpr::try_from(right)) { - l.intersects(&r) - } else { - false - } - } - - pub(crate) fn string_includes(left: &str, right: &str) -> bool { - if let (Ok(l), Ok(r)) = (KeyExpr::try_from(left), KeyExpr::try_from(right)) { - l.includes(&r) - } else { - false - } - } } impl<'a> KeyExpr<'a> { /// Equivalent to `::try_from(t)`. @@ -288,6 +272,30 @@ impl<'a> KeyExpr<'a> { Ok(r.into()) } } + + pub(crate) fn keyexpr_intersect<'b, L, R>(left: L, right: R) -> bool + where + L: TryInto>, + R: TryInto>, + { + if let (Ok(l), Ok(r)) = (left.try_into(), right.try_into()) { + l.intersects(&r) + } else { + false + } + } + + pub(crate) fn keyexpr_include<'b, L, R>(left: L, right: R) -> bool + where + L: TryInto>, + R: TryInto>, + { + if let (Ok(l), Ok(r)) = (left.try_into(), right.try_into()) { + l.includes(&r) + } else { + false + } + } } impl FromStr for KeyExpr<'static> { diff --git a/zenoh/src/net/routing/hat/client/pubsub.rs b/zenoh/src/net/routing/hat/client/pubsub.rs index 9d8fcf77c0..11fca5ff12 100644 --- a/zenoh/src/net/routing/hat/client/pubsub.rs +++ b/zenoh/src/net/routing/hat/client/pubsub.rs @@ -345,12 +345,12 @@ impl HatPubSubTrait for HatCode { && interest .res .as_ref() - .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) + .map(|res| KeyExpr::keyexpr_include(res.expr(), expr.full_expr())) .unwrap_or(true) }) || face_hat!(face) .remote_subs .values() - .any(|sub| KeyExpr::string_intersects(&sub.expr(), expr.full_expr())) + .any(|sub| KeyExpr::keyexpr_intersect(sub.expr(), expr.full_expr())) { let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); route.insert( diff --git a/zenoh/src/net/routing/hat/client/queries.rs b/zenoh/src/net/routing/hat/client/queries.rs index c37ec6869f..66ab198232 100644 --- a/zenoh/src/net/routing/hat/client/queries.rs +++ b/zenoh/src/net/routing/hat/client/queries.rs @@ -365,12 +365,12 @@ impl HatQueriesTrait for HatCode { && interest .res .as_ref() - .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) + .map(|res| KeyExpr::keyexpr_include(res.expr(), expr.full_expr())) .unwrap_or(true) }) || face_hat!(face) .remote_qabls .values() - .any(|qbl| KeyExpr::string_intersects(&qbl.expr(), expr.full_expr())) + .any(|qbl| KeyExpr::keyexpr_intersect(qbl.expr(), expr.full_expr())) { let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); route.push(QueryTargetQabl { diff --git a/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs b/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs index 7d33bcef43..b46d22eec9 100644 --- a/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs +++ b/zenoh/src/net/routing/hat/p2p_peer/pubsub.rs @@ -609,12 +609,12 @@ impl HatPubSubTrait for HatCode { && interest .res .as_ref() - .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) + .map(|res| KeyExpr::keyexpr_include(res.expr(), expr.full_expr())) .unwrap_or(true) }) || face_hat!(face) .remote_subs .values() - .any(|sub| KeyExpr::string_intersects(&sub.expr(), expr.full_expr())) + .any(|sub| KeyExpr::keyexpr_intersect(sub.expr(), expr.full_expr())) { let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); route.insert( diff --git a/zenoh/src/net/routing/hat/p2p_peer/queries.rs b/zenoh/src/net/routing/hat/p2p_peer/queries.rs index 8bdc1ee62b..8efa87cc26 100644 --- a/zenoh/src/net/routing/hat/p2p_peer/queries.rs +++ b/zenoh/src/net/routing/hat/p2p_peer/queries.rs @@ -606,12 +606,12 @@ impl HatQueriesTrait for HatCode { && interest .res .as_ref() - .map(|res| KeyExpr::string_includes(&res.expr(), expr.full_expr())) + .map(|res| KeyExpr::keyexpr_include(res.expr(), expr.full_expr())) .unwrap_or(true) }) || face_hat!(face) .remote_qabls .values() - .any(|sub| KeyExpr::string_intersects(&sub.expr(), expr.full_expr())) + .any(|sub| KeyExpr::keyexpr_intersect(sub.expr(), expr.full_expr())) { let key_expr = Resource::get_best_key(expr.prefix, expr.suffix, face.id); route.push(QueryTargetQabl { From 3e87c79acee5510b30a2c09b8ac0dfb308bab5ec Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 19 Nov 2024 13:25:50 +0100 Subject: [PATCH 06/30] remove liveliness querier --- examples/examples/z_querier_liveliness.rs | 75 ---- zenoh/src/api/liveliness.rs | 408 ---------------------- zenoh/src/api/querier.rs | 7 - zenoh/src/api/session.rs | 91 +---- zenoh/src/lib.rs | 3 +- 5 files changed, 20 insertions(+), 564 deletions(-) delete mode 100644 examples/examples/z_querier_liveliness.rs diff --git a/examples/examples/z_querier_liveliness.rs b/examples/examples/z_querier_liveliness.rs deleted file mode 100644 index c892b052b1..0000000000 --- a/examples/examples/z_querier_liveliness.rs +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// -use std::time::Duration; - -use clap::Parser; -use tokio::time::sleep; -use zenoh::{key_expr::KeyExpr, Config}; -use zenoh_examples::CommonArgs; - -#[tokio::main] -async fn main() { - // initiate logging - zenoh::init_log_from_env_or("error"); - - let (config, key_expr, timeout) = parse_args(); - - println!("Opening session..."); - let session = zenoh::open(config).await.unwrap(); - - println!("Declaring Liveliness Querier on '{}'...", &key_expr); - let querier = session - .liveliness() - .declare_querier(key_expr) - .timeout(timeout) - .await - .unwrap(); - - println!("Press CTRL-C to quit..."); - loop { - println!("Sending Liveliness Query '{}'...", querier.key_expr()); - let replies = querier.get().await.unwrap(); - while let Ok(reply) = replies.recv_async().await { - match reply.result() { - Ok(sample) => println!(">> Alive token ('{}')", sample.key_expr().as_str(),), - Err(err) => { - let payload = err - .payload() - .try_to_string() - .unwrap_or_else(|e| e.to_string().into()); - println!(">> Received (ERROR: '{}')", payload); - } - } - } - sleep(Duration::from_secs(1)).await; - } -} - -#[derive(Parser, Clone, Debug)] -struct Args { - #[arg(short, long, default_value = "group1/**")] - /// The key expression matching liveliness tokens to query. - key_expr: KeyExpr<'static>, - #[arg(short = 'o', long, default_value = "10000")] - /// The query timeout in milliseconds. - timeout: u64, - #[command(flatten)] - common: CommonArgs, -} - -fn parse_args() -> (Config, KeyExpr<'static>, Duration) { - let args = Args::parse(); - let timeout = Duration::from_millis(args.timeout); - (args.common.into(), args.key_expr, timeout) -} diff --git a/zenoh/src/api/liveliness.rs b/zenoh/src/api/liveliness.rs index 65dc337c88..20dd30b4b0 100644 --- a/zenoh/src/api/liveliness.rs +++ b/zenoh/src/api/liveliness.rs @@ -21,8 +21,6 @@ use std::{ use tracing::error; use zenoh_config::unwrap_or_default; -#[cfg(feature = "unstable")] -use zenoh_config::wrappers::EntityGlobalId; use zenoh_core::{Resolvable, Resolve, Result as ZResult, Wait}; use crate::{ @@ -176,47 +174,6 @@ impl<'a> Liveliness<'a> { } } - /// Create a [`Querier`](LivelinessQuerier) for to perform queries on liveliness tokens matching the given key expression. - /// - /// # Arguments - /// - /// * `key_expr` - The key expression to perform queries on - /// - /// # Examples - /// ```no_run - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); - /// let tokens = querier.get().await.unwrap(); - /// while let Ok(reply) = tokens.recv_async().await { - /// if let Ok(sample) = reply.result() { - /// println!(">> Liveliness token {}", sample.key_expr()); - /// } - /// } - /// # } - /// ``` - #[zenoh_macros::unstable] - pub fn declare_querier<'b, TryIntoKeyExpr>( - &self, - key_expr: TryIntoKeyExpr, - ) -> LivelinessQuerierBuilder<'a, 'b> - where - TryIntoKeyExpr: TryInto>, - >>::Error: Into, - { - let timeout = { - let conf = &self.session.0.runtime.config().lock().0; - Duration::from_millis(unwrap_or_default!(conf.queries_default_timeout())) - }; - LivelinessQuerierBuilder { - session: self.session, - key_expr: TryIntoKeyExpr::try_into(key_expr).map_err(Into::into), - timeout, - } - } - /// Query liveliness tokens with matching key expressions. /// /// # Arguments @@ -701,371 +658,6 @@ impl IntoFuture for LivelinessSubscriberBuilder<'_, '_, Callback, true> } } -/// A querier that allows to perform livleiness queries. -/// -/// Queriers are automatically undeclared when dropped. -/// -/// # Examples -/// ``` -/// # #[tokio::main] -/// # async fn main() { -/// -/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); -/// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); -/// let tokens = querier.get().await.unwrap(); -/// # } -/// ``` -#[derive(Debug)] -pub struct LivelinessQuerier<'a> { - pub(crate) session: WeakSession, - pub(crate) id: Id, - pub(crate) key_expr: KeyExpr<'a>, - pub(crate) timeout: Duration, - pub(crate) undeclare_on_drop: bool, -} - -/// A builder for initializing a liveliness querier. -/// -/// # Examples -/// ``` -/// # #[tokio::main] -/// # async fn main() { -/// -/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); -/// let querier = session -/// .liveliness() -/// .declare_querier("key/expression") -/// .await -/// .unwrap(); -/// # } -/// ``` -#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] -#[zenoh_macros::unstable] -#[derive(Debug)] -pub struct LivelinessQuerierBuilder<'a, 'b> { - session: &'a Session, - key_expr: ZResult>, - timeout: Duration, -} - -impl<'a, 'b> LivelinessQuerierBuilder<'a, 'b> { - /// Set querier's queries timeout. - #[inline] - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self - } -} - -#[zenoh_macros::unstable] -impl<'a, 'b> Resolvable for LivelinessQuerierBuilder<'a, 'b> { - type To = ZResult>; -} - -#[zenoh_macros::unstable] -impl<'a> Wait for LivelinessQuerierBuilder<'a, '_> { - #[zenoh_macros::unstable] - fn wait(self) -> ::To { - let mut key_expr = self.key_expr?; - if !key_expr.is_fully_optimized(&self.session.0) { - key_expr = self.session.declare_keyexpr(key_expr).wait()?; - } - let id = self.session.0.declare_liveliness_querier_inner(&key_expr)?; - Ok(LivelinessQuerier { - session: self.session.downgrade(), - id, - key_expr, - timeout: self.timeout, - undeclare_on_drop: true, - }) - } -} - -#[zenoh_macros::unstable] -impl IntoFuture for LivelinessQuerierBuilder<'_, '_> { - type Output = ::To; - type IntoFuture = Ready<::To>; - - #[zenoh_macros::unstable] - fn into_future(self) -> Self::IntoFuture { - std::future::ready(self.wait()) - } -} - -impl<'a> LivelinessQuerier<'a> { - /// Returns the [`EntityGlobalId`] of this Querier. - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let querier = session.declare_querier("key/expression") - /// .await - /// .unwrap(); - /// let querier_id = querier.id(); - /// # } - /// ``` - #[zenoh_macros::unstable] - pub fn id(&self) -> EntityGlobalId { - use zenoh_protocol::core::EntityGlobalIdProto; - EntityGlobalIdProto { - zid: self.session.zid().into(), - eid: self.id, - } - .into() - } - - #[inline] - pub fn key_expr(&self) -> &KeyExpr<'a> { - &self.key_expr - } - - /// Perform a liveliness a query. - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); - /// let tokens = querier.get(); - /// # } - /// ``` - #[inline] - pub fn get(&self) -> LivelinessQuerierGetBuilder<'_, DefaultHandler> { - LivelinessQuerierGetBuilder { - querier: self, - handler: DefaultHandler::default(), - } - } - - /// Undeclare the [`LivelinessQuerier`], informing the network that it needn't optimize queries for its key expression anymore. - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); - /// querier.undeclare().await.unwrap(); - /// # } - /// ``` - pub fn undeclare(self) -> impl Resolve> + 'a { - UndeclarableSealed::undeclare_inner(self, ()) - } - - fn undeclare_impl(&mut self) -> ZResult<()> { - // set the flag first to avoid double panic if this function panic - self.undeclare_on_drop = false; - self.session.undeclare_liveliness_querier_inner(self.id) - } -} - -impl<'a> UndeclarableSealed<()> for LivelinessQuerier<'a> { - type Undeclaration = LivelinessQuerierUndeclaration<'a>; - - fn undeclare_inner(self, _: ()) -> Self::Undeclaration { - LivelinessQuerierUndeclaration(self) - } -} - -/// A [`Resolvable`] returned when undeclaring a publisher. -/// -/// # Examples -/// ``` -/// # #[tokio::main] -/// # async fn main() { -/// -/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); -/// let querier = session.declare_querier("key/expression").await.unwrap(); -/// querier.undeclare().await.unwrap(); -/// # } -/// ``` -#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] -pub struct LivelinessQuerierUndeclaration<'a>(LivelinessQuerier<'a>); - -impl Resolvable for LivelinessQuerierUndeclaration<'_> { - type To = ZResult<()>; -} - -impl Wait for LivelinessQuerierUndeclaration<'_> { - fn wait(mut self) -> ::To { - self.0.undeclare_impl() - } -} - -impl IntoFuture for LivelinessQuerierUndeclaration<'_> { - type Output = ::To; - type IntoFuture = Ready<::To>; - - fn into_future(self) -> Self::IntoFuture { - std::future::ready(self.wait()) - } -} - -impl Drop for LivelinessQuerier<'_> { - fn drop(&mut self) { - if self.undeclare_on_drop { - if let Err(error) = self.undeclare_impl() { - error!(error); - } - } - } -} - -/// A builder for initializing a liveliness querier's `query`. -/// -/// # Examples -/// ``` -/// # #[tokio::main] -/// # async fn main() { -/// # use std::convert::TryFrom; -/// -/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); -/// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); -/// let tokens = querier -/// .get() -/// .await -/// .unwrap(); -/// while let Ok(token) = tokens.recv_async().await { -/// match token.result() { -/// Ok(sample) => println!("Alive token ('{}')", sample.key_expr().as_str()), -/// Err(err) => println!("Received (ERROR: '{:?}')", err.payload()), -/// } -/// } -/// # } -/// ``` -#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] -#[derive(Debug)] -pub struct LivelinessQuerierGetBuilder<'a, Handler> { - pub(crate) querier: &'a LivelinessQuerier<'a>, - pub(crate) handler: Handler, -} - -impl<'a> LivelinessQuerierGetBuilder<'a, DefaultHandler> { - /// Receive the replies for this livliness querier's query with a callback. - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); - /// let _ = querier - /// .get() - /// .callback(|reply| { println!("Received {:?}", reply.result()); }) - /// .await - /// .unwrap(); - /// # } - /// ``` - #[inline] - pub fn callback(self, callback: F) -> LivelinessQuerierGetBuilder<'a, Callback> - where - F: Fn(Reply) + Send + Sync + 'static, - { - self.with(Callback::new(Arc::new(callback))) - } - - /// Receive the replies for this liveliness querier's query with a mutable callback. - /// - /// Using this guarantees that your callback will never be called concurrently. - /// If your callback is also accepted by the [`callback`](LivelinessQuerierGetBuilder::callback) method, we suggest you use it instead of `callback_mut`. - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let mut n = 0; - /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); - /// let _ = querier - /// .get() - /// .callback_mut(move |reply| {n += 1;}) - /// .await - /// .unwrap(); - /// # } - /// ``` - #[inline] - pub fn callback_mut(self, callback: F) -> LivelinessQuerierGetBuilder<'a, Callback> - where - F: FnMut(Reply) + Send + Sync + 'static, - { - self.callback(locked(callback)) - } - - /// Receive the replies for this liveliness querier's query with a [`Handler`](crate::handlers::IntoHandler). - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let querier = session.liveliness().declare_querier("key/expression").await.unwrap(); - /// let replies = querier - /// .get() - /// .with(flume::bounded(32)) - /// .await - /// .unwrap(); - /// while let Ok(reply) = replies.recv_async().await { - /// println!("Received {:?}", reply.result()); - /// } - /// # } - /// ``` - #[inline] - pub fn with(self, handler: Handler) -> LivelinessQuerierGetBuilder<'a, Handler> - where - Handler: IntoHandler, - { - let LivelinessQuerierGetBuilder { - querier, - handler: _, - } = self; - LivelinessQuerierGetBuilder { querier, handler } - } -} - -impl Resolvable for LivelinessQuerierGetBuilder<'_, Handler> -where - Handler: IntoHandler + Send, - Handler::Handler: Send, -{ - type To = ZResult; -} - -impl Wait for LivelinessQuerierGetBuilder<'_, Handler> -where - Handler: IntoHandler + Send, - Handler::Handler: Send, -{ - fn wait(self) -> ::To { - let (callback, receiver) = self.handler.into_handler(); - self.querier - .session - .liveliness_query(&self.querier.key_expr, self.querier.timeout, callback) - .map(|_| receiver) - } -} - -impl IntoFuture for LivelinessQuerierGetBuilder<'_, Handler> -where - Handler: IntoHandler + Send, - Handler::Handler: Send, -{ - type Output = ::To; - type IntoFuture = Ready<::To>; - - fn into_future(self) -> Self::IntoFuture { - std::future::ready(self.wait()) - } -} - /// A builder for initializing a liveliness `query`. /// /// # Examples diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 12bde1935c..6f45124669 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -219,10 +219,3 @@ impl Drop for Querier<'_> { } } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum QueriersKind { - Querier, - #[allow(dead_code)] - LivelinessQuerier, -} diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index b6714c9120..30145fe478 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -67,7 +67,6 @@ use zenoh_result::ZResult; use zenoh_shm::api::client_storage::ShmClientStorage; use zenoh_task::TaskController; -use super::querier::QueriersKind; #[cfg(feature = "unstable")] use crate::api::selector::ZenohParameters; #[cfg(feature = "unstable")] @@ -134,7 +133,6 @@ pub(crate) struct SessionState { pub(crate) remote_subscribers: HashMap>, pub(crate) publishers: HashMap, pub(crate) queriers: HashMap, - pub(crate) liveliness_queriers: HashMap, #[cfg(feature = "unstable")] pub(crate) remote_tokens: HashMap>, //pub(crate) publications: Vec, @@ -171,7 +169,6 @@ impl SessionState { remote_subscribers: HashMap::new(), publishers: HashMap::new(), queriers: HashMap::new(), - liveliness_queriers: HashMap::new(), #[cfg(feature = "unstable")] remote_tokens: HashMap::new(), //publications: Vec::new(), @@ -311,26 +308,11 @@ impl SessionState { } } - pub(crate) fn queriers(&self, kind: QueriersKind) -> &HashMap { - match kind { - QueriersKind::Querier => &self.queriers, - QueriersKind::LivelinessQuerier => &self.liveliness_queriers, - } - } - - pub(crate) fn queriers_mut(&mut self, kind: QueriersKind) -> &mut HashMap { - match kind { - QueriersKind::Querier => &mut self.queriers, - QueriersKind::LivelinessQuerier => &mut self.liveliness_queriers, - } - } - fn register_querier<'a>( &mut self, id: EntityId, key_expr: &'a KeyExpr, destination: Locality, - kind: QueriersKind, ) -> Option> { let mut querier_state = QuerierState { id, @@ -338,16 +320,16 @@ impl SessionState { key_expr: key_expr.clone().into_owned(), destination, }; - let aggregated_queriers: &[OwnedKeyExpr] = match kind { - QueriersKind::Querier => self.aggregated_queriers.as_slice(), - QueriersKind::LivelinessQuerier => &[] as &[OwnedKeyExpr; 0], - }; let declared_querier = (destination != Locality::SessionLocal) - .then( - || match aggregated_queriers.iter().find(|s| s.includes(key_expr)) { + .then(|| { + match self + .aggregated_queriers + .iter() + .find(|s| s.includes(key_expr)) + { Some(join_querier) => { - if let Some(joined_querier) = self.queriers(kind).values().find(|q| { + if let Some(joined_querier) = self.queriers.values().find(|q| { q.destination != Locality::SessionLocal && join_querier.includes(&q.key_expr) }) { @@ -358,7 +340,7 @@ impl SessionState { } } None => { - if let Some(twin_querier) = self.queriers(kind).values().find(|p| { + if let Some(twin_querier) = self.queriers.values().find(|p| { p.destination != Locality::SessionLocal && &p.key_expr == key_expr }) { querier_state.remote_id = twin_querier.remote_id; @@ -367,10 +349,10 @@ impl SessionState { Some(key_expr.clone()) } } - }, - ) + } + }) .flatten(); - self.queriers_mut(kind).insert(id, querier_state); + self.queriers.insert(id, querier_state); declared_querier } } @@ -1387,28 +1369,22 @@ impl SessionInner { } } - fn _declare_querier_inner( + pub(crate) fn declare_querier_inner( &self, - key_expr: &KeyExpr, + key_expr: KeyExpr, destination: Locality, - kind: QueriersKind, ) -> ZResult { + tracing::trace!("declare_querier({:?})", key_expr); let mut state = zwrite!(self.state); let id = self.runtime.next_id(); - let declared_querier = state.register_querier(id, key_expr, destination, kind); + let declared_querier = state.register_querier(id, &key_expr, destination); if let Some(res) = declared_querier { let primitives = state.primitives()?; drop(state); - let interest_options = match kind { - QueriersKind::Querier => InterestOptions::KEYEXPRS + InterestOptions::QUERYABLES, - QueriersKind::LivelinessQuerier => { - InterestOptions::KEYEXPRS + InterestOptions::TOKENS - } - }; primitives.send_interest(Interest { id, mode: InterestMode::CurrentFuture, - options: interest_options, + options: InterestOptions::KEYEXPRS + InterestOptions::QUERYABLES, wire_expr: Some(res.to_wire(self).to_owned()), ext_qos: network::ext::QoSType::DEFAULT, ext_tstamp: None, @@ -1418,27 +1394,17 @@ impl SessionInner { Ok(id) } - pub(crate) fn declare_querier_inner( - &self, - key_expr: KeyExpr, - destination: Locality, - ) -> ZResult { - tracing::trace!("declare_querier({:?})", key_expr); - self._declare_querier_inner(&key_expr, destination, QueriersKind::Querier) - } - - fn _undeclare_querier_inner(&self, pid: Id, kind: QueriersKind) -> ZResult<()> { + pub(crate) fn undeclare_querier_inner(&self, pid: Id) -> ZResult<()> { let mut state = zwrite!(self.state); let Ok(primitives) = state.primitives() else { return Ok(()); }; - let queriers = state.queriers_mut(kind); - if let Some(querier_state) = queriers.remove(&pid) { + if let Some(querier_state) = state.queriers.remove(&pid) { trace!("undeclare_querier({:?})", querier_state); if querier_state.destination != Locality::SessionLocal { // Note: there might be several queriers on the same KeyExpr. // Before calling forget_queriers(key_expr), check if this was the last one. - if !queriers.values().any(|p| { + if !state.queriers.values().any(|p| { p.destination != Locality::SessionLocal && p.remote_id == querier_state.remote_id }) { @@ -1460,10 +1426,6 @@ impl SessionInner { } } - pub(crate) fn undeclare_querier_inner(&self, pid: Id) -> ZResult<()> { - self._undeclare_querier_inner(pid, QueriersKind::Querier) - } - pub(crate) fn declare_subscriber_inner( self: &Arc, key_expr: &KeyExpr, @@ -1871,21 +1833,6 @@ impl SessionInner { Ok(sub_state) } - #[cfg(feature = "unstable")] - pub(crate) fn declare_liveliness_querier_inner(&self, key_expr: &KeyExpr) -> ZResult { - trace!("declare_liveliness_querier({:?})", key_expr); - self._declare_querier_inner( - key_expr, - Locality::default(), - QueriersKind::LivelinessQuerier, - ) - } - - #[cfg(feature = "unstable")] - pub(crate) fn undeclare_liveliness_querier_inner(&self, pid: Id) -> ZResult<()> { - self._undeclare_querier_inner(pid, QueriersKind::LivelinessQuerier) - } - #[zenoh_macros::unstable] pub(crate) fn undeclare_liveliness(&self, tid: Id) -> ZResult<()> { let mut state = zwrite!(self.state); diff --git a/zenoh/src/lib.rs b/zenoh/src/lib.rs index f940e9cbe9..e43f46770e 100644 --- a/zenoh/src/lib.rs +++ b/zenoh/src/lib.rs @@ -417,8 +417,7 @@ pub mod scouting { #[zenoh_macros::unstable] pub mod liveliness { pub use crate::api::liveliness::{ - Liveliness, LivelinessGetBuilder, LivelinessQuerier, LivelinessQuerierBuilder, - LivelinessQuerierGetBuilder, LivelinessSubscriberBuilder, LivelinessToken, + Liveliness, LivelinessGetBuilder, LivelinessSubscriberBuilder, LivelinessToken, LivelinessTokenBuilder, LivelinessTokenUndeclaration, }; } From abf5d5ded00d7941785b93729d857c9929d03b50 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 20 Nov 2024 13:18:16 +0100 Subject: [PATCH 07/30] add matching status for querier --- examples/examples/z_querier.rs | 5 + zenoh/src/api/builders/matching_listener.rs | 6 +- zenoh/src/api/publisher.rs | 27 +++-- zenoh/src/api/querier.rs | 56 +++++++-- zenoh/src/api/session.rs | 57 ++++++---- zenoh/src/net/routing/dispatcher/queries.rs | 13 +++ zenoh/src/net/routing/hat/client/pubsub.rs | 16 +-- zenoh/src/net/routing/hat/client/queries.rs | 75 +++++++++++++ .../net/routing/hat/linkstate_peer/queries.rs | 85 ++++++++++++++ zenoh/src/net/routing/hat/mod.rs | 8 ++ zenoh/src/net/routing/hat/p2p_peer/queries.rs | 42 +++++++ zenoh/src/net/routing/hat/router/queries.rs | 106 ++++++++++++++++++ zenoh/tests/matching.rs | 46 ++++---- 13 files changed, 464 insertions(+), 78 deletions(-) diff --git a/examples/examples/z_querier.rs b/examples/examples/z_querier.rs index fe21e9801a..d4a8845b73 100644 --- a/examples/examples/z_querier.rs +++ b/examples/examples/z_querier.rs @@ -37,6 +37,11 @@ async fn main() { println!("Press CTRL-C to quit..."); for idx in 0..u32::MAX { tokio::time::sleep(Duration::from_secs(1)).await; + #[cfg(feature = "unstable")] + println!( + "Matching status: {}", + querier.matching_status().await.unwrap().matching() + ); let buf = format!("[{idx:4}] {}", payload.clone().unwrap_or_default()); println!( "Querying '{}' with payload: '{}')...", diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index d81ba6e4c9..48e9debfd9 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -49,7 +49,7 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, DefaultHandler> { /// let matching_listener = publisher /// .matching_listener() /// .callback(|matching_status| { - /// if matching_status.matching_subscribers() { + /// if matching_status.matching() { /// println!("Publisher has matching subscribers."); /// } else { /// println!("Publisher has NO MORE matching subscribers."); @@ -115,7 +115,7 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, DefaultHandler> { /// .await /// .unwrap(); /// while let Ok(matching_status) = matching_listener.recv_async().await { - /// if matching_status.matching_subscribers() { + /// if matching_status.matching() { /// println!("Publisher has matching subscribers."); /// } else { /// println!("Publisher has NO MORE matching subscribers."); @@ -154,7 +154,7 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, Callback> { /// publisher /// .matching_listener() /// .callback(|matching_status| { - /// if matching_status.matching_subscribers() { + /// if matching_status.matching() { /// println!("Publisher has matching subscribers."); /// } else { /// println!("Publisher has NO MORE matching subscribers."); diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index 2b996504db..5c7ad1dcae 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -222,7 +222,7 @@ impl<'a> Publisher<'a> { /// Return the [`MatchingStatus`] of the publisher. /// - /// [`MatchingStatus::matching_subscribers`] will return true if there exist Subscribers + /// [`MatchingStatus::matching`] will return true if there exist Subscribers /// matching the Publisher's key expression and false otherwise. /// /// # Examples @@ -236,14 +236,17 @@ impl<'a> Publisher<'a> { /// .matching_status() /// .await /// .unwrap() - /// .matching_subscribers(); + /// .matching(); /// # } /// ``` #[zenoh_macros::unstable] pub fn matching_status(&self) -> impl Resolve> + '_ { zenoh_core::ResolveFuture::new(async move { - self.session - .matching_status(self.key_expr(), self.destination) + self.session.matching_status( + self.key_expr(), + self.destination, + MatchingStatusType::Subscribers, + ) }) } @@ -261,7 +264,7 @@ impl<'a> Publisher<'a> { /// let publisher = session.declare_publisher("key/expression").await.unwrap(); /// let matching_listener = publisher.matching_listener().await.unwrap(); /// while let Ok(matching_status) = matching_listener.recv_async().await { - /// if matching_status.matching_subscribers() { + /// if matching_status.matching() { /// println!("Publisher has matching subscribers."); /// } else { /// println!("Publisher has NO MORE matching subscribers."); @@ -500,7 +503,7 @@ impl TryFrom for Priority { } } -/// A struct that indicates if there exist Subscribers matching the Publisher's key expression. +/// A struct that indicates if there exist entities matching the key expression. /// /// # Examples /// ``` @@ -518,6 +521,12 @@ pub struct MatchingStatus { pub(crate) matching: bool, } +#[cfg(feature = "unstable")] +pub(crate) enum MatchingStatusType { + Subscribers, + Queryables(bool), +} + #[zenoh_macros::unstable] impl MatchingStatus { /// Return true if there exist Subscribers matching the Publisher's key expression. @@ -533,10 +542,10 @@ impl MatchingStatus { /// .matching_status() /// .await /// .unwrap() - /// .matching_subscribers(); + /// .matching(); /// # } /// ``` - pub fn matching_subscribers(&self) -> bool { + pub fn matching(&self) -> bool { self.matching } } @@ -583,7 +592,7 @@ pub(crate) struct MatchingListenerInner { /// let publisher = session.declare_publisher("key/expression").await.unwrap(); /// let matching_listener = publisher.matching_listener().await.unwrap(); /// while let Ok(matching_status) = matching_listener.recv_async().await { -/// if matching_status.matching_subscribers() { +/// if matching_status.matching() { /// println!("Publisher has matching subscribers."); /// } else { /// println!("Publisher has NO MORE matching subscribers."); diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 6f45124669..39451ac592 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -4,6 +4,15 @@ use std::{ time::Duration, }; +use super::{ + builders::querier::QuerierGetBuilder, + key_expr::KeyExpr, + query::QueryConsolidation, + sample::{Locality, QoS}, + session::{UndeclarableSealed, WeakSession}, + Id, +}; +use crate::{api::handlers::DefaultHandler, qos::Priority}; use tracing::error; use zenoh_core::{Resolvable, Resolve, Wait}; use zenoh_protocol::{ @@ -13,19 +22,12 @@ use zenoh_protocol::{ use zenoh_result::ZResult; #[cfg(feature = "unstable")] use { - crate::api::sample::SourceInfo, crate::query::ReplyKeyExpr, - zenoh_config::wrappers::EntityGlobalId, zenoh_protocol::core::EntityGlobalIdProto, -}; - -use super::{ - builders::querier::QuerierGetBuilder, - key_expr::KeyExpr, - query::QueryConsolidation, - sample::{Locality, QoS}, - session::{UndeclarableSealed, WeakSession}, - Id, + crate::api::publisher::{MatchingStatus, MatchingStatusType}, + crate::api::sample::SourceInfo, + crate::query::ReplyKeyExpr, + zenoh_config::wrappers::EntityGlobalId, + zenoh_protocol::core::EntityGlobalIdProto, }; -use crate::{api::handlers::DefaultHandler, qos::Priority}; pub(crate) struct QuerierState { pub(crate) id: Id, @@ -166,6 +168,36 @@ impl<'a> Querier<'a> { self.undeclare_on_drop = false; self.session.undeclare_querier_inner(self.id) } + + /// Return the [`MatchingStatus`] of the querier. + /// + /// [`MatchingStatus::matching`] will return true if there exist Queryables + /// matching the Queriers's key expression and target and false otherwise. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let querier = session.declare_querier("key/expression").await.unwrap(); + /// let matching_queriers: bool = querier + /// .matching_status() + /// .await + /// .unwrap() + /// .matching(); + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn matching_status(&self) -> impl Resolve> + '_ { + zenoh_core::ResolveFuture::new(async move { + self.session.matching_status( + self.key_expr(), + self.destination, + MatchingStatusType::Queryables(self.target == QueryTarget::AllComplete), + ) + }) + } } impl<'a> UndeclarableSealed<()> for Querier<'a> { diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 30145fe478..2d10ac099d 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -73,7 +73,7 @@ use crate::api::selector::ZenohParameters; use crate::api::{ liveliness::{Liveliness, LivelinessTokenState}, publisher::Publisher, - publisher::{MatchingListenerState, MatchingStatus}, + publisher::{MatchingListenerState, MatchingStatus, MatchingStatusType}, query::{LivelinessQueryState, ReplyKeyExpr}, sample::SourceInfo, }; @@ -1884,8 +1884,12 @@ impl SessionInner { match listener_state.current.lock() { Ok(mut current) => { if self - .matching_status(&publisher.key_expr, listener_state.destination) - .map(|s| s.matching_subscribers()) + .matching_status( + &publisher.key_expr, + listener_state.destination, + MatchingStatusType::Subscribers, + ) + .map(|s| s.matching()) .unwrap_or(true) { *current = true; @@ -1904,30 +1908,37 @@ impl SessionInner { &self, key_expr: &KeyExpr, destination: Locality, + matching_type: MatchingStatusType, ) -> ZResult { let router = self.runtime.router(); let tables = zread!(router.tables.tables); - let matching_subscriptions = - crate::net::routing::dispatcher::pubsub::get_matching_subscriptions(&tables, key_expr); + let matches = match matching_type { + MatchingStatusType::Subscribers => { + crate::net::routing::dispatcher::pubsub::get_matching_subscriptions( + &tables, key_expr, + ) + } + MatchingStatusType::Queryables(complete) => { + crate::net::routing::dispatcher::queries::get_matching_queryables( + &tables, key_expr, complete, + ) + } + }; drop(tables); let matching = match destination { - Locality::Any => !matching_subscriptions.is_empty(), + Locality::Any => !matches.is_empty(), Locality::Remote => { if let Some(face) = zread!(self.state).primitives.as_ref() { - matching_subscriptions - .values() - .any(|dir| !Arc::ptr_eq(dir, &face.state)) + matches.values().any(|dir| !Arc::ptr_eq(dir, &face.state)) } else { - !matching_subscriptions.is_empty() + !matches.is_empty() } } Locality::SessionLocal => { if let Some(face) = zread!(self.state).primitives.as_ref() { - matching_subscriptions - .values() - .any(|dir| Arc::ptr_eq(dir, &face.state)) + matches.values().any(|dir| Arc::ptr_eq(dir, &face.state)) } else { false } @@ -1950,10 +1961,12 @@ impl SessionInner { match msub.current.lock() { Ok(mut current) => { if !*current { - if let Ok(status) = session - .matching_status(&msub.key_expr, msub.destination) - { - if status.matching_subscribers() { + if let Ok(status) = session.matching_status( + &msub.key_expr, + msub.destination, + MatchingStatusType::Subscribers, + ) { + if status.matching() { *current = true; let callback = msub.callback.clone(); callback.call(status) @@ -1988,10 +2001,12 @@ impl SessionInner { match msub.current.lock() { Ok(mut current) => { if *current { - if let Ok(status) = session - .matching_status(&msub.key_expr, msub.destination) - { - if !status.matching_subscribers() { + if let Ok(status) = session.matching_status( + &msub.key_expr, + msub.destination, + MatchingStatusType::Subscribers, + ) { + if !status.matching() { *current = false; let callback = msub.callback.clone(); callback.call(status) diff --git a/zenoh/src/net/routing/dispatcher/queries.rs b/zenoh/src/net/routing/dispatcher/queries.rs index f8a9f1f128..89dde9ab9a 100644 --- a/zenoh/src/net/routing/dispatcher/queries.rs +++ b/zenoh/src/net/routing/dispatcher/queries.rs @@ -43,6 +43,7 @@ use super::{ resource::{QueryRoute, QueryRoutes, QueryTargetQablSet, Resource}, tables::{NodeId, RoutingExpr, Tables, TablesLock}, }; +use crate::key_expr::KeyExpr; use crate::net::routing::hat::{HatTrait, SendDeclare}; pub(crate) struct Query { @@ -50,6 +51,18 @@ pub(crate) struct Query { src_qid: RequestId, } +#[zenoh_macros::unstable] +#[inline] +pub(crate) fn get_matching_queryables( + tables: &Tables, + key_expr: &KeyExpr<'_>, + complete: bool, +) -> HashMap> { + tables + .hat_code + .get_matching_queryables(tables, key_expr, complete) +} + #[allow(clippy::too_many_arguments)] pub(crate) fn declare_queryable( hat_code: &(dyn HatTrait + Send + Sync), diff --git a/zenoh/src/net/routing/hat/client/pubsub.rs b/zenoh/src/net/routing/hat/client/pubsub.rs index 11fca5ff12..cf92614e5f 100644 --- a/zenoh/src/net/routing/hat/client/pubsub.rs +++ b/zenoh/src/net/routing/hat/client/pubsub.rs @@ -410,17 +410,13 @@ impl HatPubSubTrait for HatCode { && interest .res .as_ref() - .map(|res| { - KeyExpr::try_from(res.expr()) - .map(|intres| intres.includes(key_expr)) - .unwrap_or(false) - }) + .map(|res| KeyExpr::keyexpr_include(res.expr(), key_expr)) .unwrap_or(true) - }) && face_hat!(face).remote_subs.values().any(|sub| { - KeyExpr::try_from(sub.expr()) - .map(|subres| subres.intersects(key_expr)) - .unwrap_or(false) - }) { + }) && face_hat!(face) + .remote_subs + .values() + .any(|sub| KeyExpr::keyexpr_intersect(sub.expr(), key_expr)) + { matching_subscriptions.insert(face.id, face.clone()); } } diff --git a/zenoh/src/net/routing/hat/client/queries.rs b/zenoh/src/net/routing/hat/client/queries.rs index 66ab198232..9d666239e6 100644 --- a/zenoh/src/net/routing/hat/client/queries.rs +++ b/zenoh/src/net/routing/hat/client/queries.rs @@ -411,4 +411,79 @@ impl HatQueriesTrait for HatCode { fn get_query_routes_entries(&self, _tables: &Tables) -> RoutesIndexes { get_routes_entries() } + + #[cfg(feature = "unstable")] + fn get_matching_queryables( + &self, + tables: &Tables, + key_expr: &KeyExpr<'_>, + complete: bool, + ) -> HashMap> { + let mut matching_queryables = HashMap::new(); + if key_expr.ends_with('/') { + return matching_queryables; + } + tracing::trace!( + "get_matching_queryables({}; complete: {})", + key_expr, + complete + ); + for face in tables + .faces + .values() + .filter(|f| f.whatami != WhatAmI::Client) + { + if face.local_interests.values().any(|interest| { + interest.finalized + && interest.options.queryables() + && interest + .res + .as_ref() + .map(|res| KeyExpr::keyexpr_include(res.expr(), key_expr)) + .unwrap_or(true) + }) && face_hat!(face) + .remote_qabls + .values() + .any(|qbl| match complete { + true => { + qbl.session_ctxs + .get(&face.id) + .and_then(|sc| sc.qabl) + .map_or(false, |q| q.complete) + && KeyExpr::keyexpr_include(qbl.expr(), key_expr) + } + false => KeyExpr::keyexpr_intersect(qbl.expr(), key_expr), + }) + { + matching_queryables.insert(face.id, face.clone()); + } + } + + let res = Resource::get_resource(&tables.root_res, key_expr); + let matches = res + .as_ref() + .and_then(|res| res.context.as_ref()) + .map(|ctx| Cow::from(&ctx.matches)) + .unwrap_or_else(|| Cow::from(Resource::get_matches(tables, key_expr))); + + for mres in matches.iter() { + let mres = mres.upgrade().unwrap(); + if complete && !KeyExpr::keyexpr_include(mres.expr(), key_expr) { + continue; + } + for (sid, context) in &mres.session_ctxs { + if context.face.whatami == WhatAmI::Client { + if match complete { + true => context.qabl.map_or(false, |q| q.complete), + false => context.qabl.is_some(), + } { + matching_queryables + .entry(*sid) + .or_insert_with(|| context.face.clone()); + } + } + } + } + matching_queryables + } } diff --git a/zenoh/src/net/routing/hat/linkstate_peer/queries.rs b/zenoh/src/net/routing/hat/linkstate_peer/queries.rs index 8668c01ba2..4f2e061f57 100644 --- a/zenoh/src/net/routing/hat/linkstate_peer/queries.rs +++ b/zenoh/src/net/routing/hat/linkstate_peer/queries.rs @@ -40,6 +40,7 @@ use super::{ face_hat, face_hat_mut, get_peer, get_routes_entries, hat, hat_mut, network::Network, res_hat, res_hat_mut, HatCode, HatContext, HatFace, HatTables, }; +use crate::key_expr::KeyExpr; use crate::net::routing::{ dispatcher::{ face::FaceState, @@ -1011,4 +1012,88 @@ impl HatQueriesTrait for HatCode { fn get_query_routes_entries(&self, tables: &Tables) -> RoutesIndexes { get_routes_entries(tables) } + + #[cfg(feature = "unstable")] + fn get_matching_queryables( + &self, + tables: &Tables, + key_expr: &KeyExpr<'_>, + complete: bool, + ) -> HashMap> { + let mut matching_queryables = HashMap::new(); + if key_expr.ends_with('/') { + return matching_queryables; + } + tracing::trace!( + "get_matching_queryables({}; complete: {})", + key_expr, + complete + ); + + let res = Resource::get_resource(&tables.root_res, key_expr); + let matches = res + .as_ref() + .and_then(|res| res.context.as_ref()) + .map(|ctx| Cow::from(&ctx.matches)) + .unwrap_or_else(|| Cow::from(Resource::get_matches(tables, key_expr))); + + for mres in matches.iter() { + let mres = mres.upgrade().unwrap(); + if complete && !KeyExpr::keyexpr_include(mres.expr(), key_expr) { + continue; + } + + let net = hat!(tables).linkstatepeers_net.as_ref().unwrap(); + insert_faces_for_qbls( + &mut matching_queryables, + tables, + net, + &res_hat!(mres).linkstatepeer_qabls, + complete, + ); + + for (sid, context) in &mres.session_ctxs { + if match complete { + true => context.qabl.map_or(false, |q| q.complete), + false => context.qabl.is_some(), + } { + matching_queryables + .entry(*sid) + .or_insert_with(|| context.face.clone()); + } + } + } + matching_queryables + } +} + +#[inline] +fn insert_faces_for_qbls( + route: &mut HashMap>, + tables: &Tables, + net: &Network, + qbls: &HashMap, + complete: bool, +) { + let source = net.idx.index(); + if net.trees.len() > source { + for qbl in qbls { + if complete && !qbl.1.complete { + continue; + } + if let Some(qbl_idx) = net.get_idx(qbl.0) { + if net.trees[source].directions.len() > qbl_idx.index() { + if let Some(direction) = net.trees[source].directions[qbl_idx.index()] { + if net.graph.contains_node(direction) { + if let Some(face) = tables.get_face(&net.graph[direction].zid) { + route.entry(face.id).or_insert_with(|| face.clone()); + } + } + } + } + } + } + } else { + tracing::trace!("Tree for node sid:{} not yet ready", source); + } } diff --git a/zenoh/src/net/routing/hat/mod.rs b/zenoh/src/net/routing/hat/mod.rs index fb2ae44c3a..92d33115f1 100644 --- a/zenoh/src/net/routing/hat/mod.rs +++ b/zenoh/src/net/routing/hat/mod.rs @@ -232,6 +232,14 @@ pub(crate) trait HatQueriesTrait { ) -> Arc; fn get_query_routes_entries(&self, tables: &Tables) -> RoutesIndexes; + + #[zenoh_macros::unstable] + fn get_matching_queryables( + &self, + tables: &Tables, + key_expr: &KeyExpr<'_>, + complete: bool, + ) -> HashMap>; } pub(crate) fn new_hat(whatami: WhatAmI, config: &Config) -> Box { diff --git a/zenoh/src/net/routing/hat/p2p_peer/queries.rs b/zenoh/src/net/routing/hat/p2p_peer/queries.rs index 8efa87cc26..db67952745 100644 --- a/zenoh/src/net/routing/hat/p2p_peer/queries.rs +++ b/zenoh/src/net/routing/hat/p2p_peer/queries.rs @@ -669,4 +669,46 @@ impl HatQueriesTrait for HatCode { fn get_query_routes_entries(&self, _tables: &Tables) -> RoutesIndexes { get_routes_entries() } + + #[cfg(feature = "unstable")] + fn get_matching_queryables( + &self, + tables: &Tables, + key_expr: &KeyExpr<'_>, + complete: bool, + ) -> HashMap> { + let mut matching_queryables = HashMap::new(); + if key_expr.ends_with('/') { + return matching_queryables; + } + tracing::trace!( + "get_matching_queryables({}; complete: {})", + key_expr, + complete + ); + let res = Resource::get_resource(&tables.root_res, key_expr); + let matches = res + .as_ref() + .and_then(|res| res.context.as_ref()) + .map(|ctx| Cow::from(&ctx.matches)) + .unwrap_or_else(|| Cow::from(Resource::get_matches(tables, key_expr))); + + for mres in matches.iter() { + let mres = mres.upgrade().unwrap(); + if complete && !KeyExpr::keyexpr_include(mres.expr(), key_expr) { + continue; + } + for (sid, context) in &mres.session_ctxs { + if match complete { + true => context.qabl.map_or(false, |q| q.complete), + false => context.qabl.is_some(), + } { + matching_queryables + .entry(*sid) + .or_insert_with(|| context.face.clone()); + } + } + } + matching_queryables + } } diff --git a/zenoh/src/net/routing/hat/router/queries.rs b/zenoh/src/net/routing/hat/router/queries.rs index 34e05c43c0..f3c336f6f8 100644 --- a/zenoh/src/net/routing/hat/router/queries.rs +++ b/zenoh/src/net/routing/hat/router/queries.rs @@ -53,6 +53,8 @@ use crate::net::routing::{ RoutingContext, }; +use crate::key_expr::KeyExpr; + #[inline] fn merge_qabl_infos(mut this: QueryableInfoType, info: &QueryableInfoType) -> QueryableInfoType { this.complete = this.complete || info.complete; @@ -1494,4 +1496,108 @@ impl HatQueriesTrait for HatCode { fn get_query_routes_entries(&self, tables: &Tables) -> RoutesIndexes { get_routes_entries(tables) } + + #[cfg(feature = "unstable")] + fn get_matching_queryables( + &self, + tables: &Tables, + key_expr: &KeyExpr<'_>, + complete: bool, + ) -> HashMap> { + let mut matching_queryables = HashMap::new(); + if key_expr.ends_with('/') { + return matching_queryables; + } + tracing::trace!( + "get_matching_queryables({}; complete: {})", + key_expr, + complete + ); + crate::net::routing::dispatcher::pubsub::get_matching_subscriptions(&tables, key_expr); + let res = Resource::get_resource(&tables.root_res, key_expr); + let matches = res + .as_ref() + .and_then(|res| res.context.as_ref()) + .map(|ctx| Cow::from(&ctx.matches)) + .unwrap_or_else(|| Cow::from(Resource::get_matches(tables, key_expr))); + + let master = !hat!(tables).full_net(WhatAmI::Peer) + || *hat!(tables).elect_router(&tables.zid, key_expr, hat!(tables).shared_nodes.iter()) + == tables.zid; + + for mres in matches.iter() { + let mres = mres.upgrade().unwrap(); + if complete && !KeyExpr::keyexpr_include(mres.expr(), key_expr) { + continue; + } + + if master { + let net = hat!(tables).routers_net.as_ref().unwrap(); + insert_faces_for_qbls( + &mut matching_queryables, + tables, + net, + &res_hat!(mres).router_qabls, + complete, + ); + } + + if hat!(tables).full_net(WhatAmI::Peer) { + let net = hat!(tables).linkstatepeers_net.as_ref().unwrap(); + insert_faces_for_qbls( + &mut matching_queryables, + tables, + net, + &res_hat!(mres).linkstatepeer_qabls, + complete, + ); + } + + if master { + for (sid, context) in &mres.session_ctxs { + if match complete { + true => context.qabl.map_or(false, |q| q.complete), + false => context.qabl.is_some(), + } && context.face.whatami != WhatAmI::Router + { + matching_queryables + .entry(*sid) + .or_insert_with(|| context.face.clone()); + } + } + } + } + matching_queryables + } +} + +#[inline] +fn insert_faces_for_qbls( + route: &mut HashMap>, + tables: &Tables, + net: &Network, + qbls: &HashMap, + complete: bool, +) { + let source = net.idx.index(); + if net.trees.len() > source { + for qbl in qbls { + if complete && !qbl.1.complete { + continue; + } + if let Some(qbl_idx) = net.get_idx(qbl.0) { + if net.trees[source].directions.len() > qbl_idx.index() { + if let Some(direction) = net.trees[source].directions[qbl_idx.index()] { + if net.graph.contains_node(direction) { + if let Some(face) = tables.get_face(&net.graph[direction].zid) { + route.entry(face.id).or_insert_with(|| face.clone()); + } + } + } + } + } + } + } else { + tracing::trace!("Tree for node sid:{} not yet ready", source); + } } diff --git a/zenoh/tests/matching.rs b/zenoh/tests/matching.rs index 5fc3256cf6..d92148ffde 100644 --- a/zenoh/tests/matching.rs +++ b/zenoh/tests/matching.rs @@ -61,7 +61,7 @@ async fn zenoh_matching_status_any() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); let sub = ztimeout!(session1.declare_subscriber("zenoh_matching_status_any_test")).unwrap(); @@ -69,11 +69,11 @@ async fn zenoh_matching_status_any() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(true))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching_subscribers()); + assert!(matching_status.matching()); ztimeout!(sub.undeclare()).unwrap(); @@ -81,11 +81,11 @@ async fn zenoh_matching_status_any() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(false))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); let sub = ztimeout!(session2.declare_subscriber("zenoh_matching_status_any_test")).unwrap(); @@ -93,11 +93,11 @@ async fn zenoh_matching_status_any() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(true))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching_subscribers()); + assert!(matching_status.matching()); ztimeout!(sub.undeclare()).unwrap(); @@ -105,11 +105,11 @@ async fn zenoh_matching_status_any() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(false))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); Ok(()) } @@ -131,7 +131,7 @@ async fn zenoh_matching_status_remote() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); let sub = ztimeout!(session1.declare_subscriber("zenoh_matching_status_remote_test")).unwrap(); @@ -139,7 +139,7 @@ async fn zenoh_matching_status_remote() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); ztimeout!(sub.undeclare()).unwrap(); @@ -147,7 +147,7 @@ async fn zenoh_matching_status_remote() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); let sub = ztimeout!(session2.declare_subscriber("zenoh_matching_status_remote_test")).unwrap(); @@ -155,11 +155,11 @@ async fn zenoh_matching_status_remote() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(true))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching_subscribers()); + assert!(matching_status.matching()); ztimeout!(sub.undeclare()).unwrap(); @@ -167,11 +167,11 @@ async fn zenoh_matching_status_remote() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(false))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); Ok(()) } @@ -194,7 +194,7 @@ async fn zenoh_matching_status_local() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); let sub = ztimeout!(session1.declare_subscriber("zenoh_matching_status_local_test")).unwrap(); @@ -202,11 +202,11 @@ async fn zenoh_matching_status_local() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(true))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching_subscribers()); + assert!(matching_status.matching()); ztimeout!(sub.undeclare()).unwrap(); @@ -214,11 +214,11 @@ async fn zenoh_matching_status_local() -> ZResult<()> { assert!(received_status .ok() .flatten() - .map(|s| s.matching_subscribers()) + .map(|s| s.matching()) .eq(&Some(false))); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); let sub = ztimeout!(session2.declare_subscriber("zenoh_matching_status_local_test")).unwrap(); @@ -226,7 +226,7 @@ async fn zenoh_matching_status_local() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); ztimeout!(sub.undeclare()).unwrap(); @@ -234,7 +234,7 @@ async fn zenoh_matching_status_local() -> ZResult<()> { assert!(received_status.unwrap().is_none()); let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching_subscribers()); + assert!(!matching_status.matching()); Ok(()) } From 92be499790f1e0ab132e5aca92ebcb8c2ab3f28e Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 19:44:47 +0100 Subject: [PATCH 08/30] add matching listener support --- examples/examples/z_querier.rs | 20 +- zenoh/src/api/admin.rs | 4 +- zenoh/src/api/builders/matching_listener.rs | 93 ++++++---- zenoh/src/api/builders/querier.rs | 2 + zenoh/src/api/builders/queryable.rs | 9 +- zenoh/src/api/publisher.rs | 32 +++- zenoh/src/api/querier.rs | 48 +++++ zenoh/src/api/session.rs | 193 +++++++++++++------- zenoh/src/net/routing/hat/router/queries.rs | 2 +- 9 files changed, 286 insertions(+), 117 deletions(-) diff --git a/examples/examples/z_querier.rs b/examples/examples/z_querier.rs index d4a8845b73..19f610e69e 100644 --- a/examples/examples/z_querier.rs +++ b/examples/examples/z_querier.rs @@ -34,14 +34,24 @@ async fn main() { .timeout(timeout) .await .unwrap(); + + #[cfg(feature = "unstable")] + querier + .matching_listener() + .callback(|matching_status| { + if matching_status.matching() { + println!("Querier has matching queryables."); + } else { + println!("Querier has NO MORE matching queryables."); + } + }) + .background() + .await + .unwrap(); + println!("Press CTRL-C to quit..."); for idx in 0..u32::MAX { tokio::time::sleep(Duration::from_secs(1)).await; - #[cfg(feature = "unstable")] - println!( - "Matching status: {}", - querier.matching_status().await.unwrap().matching() - ); let buf = format!("[{idx:4}] {}", payload.clone().unwrap_or_default()); println!( "Querying '{}' with payload: '{}')...", diff --git a/zenoh/src/api/admin.rs b/zenoh/src/api/admin.rs index b0d0bed0f7..d043ec856c 100644 --- a/zenoh/src/api/admin.rs +++ b/zenoh/src/api/admin.rs @@ -48,9 +48,7 @@ lazy_static::lazy_static!( pub(crate) fn init(session: WeakSession) { if let Ok(own_zid) = keyexpr::new(&session.zid().to_string()) { - let admin_key = KeyExpr::from(*KE_PREFIX / own_zid / *KE_SESSION / *KE_STARSTAR) - .to_wire(&session) - .to_owned(); + let admin_key = KeyExpr::from(*KE_PREFIX / own_zid / *KE_SESSION / *KE_STARSTAR); let _admin_qabl = session.declare_queryable_inner( &admin_key, diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index 48e9debfd9..2ef4d0afcb 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -13,16 +13,23 @@ // #[cfg(feature = "unstable")] use std::future::{IntoFuture, Ready}; +use std::{collections::HashSet, sync::Mutex}; +use crate::{ + api::{publisher::MatchingStatusType, session::WeakSession, Id}, + key_expr::KeyExpr, + sample::Locality, +}; #[cfg(feature = "unstable")] use zenoh_core::{Resolvable, Wait}; #[cfg(feature = "unstable")] use zenoh_result::ZResult; + #[cfg(feature = "unstable")] use { crate::api::{ handlers::{Callback, DefaultHandler, IntoHandler}, - publisher::{MatchingListener, MatchingListenerInner, MatchingStatus, Publisher}, + publisher::{MatchingListener, MatchingListenerInner, MatchingStatus}, }, std::sync::Arc, }; @@ -30,13 +37,17 @@ use { /// A builder for initializing a [`MatchingListener`]. #[zenoh_macros::unstable] #[derive(Debug)] -pub struct MatchingListenerBuilder<'a, 'b, Handler, const BACKGROUND: bool = false> { - pub(crate) publisher: &'a Publisher<'b>, +pub struct MatchingListenerBuilder<'a, Handler, const BACKGROUND: bool = false> { + pub(crate) session: &'a WeakSession, + pub(crate) key_expr: &'a KeyExpr<'a>, + pub(crate) destination: Locality, + pub(crate) matching_listeners: &'a Arc>>, + pub(crate) matching_status_type: MatchingStatusType, pub handler: Handler, } #[zenoh_macros::unstable] -impl<'a, 'b> MatchingListenerBuilder<'a, 'b, DefaultHandler> { +impl<'a> MatchingListenerBuilder<'a, DefaultHandler> { /// Receive the MatchingStatuses for this listener with a callback. /// /// # Examples @@ -61,10 +72,7 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, DefaultHandler> { /// ``` #[inline] #[zenoh_macros::unstable] - pub fn callback( - self, - callback: F, - ) -> MatchingListenerBuilder<'a, 'b, Callback> + pub fn callback(self, callback: F) -> MatchingListenerBuilder<'a, Callback> where F: Fn(MatchingStatus) + Send + Sync + 'static, { @@ -93,7 +101,7 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, DefaultHandler> { pub fn callback_mut( self, callback: F, - ) -> MatchingListenerBuilder<'a, 'b, Callback> + ) -> MatchingListenerBuilder<'a, Callback> where F: FnMut(MatchingStatus) + Send + Sync + 'static, { @@ -125,20 +133,31 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, DefaultHandler> { /// ``` #[inline] #[zenoh_macros::unstable] - pub fn with(self, handler: Handler) -> MatchingListenerBuilder<'a, 'b, Handler> + pub fn with(self, handler: Handler) -> MatchingListenerBuilder<'a, Handler> where Handler: IntoHandler, { let MatchingListenerBuilder { - publisher, + session, + key_expr, + destination, + matching_listeners, + matching_status_type, handler: _, } = self; - MatchingListenerBuilder { publisher, handler } + MatchingListenerBuilder { + session, + key_expr, + destination, + matching_listeners, + matching_status_type, + handler, + } } } #[zenoh_macros::unstable] -impl<'a, 'b> MatchingListenerBuilder<'a, 'b, Callback> { +impl<'a> MatchingListenerBuilder<'a, Callback> { /// Register the listener callback to be run in background until the publisher is undeclared. /// /// Background builder doesn't return a `MatchingListener` object anymore. @@ -165,16 +184,20 @@ impl<'a, 'b> MatchingListenerBuilder<'a, 'b, Callback> { /// .unwrap(); /// # } /// ``` - pub fn background(self) -> MatchingListenerBuilder<'a, 'b, Callback, true> { + pub fn background(self) -> MatchingListenerBuilder<'a, Callback, true> { MatchingListenerBuilder { - publisher: self.publisher, + session: self.session, + destination: self.destination, + matching_listeners: self.matching_listeners, + key_expr: self.key_expr, + matching_status_type: self.matching_status_type, handler: self.handler, } } } #[zenoh_macros::unstable] -impl Resolvable for MatchingListenerBuilder<'_, '_, Handler> +impl Resolvable for MatchingListenerBuilder<'_, Handler> where Handler: IntoHandler + Send, Handler::Handler: Send, @@ -183,7 +206,7 @@ where } #[zenoh_macros::unstable] -impl Wait for MatchingListenerBuilder<'_, '_, Handler> +impl Wait for MatchingListenerBuilder<'_, Handler> where Handler: IntoHandler + Send, Handler::Handler: Send, @@ -191,15 +214,17 @@ where #[zenoh_macros::unstable] fn wait(self) -> ::To { let (callback, handler) = self.handler.into_handler(); - let state = self - .publisher - .session - .declare_matches_listener_inner(self.publisher, callback)?; - zlock!(self.publisher.matching_listeners).insert(state.id); + let state = self.session.declare_matches_listener_inner( + self.key_expr, + self.destination, + self.matching_status_type, + callback, + )?; + zlock!(self.matching_listeners).insert(state.id); Ok(MatchingListener { inner: MatchingListenerInner { - session: self.publisher.session.clone(), - matching_listeners: self.publisher.matching_listeners.clone(), + session: self.session.clone(), + matching_listeners: self.matching_listeners.clone(), id: state.id, undeclare_on_drop: true, }, @@ -209,7 +234,7 @@ where } #[zenoh_macros::unstable] -impl IntoFuture for MatchingListenerBuilder<'_, '_, Handler> +impl IntoFuture for MatchingListenerBuilder<'_, Handler> where Handler: IntoHandler + Send, Handler::Handler: Send, @@ -224,25 +249,27 @@ where } #[zenoh_macros::unstable] -impl Resolvable for MatchingListenerBuilder<'_, '_, Callback, true> { +impl Resolvable for MatchingListenerBuilder<'_, Callback, true> { type To = ZResult<()>; } #[zenoh_macros::unstable] -impl Wait for MatchingListenerBuilder<'_, '_, Callback, true> { +impl Wait for MatchingListenerBuilder<'_, Callback, true> { #[zenoh_macros::unstable] fn wait(self) -> ::To { - let state = self - .publisher - .session - .declare_matches_listener_inner(self.publisher, self.handler)?; - zlock!(self.publisher.matching_listeners).insert(state.id); + let state = self.session.declare_matches_listener_inner( + self.key_expr, + self.destination, + self.matching_status_type, + self.handler, + )?; + zlock!(self.matching_listeners).insert(state.id); Ok(()) } } #[zenoh_macros::unstable] -impl IntoFuture for MatchingListenerBuilder<'_, '_, Callback, true> { +impl IntoFuture for MatchingListenerBuilder<'_, Callback, true> { type Output = ::To; type IntoFuture = Ready<::To>; diff --git a/zenoh/src/api/builders/querier.rs b/zenoh/src/api/builders/querier.rs index 52ccc0fb97..20daf9546b 100644 --- a/zenoh/src/api/builders/querier.rs +++ b/zenoh/src/api/builders/querier.rs @@ -176,6 +176,8 @@ impl<'a, 'b> Wait for QuerierBuilder<'a, 'b> { timeout: self.timeout, #[cfg(feature = "unstable")] accept_replies: self.accept_replies, + #[cfg(feature = "unstable")] + matching_listeners: Default::default(), }) } } diff --git a/zenoh/src/api/builders/queryable.rs b/zenoh/src/api/builders/queryable.rs index 8d4befbef2..f35e19e731 100644 --- a/zenoh/src/api/builders/queryable.rs +++ b/zenoh/src/api/builders/queryable.rs @@ -211,12 +211,7 @@ where let (callback, receiver) = self.handler.into_handler(); session .0 - .declare_queryable_inner( - &self.key_expr?.to_wire(&session.0), - self.complete, - self.origin, - callback, - ) + .declare_queryable_inner(&self.key_expr?, self.complete, self.origin, callback) .map(|qable_state| Queryable { inner: QueryableInner { session: self.session.downgrade(), @@ -248,7 +243,7 @@ impl Resolvable for QueryableBuilder<'_, '_, Callback, true> { impl Wait for QueryableBuilder<'_, '_, Callback, true> { fn wait(self) -> ::To { self.session.0.declare_queryable_inner( - &self.key_expr?.to_wire(&self.session.0), + &self.key_expr?, self.complete, self.origin, self.handler, diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index 5c7ad1dcae..ce8fa39dc7 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -273,9 +273,13 @@ impl<'a> Publisher<'a> { /// # } /// ``` #[zenoh_macros::unstable] - pub fn matching_listener(&self) -> MatchingListenerBuilder<'_, 'a, DefaultHandler> { + pub fn matching_listener(&self) -> MatchingListenerBuilder<'_, DefaultHandler> { MatchingListenerBuilder { - publisher: self, + session: &self.session, + key_expr: &self.key_expr, + destination: self.destination, + matching_listeners: &self.matching_listeners, + matching_status_type: MatchingStatusType::Subscribers, handler: DefaultHandler::default(), } } @@ -522,6 +526,7 @@ pub struct MatchingStatus { } #[cfg(feature = "unstable")] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum MatchingStatusType { Subscribers, Queryables(bool), @@ -555,15 +560,38 @@ pub(crate) struct MatchingListenerState { pub(crate) current: Mutex, pub(crate) key_expr: KeyExpr<'static>, pub(crate) destination: Locality, + pub(crate) match_type: MatchingStatusType, pub(crate) callback: Callback, } +impl MatchingListenerState { + pub(crate) fn is_matching(&self, key_expr: &KeyExpr, match_type: &MatchingStatusType) -> bool { + match match_type { + MatchingStatusType::Subscribers => { + self.match_type == MatchingStatusType::Subscribers + && self.key_expr.intersects(key_expr) + } + MatchingStatusType::Queryables(false) => { + self.match_type == MatchingStatusType::Queryables(false) + && self.key_expr.intersects(key_expr) + } + MatchingStatusType::Queryables(true) => { + (self.match_type == MatchingStatusType::Queryables(false) + && self.key_expr.intersects(key_expr)) + || (self.match_type == MatchingStatusType::Queryables(true) + && key_expr.includes(&self.key_expr)) + } + } + } +} + #[zenoh_macros::unstable] impl fmt::Debug for MatchingListenerState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("MatchingListener") .field("id", &self.id) .field("key_expr", &self.key_expr) + .field("match_type", &self.match_type) .finish() } } diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 39451ac592..e54b955b10 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -1,6 +1,8 @@ use core::fmt; use std::{ + collections::HashSet, future::{IntoFuture, Ready}, + sync::{Arc, Mutex}, time::Duration, }; @@ -24,6 +26,7 @@ use zenoh_result::ZResult; use { crate::api::publisher::{MatchingStatus, MatchingStatusType}, crate::api::sample::SourceInfo, + crate::pubsub::MatchingListenerBuilder, crate::query::ReplyKeyExpr, zenoh_config::wrappers::EntityGlobalId, zenoh_protocol::core::EntityGlobalIdProto, @@ -63,6 +66,8 @@ pub struct Querier<'a> { #[cfg(feature = "unstable")] pub(crate) accept_replies: ReplyKeyExpr, pub(crate) undeclare_on_drop: bool, + #[cfg(feature = "unstable")] + pub(crate) matching_listeners: Arc>>, } impl fmt::Debug for QuerierState { @@ -166,6 +171,13 @@ impl<'a> Querier<'a> { fn undeclare_impl(&mut self) -> ZResult<()> { // set the flag first to avoid double panic if this function panic self.undeclare_on_drop = false; + #[cfg(feature = "unstable")] + { + let ids: Vec = zlock!(self.matching_listeners).drain().collect(); + for id in ids { + self.session.undeclare_matches_listener_inner(id)? + } + } self.session.undeclare_querier_inner(self.id) } @@ -198,6 +210,42 @@ impl<'a> Querier<'a> { ) }) } + + /// Return a [`MatchingListener`] for this Publisher. + /// + /// The [`MatchingListener`] that will send a notification each time the [`MatchingStatus`] of + /// the Publisher changes. + /// + /// # Examples + /// ```no_run + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_querier("key/expression").await.unwrap(); + /// let matching_listener = querier.matching_listener().await.unwrap(); + /// while let Ok(matching_status) = matching_listener.recv_async().await { + /// if matching_status.matching() { + /// println!("Querier has matching queryables."); + /// } else { + /// println!("Querier has NO MORE matching queryables."); + /// } + /// } + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn matching_listener(&self) -> MatchingListenerBuilder<'_, DefaultHandler> { + MatchingListenerBuilder { + session: &self.session, + key_expr: &self.key_expr, + destination: self.destination, + matching_listeners: &self.matching_listeners, + matching_status_type: MatchingStatusType::Queryables( + self.target == QueryTarget::AllComplete, + ), + handler: DefaultHandler::default(), + } + } } impl<'a> UndeclarableSealed<()> for Querier<'a> { diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 2d10ac099d..39fa88d1fb 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -72,7 +72,6 @@ use crate::api::selector::ZenohParameters; #[cfg(feature = "unstable")] use crate::api::{ liveliness::{Liveliness, LivelinessTokenState}, - publisher::Publisher, publisher::{MatchingListenerState, MatchingStatus, MatchingStatusType}, query::{LivelinessQueryState, ReplyKeyExpr}, sample::SourceInfo, @@ -140,6 +139,8 @@ pub(crate) struct SessionState { pub(crate) liveliness_subscribers: HashMap>, pub(crate) queryables: HashMap>, #[cfg(feature = "unstable")] + pub(crate) remote_queryables: HashMap, bool)>, + #[cfg(feature = "unstable")] pub(crate) tokens: HashMap>, #[cfg(feature = "unstable")] pub(crate) matching_listeners: HashMap>, @@ -176,6 +177,8 @@ impl SessionState { liveliness_subscribers: HashMap::new(), queryables: HashMap::new(), #[cfg(feature = "unstable")] + remote_queryables: HashMap::new(), + #[cfg(feature = "unstable")] tokens: HashMap::new(), #[cfg(feature = "unstable")] matching_listeners: HashMap::new(), @@ -1549,7 +1552,12 @@ impl SessionInner { #[cfg(feature = "unstable")] { let state = zread!(self.state); - self.update_status_up(&state, &key_expr) + self.update_matching_status( + &state, + &key_expr, + MatchingStatusType::Subscribers, + true, + ) } } @@ -1606,7 +1614,12 @@ impl SessionInner { #[cfg(feature = "unstable")] { let state = zread!(self.state); - self.update_status_down(&state, &sub_state.key_expr) + self.update_matching_status( + &state, + &sub_state.key_expr, + MatchingStatusType::Subscribers, + false, + ) } } } else { @@ -1634,25 +1647,25 @@ impl SessionInner { } pub(crate) fn declare_queryable_inner( - &self, - key_expr: &WireExpr, + self: &Arc, + key_expr: &KeyExpr, complete: bool, origin: Locality, callback: Callback, ) -> ZResult> { + let wire_expr = key_expr.to_wire(self); let mut state = zwrite!(self.state); tracing::trace!("declare_queryable({:?})", key_expr); let id = self.runtime.next_id(); let qable_state = Arc::new(QueryableState { id, - key_expr: key_expr.to_owned(), + key_expr: wire_expr.to_owned(), complete, origin, callback, }); state.queryables.insert(id, qable_state.clone()); - if origin != Locality::SessionLocal { let primitives = state.primitives()?; drop(state); @@ -1667,15 +1680,29 @@ impl SessionInner { ext_nodeid: declare::ext::NodeIdType::DEFAULT, body: DeclareBody::DeclareQueryable(DeclareQueryable { id, - wire_expr: key_expr.to_owned(), + wire_expr: wire_expr.to_owned(), ext_info: qabl_info, }), }); + } else { + drop(state); + } + + #[cfg(feature = "unstable")] + { + let state = zread!(self.state); + self.update_matching_status( + &state, + key_expr, + MatchingStatusType::Queryables(complete), + true, + ) } + Ok(qable_state) } - pub(crate) fn close_queryable(&self, qid: Id) -> ZResult<()> { + pub(crate) fn close_queryable(self: &Arc, qid: Id) -> ZResult<()> { let mut state = zwrite!(self.state); let Ok(primitives) = state.primitives() else { return Ok(()); @@ -1696,6 +1723,18 @@ impl SessionInner { }, }), }); + } else { + drop(state); + } + #[cfg(feature = "unstable")] + { + let state = zread!(self.state); + self.update_matching_status( + &state, + &state.local_wireexpr_to_expr(&qable_state.key_expr)?, + MatchingStatusType::Queryables(qable_state.complete), + false, + ) } Ok(()) } else { @@ -1866,17 +1905,20 @@ impl SessionInner { #[zenoh_macros::unstable] pub(crate) fn declare_matches_listener_inner( &self, - publisher: &Publisher, + key_expr: &KeyExpr, + destination: Locality, + match_type: MatchingStatusType, callback: Callback, ) -> ZResult> { let mut state = zwrite!(self.state); let id = self.runtime.next_id(); - tracing::trace!("matches_listener({:?}) => {id}", publisher.key_expr); + tracing::trace!("matches_listener({:?}: {:?}) => {id}", match_type, key_expr); let listener_state = Arc::new(MatchingListenerState { id, current: std::sync::Mutex::new(false), - destination: publisher.destination, - key_expr: publisher.key_expr.clone().into_owned(), + destination, + key_expr: key_expr.clone().into_owned(), + match_type: match_type.clone(), callback, }); state.matching_listeners.insert(id, listener_state.clone()); @@ -1884,11 +1926,7 @@ impl SessionInner { match listener_state.current.lock() { Ok(mut current) => { if self - .matching_status( - &publisher.key_expr, - listener_state.destination, - MatchingStatusType::Subscribers, - ) + .matching_status(key_expr, listener_state.destination, match_type) .map(|s| s.matching()) .unwrap_or(true) { @@ -1948,66 +1986,33 @@ impl SessionInner { } #[zenoh_macros::unstable] - pub(crate) fn update_status_up(self: &Arc, state: &SessionState, key_expr: &KeyExpr) { - for msub in state.matching_listeners.values() { - if key_expr.intersects(&msub.key_expr) { - // Cannot hold session lock when calling tables (matching_status()) - // TODO: check which ZRuntime should be used - self.task_controller - .spawn_with_rt(zenoh_runtime::ZRuntime::Net, { - let session = WeakSession::new(self); - let msub = msub.clone(); - async move { - match msub.current.lock() { - Ok(mut current) => { - if !*current { - if let Ok(status) = session.matching_status( - &msub.key_expr, - msub.destination, - MatchingStatusType::Subscribers, - ) { - if status.matching() { - *current = true; - let callback = msub.callback.clone(); - callback.call(status) - } - } - } - } - Err(e) => { - tracing::error!( - "Error trying to acquire MathginListener lock: {}", - e - ); - } - } - } - }); - } - } - } - - #[zenoh_macros::unstable] - pub(crate) fn update_status_down(self: &Arc, state: &SessionState, key_expr: &KeyExpr) { + pub(crate) fn update_matching_status( + self: &Arc, + state: &SessionState, + key_expr: &KeyExpr, + match_type: MatchingStatusType, + status_value: bool, + ) { for msub in state.matching_listeners.values() { - if key_expr.intersects(&msub.key_expr) { + if msub.is_matching(key_expr, &match_type) { // Cannot hold session lock when calling tables (matching_status()) // TODO: check which ZRuntime should be used self.task_controller .spawn_with_rt(zenoh_runtime::ZRuntime::Net, { let session = WeakSession::new(self); let msub = msub.clone(); + let match_type = match_type.clone(); async move { match msub.current.lock() { Ok(mut current) => { - if *current { + if *current != status_value { if let Ok(status) = session.matching_status( &msub.key_expr, msub.destination, - MatchingStatusType::Subscribers, + match_type, ) { - if !status.matching() { - *current = false; + if status.matching() == status_value { + *current = status_value; let callback = msub.callback.clone(); callback.call(status) } @@ -2527,7 +2532,12 @@ impl Primitives for WeakSession { { Ok(expr) => { state.remote_subscribers.insert(m.id, expr.clone()); - self.update_status_up(&state, &expr); + self.update_matching_status( + &state, + &expr, + MatchingStatusType::Subscribers, + true, + ); } Err(err) => { tracing::error!( @@ -2547,7 +2557,12 @@ impl Primitives for WeakSession { return; // Session closing or closed } if let Some(expr) = state.remote_subscribers.remove(&m.id) { - self.update_status_down(&state, &expr); + self.update_matching_status( + &state, + &expr, + MatchingStatusType::Subscribers, + false, + ); } else { tracing::error!("Received Undeclare Subscriber for unknown id: {}", m.id); } @@ -2555,9 +2570,55 @@ impl Primitives for WeakSession { } zenoh_protocol::network::DeclareBody::DeclareQueryable(m) => { trace!("recv DeclareQueryable {} {:?}", m.id, m.wire_expr); + #[cfg(feature = "unstable")] + { + let mut state = zwrite!(self.state); + if state.primitives.is_none() { + return; // Session closing or closed + } + match state + .wireexpr_to_keyexpr(&m.wire_expr, false) + .map(|e| e.into_owned()) + { + Ok(expr) => { + state + .remote_queryables + .insert(m.id, (expr.clone(), m.ext_info.complete)); + self.update_matching_status( + &state, + &expr, + MatchingStatusType::Queryables(m.ext_info.complete), + true, + ); + } + Err(err) => { + tracing::error!( + "Received DeclareQueryable for unknown wire_expr: {}", + err + ) + } + } + } } zenoh_protocol::network::DeclareBody::UndeclareQueryable(m) => { trace!("recv UndeclareQueryable {:?}", m.id); + #[cfg(feature = "unstable")] + { + let mut state = zwrite!(self.state); + if state.primitives.is_none() { + return; // Session closing or closed + } + if let Some((expr, complete)) = state.remote_queryables.remove(&m.id) { + self.update_matching_status( + &state, + &expr, + MatchingStatusType::Queryables(complete), + false, + ); + } else { + tracing::error!("Received Undeclare Queryable for unknown id: {}", m.id); + } + } } #[cfg(not(feature = "unstable"))] zenoh_protocol::network::DeclareBody::DeclareToken(_) => {} diff --git a/zenoh/src/net/routing/hat/router/queries.rs b/zenoh/src/net/routing/hat/router/queries.rs index f3c336f6f8..12effbca65 100644 --- a/zenoh/src/net/routing/hat/router/queries.rs +++ b/zenoh/src/net/routing/hat/router/queries.rs @@ -1513,7 +1513,7 @@ impl HatQueriesTrait for HatCode { key_expr, complete ); - crate::net::routing::dispatcher::pubsub::get_matching_subscriptions(&tables, key_expr); + crate::net::routing::dispatcher::pubsub::get_matching_subscriptions(tables, key_expr); let res = Resource::get_resource(&tables.root_res, key_expr); let matches = res .as_ref() From 42495b71757c95774a0507e797d761067feda355 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 20:00:07 +0100 Subject: [PATCH 09/30] clippy fix --- zenoh/src/net/routing/hat/client/queries.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/zenoh/src/net/routing/hat/client/queries.rs b/zenoh/src/net/routing/hat/client/queries.rs index 9d666239e6..7f2f9cb8d9 100644 --- a/zenoh/src/net/routing/hat/client/queries.rs +++ b/zenoh/src/net/routing/hat/client/queries.rs @@ -472,15 +472,15 @@ impl HatQueriesTrait for HatCode { continue; } for (sid, context) in &mres.session_ctxs { - if context.face.whatami == WhatAmI::Client { - if match complete { + if context.face.whatami == WhatAmI::Client + && match complete { true => context.qabl.map_or(false, |q| q.complete), false => context.qabl.is_some(), - } { - matching_queryables - .entry(*sid) - .or_insert_with(|| context.face.clone()); } + { + matching_queryables + .entry(*sid) + .or_insert_with(|| context.face.clone()); } } } From 0dd0b1cb16891c9e017a213ba536125b74c4ea30 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 21:11:49 +0100 Subject: [PATCH 10/30] clippy fix --- zenoh/src/api/builders/matching_listener.rs | 10 ++++++---- zenoh/src/api/publisher.rs | 1 + zenoh/src/api/querier.rs | 4 ++-- zenoh/src/net/routing/dispatcher/queries.rs | 1 + zenoh/src/net/routing/hat/linkstate_peer/queries.rs | 2 ++ zenoh/src/net/routing/hat/router/queries.rs | 2 ++ 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index 2ef4d0afcb..13dd4e2ab0 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -13,12 +13,11 @@ // #[cfg(feature = "unstable")] use std::future::{IntoFuture, Ready}; -use std::{collections::HashSet, sync::Mutex}; +#[cfg(feature = "unstable")] use crate::{ - api::{publisher::MatchingStatusType, session::WeakSession, Id}, + api::session::WeakSession, key_expr::KeyExpr, - sample::Locality, }; #[cfg(feature = "unstable")] use zenoh_core::{Resolvable, Wait}; @@ -29,9 +28,12 @@ use zenoh_result::ZResult; use { crate::api::{ handlers::{Callback, DefaultHandler, IntoHandler}, - publisher::{MatchingListener, MatchingListenerInner, MatchingStatus}, + publisher::{MatchingListener, MatchingListenerInner, MatchingStatus, MatchingStatusType}, + Id }, + crate::sample::Locality, std::sync::Arc, + std::{collections::HashSet, sync::Mutex} }; /// A builder for initializing a [`MatchingListener`]. diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index ce8fa39dc7..d5a9cd7acb 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -564,6 +564,7 @@ pub(crate) struct MatchingListenerState { pub(crate) callback: Callback, } +#[cfg(feature = "unstable")] impl MatchingListenerState { pub(crate) fn is_matching(&self, key_expr: &KeyExpr, match_type: &MatchingStatusType) -> bool { match match_type { diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index e54b955b10..3db7c62f6f 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -1,8 +1,6 @@ use core::fmt; use std::{ - collections::HashSet, future::{IntoFuture, Ready}, - sync::{Arc, Mutex}, time::Duration, }; @@ -30,6 +28,8 @@ use { crate::query::ReplyKeyExpr, zenoh_config::wrappers::EntityGlobalId, zenoh_protocol::core::EntityGlobalIdProto, + std::sync::{Arc, Mutex}, + std::collections::HashSet, }; pub(crate) struct QuerierState { diff --git a/zenoh/src/net/routing/dispatcher/queries.rs b/zenoh/src/net/routing/dispatcher/queries.rs index 89dde9ab9a..b56c887b65 100644 --- a/zenoh/src/net/routing/dispatcher/queries.rs +++ b/zenoh/src/net/routing/dispatcher/queries.rs @@ -43,6 +43,7 @@ use super::{ resource::{QueryRoute, QueryRoutes, QueryTargetQablSet, Resource}, tables::{NodeId, RoutingExpr, Tables, TablesLock}, }; +#[cfg(feature = "unstable")] use crate::key_expr::KeyExpr; use crate::net::routing::hat::{HatTrait, SendDeclare}; diff --git a/zenoh/src/net/routing/hat/linkstate_peer/queries.rs b/zenoh/src/net/routing/hat/linkstate_peer/queries.rs index 4f2e061f57..2f7772193a 100644 --- a/zenoh/src/net/routing/hat/linkstate_peer/queries.rs +++ b/zenoh/src/net/routing/hat/linkstate_peer/queries.rs @@ -40,6 +40,7 @@ use super::{ face_hat, face_hat_mut, get_peer, get_routes_entries, hat, hat_mut, network::Network, res_hat, res_hat_mut, HatCode, HatContext, HatFace, HatTables, }; +#[cfg(feature = "unstable")] use crate::key_expr::KeyExpr; use crate::net::routing::{ dispatcher::{ @@ -1067,6 +1068,7 @@ impl HatQueriesTrait for HatCode { } } +#[cfg(feature = "unstable")] #[inline] fn insert_faces_for_qbls( route: &mut HashMap>, diff --git a/zenoh/src/net/routing/hat/router/queries.rs b/zenoh/src/net/routing/hat/router/queries.rs index 12effbca65..8180a15c1b 100644 --- a/zenoh/src/net/routing/hat/router/queries.rs +++ b/zenoh/src/net/routing/hat/router/queries.rs @@ -53,6 +53,7 @@ use crate::net::routing::{ RoutingContext, }; +#[cfg(feature = "unstable")] use crate::key_expr::KeyExpr; #[inline] @@ -1571,6 +1572,7 @@ impl HatQueriesTrait for HatCode { } } +#[cfg(feature = "unstable")] #[inline] fn insert_faces_for_qbls( route: &mut HashMap>, From 352a0fb7bc90a63f99a5df79a7de62b76a8b1737 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 22:45:45 +0100 Subject: [PATCH 11/30] clippy fix --- zenoh/src/api/builders/matching_listener.rs | 9 +++------ zenoh/src/api/querier.rs | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index 13dd4e2ab0..25a3f4c06a 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -15,10 +15,7 @@ use std::future::{IntoFuture, Ready}; #[cfg(feature = "unstable")] -use crate::{ - api::session::WeakSession, - key_expr::KeyExpr, -}; +use crate::{api::session::WeakSession, key_expr::KeyExpr}; #[cfg(feature = "unstable")] use zenoh_core::{Resolvable, Wait}; #[cfg(feature = "unstable")] @@ -29,11 +26,11 @@ use { crate::api::{ handlers::{Callback, DefaultHandler, IntoHandler}, publisher::{MatchingListener, MatchingListenerInner, MatchingStatus, MatchingStatusType}, - Id + Id, }, crate::sample::Locality, std::sync::Arc, - std::{collections::HashSet, sync::Mutex} + std::{collections::HashSet, sync::Mutex}, }; /// A builder for initializing a [`MatchingListener`]. diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 3db7c62f6f..92184cd032 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -26,10 +26,10 @@ use { crate::api::sample::SourceInfo, crate::pubsub::MatchingListenerBuilder, crate::query::ReplyKeyExpr, + std::collections::HashSet, + std::sync::{Arc, Mutex}, zenoh_config::wrappers::EntityGlobalId, zenoh_protocol::core::EntityGlobalIdProto, - std::sync::{Arc, Mutex}, - std::collections::HashSet, }; pub(crate) struct QuerierState { From 5a3a7c29920601f5c09fbf9c48e96932dfc9de2f Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 23:10:29 +0100 Subject: [PATCH 12/30] clippy and fmt fix --- zenoh/src/api/builders/matching_listener.rs | 6 +++--- zenoh/src/api/publisher.rs | 4 ++-- zenoh/src/api/querier.rs | 19 ++++++++++--------- zenoh/src/api/session.rs | 5 ++--- zenoh/src/net/routing/hat/router/queries.rs | 5 ++--- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index 25a3f4c06a..4671a1e18e 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -14,13 +14,10 @@ #[cfg(feature = "unstable")] use std::future::{IntoFuture, Ready}; -#[cfg(feature = "unstable")] -use crate::{api::session::WeakSession, key_expr::KeyExpr}; #[cfg(feature = "unstable")] use zenoh_core::{Resolvable, Wait}; #[cfg(feature = "unstable")] use zenoh_result::ZResult; - #[cfg(feature = "unstable")] use { crate::api::{ @@ -33,6 +30,9 @@ use { std::{collections::HashSet, sync::Mutex}, }; +#[cfg(feature = "unstable")] +use crate::{api::session::WeakSession, key_expr::KeyExpr}; + /// A builder for initializing a [`MatchingListener`]. #[zenoh_macros::unstable] #[derive(Debug)] diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index d5a9cd7acb..51b8a342f2 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -526,7 +526,7 @@ pub struct MatchingStatus { } #[cfg(feature = "unstable")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub(crate) enum MatchingStatusType { Subscribers, Queryables(bool), @@ -566,7 +566,7 @@ pub(crate) struct MatchingListenerState { #[cfg(feature = "unstable")] impl MatchingListenerState { - pub(crate) fn is_matching(&self, key_expr: &KeyExpr, match_type: &MatchingStatusType) -> bool { + pub(crate) fn is_matching(&self, key_expr: &KeyExpr, match_type: MatchingStatusType) -> bool { match match_type { MatchingStatusType::Subscribers => { self.match_type == MatchingStatusType::Subscribers diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 92184cd032..6d6e4c897b 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -4,15 +4,6 @@ use std::{ time::Duration, }; -use super::{ - builders::querier::QuerierGetBuilder, - key_expr::KeyExpr, - query::QueryConsolidation, - sample::{Locality, QoS}, - session::{UndeclarableSealed, WeakSession}, - Id, -}; -use crate::{api::handlers::DefaultHandler, qos::Priority}; use tracing::error; use zenoh_core::{Resolvable, Resolve, Wait}; use zenoh_protocol::{ @@ -32,6 +23,16 @@ use { zenoh_protocol::core::EntityGlobalIdProto, }; +use super::{ + builders::querier::QuerierGetBuilder, + key_expr::KeyExpr, + query::QueryConsolidation, + sample::{Locality, QoS}, + session::{UndeclarableSealed, WeakSession}, + Id, +}; +use crate::{api::handlers::DefaultHandler, qos::Priority}; + pub(crate) struct QuerierState { pub(crate) id: Id, pub(crate) remote_id: Id, diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 39fa88d1fb..9755727573 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -1918,7 +1918,7 @@ impl SessionInner { current: std::sync::Mutex::new(false), destination, key_expr: key_expr.clone().into_owned(), - match_type: match_type.clone(), + match_type, callback, }); state.matching_listeners.insert(id, listener_state.clone()); @@ -1994,14 +1994,13 @@ impl SessionInner { status_value: bool, ) { for msub in state.matching_listeners.values() { - if msub.is_matching(key_expr, &match_type) { + if msub.is_matching(key_expr, match_type) { // Cannot hold session lock when calling tables (matching_status()) // TODO: check which ZRuntime should be used self.task_controller .spawn_with_rt(zenoh_runtime::ZRuntime::Net, { let session = WeakSession::new(self); let msub = msub.clone(); - let match_type = match_type.clone(); async move { match msub.current.lock() { Ok(mut current) => { diff --git a/zenoh/src/net/routing/hat/router/queries.rs b/zenoh/src/net/routing/hat/router/queries.rs index 8180a15c1b..6739f22bb9 100644 --- a/zenoh/src/net/routing/hat/router/queries.rs +++ b/zenoh/src/net/routing/hat/router/queries.rs @@ -41,6 +41,8 @@ use super::{ interests::push_declaration_profile, network::Network, res_hat, res_hat_mut, HatCode, HatContext, HatFace, HatTables, }; +#[cfg(feature = "unstable")] +use crate::key_expr::KeyExpr; use crate::net::routing::{ dispatcher::{ face::FaceState, @@ -53,9 +55,6 @@ use crate::net::routing::{ RoutingContext, }; -#[cfg(feature = "unstable")] -use crate::key_expr::KeyExpr; - #[inline] fn merge_qabl_infos(mut this: QueryableInfoType, info: &QueryableInfoType) -> QueryableInfoType { this.complete = this.complete || info.complete; From 70b855bc20733127ee0294068f6f8aad6e035c1b Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 23:30:10 +0100 Subject: [PATCH 13/30] doc test fix --- zenoh/src/api/querier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 6d6e4c897b..1edf87d493 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -223,7 +223,7 @@ impl<'a> Querier<'a> { /// # async fn main() { /// /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let publisher = session.declare_querier("key/expression").await.unwrap(); + /// let querier = session.declare_querier("key/expression").await.unwrap(); /// let matching_listener = querier.matching_listener().await.unwrap(); /// while let Ok(matching_status) = matching_listener.recv_async().await { /// if matching_status.matching() { From be22f2c3c693cb502dfd41919f6e37be1b50d7d7 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 21 Nov 2024 23:49:23 +0100 Subject: [PATCH 14/30] docs fix --- zenoh/src/api/querier.rs | 6 +++--- zenoh/src/api/session.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 1edf87d493..429379495b 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -212,10 +212,10 @@ impl<'a> Querier<'a> { }) } - /// Return a [`MatchingListener`] for this Publisher. + /// Return a [`MatchingListener`](crate::api::publisher::MatchingListener) for this Querier. /// - /// The [`MatchingListener`] that will send a notification each time the [`MatchingStatus`] of - /// the Publisher changes. + /// The [`MatchingListener`](crate::api::publisher::MatchingListener) that will send a notification each time the [`MatchingStatus`](crate::api::publisher::MatchingStatus) of + /// the Querier changes. /// /// # Examples /// ```no_run diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 9755727573..f79b1f389b 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -579,7 +579,7 @@ impl Drop for WeakSession { /// Error indicating the operation cannot proceed because the session is closed. /// -/// It may be returned by operations like [`Session::get`] or [`Publisher::put`] when +/// It may be returned by operations like [`Session::get`] or [`Publisher::put`](crate::api::publisher::Publisher::put) when /// [`Session::close`] has been called before. #[derive(Debug)] pub struct SessionClosedError; From 39aa04ac135b61600cdfdd8e8d7310a78f577f6a Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 22 Nov 2024 00:05:46 +0100 Subject: [PATCH 15/30] fix MatchingStatus/Listener to work on session-local entities with origin=Locality::SessionLocal --- zenoh/src/api/session.rs | 284 ++++++++++++++++++++++++--------------- zenoh/tests/matching.rs | 47 +++++++ 2 files changed, 222 insertions(+), 109 deletions(-) diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index f79b1f389b..b9df96c6b7 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -358,6 +358,88 @@ impl SessionState { self.queriers.insert(id, querier_state); declared_querier } + + fn register_subscriber<'a>( + &mut self, + id: EntityId, + key_expr: &'a KeyExpr, + origin: Locality, + callback: Callback, + ) -> (Arc, Option>) { + let mut sub_state = SubscriberState { + id, + remote_id: id, + key_expr: key_expr.clone().into_owned(), + origin, + callback, + }; + + let declared_sub = origin != Locality::SessionLocal; + + let declared_sub = declared_sub + .then(|| { + match self + .aggregated_subscribers + .iter() + .find(|s| s.includes(key_expr)) + { + Some(join_sub) => { + if let Some(joined_sub) = self + .subscribers(SubscriberKind::Subscriber) + .values() + .find(|s| { + s.origin != Locality::SessionLocal && join_sub.includes(&s.key_expr) + }) + { + sub_state.remote_id = joined_sub.remote_id; + None + } else { + Some(join_sub.clone().into()) + } + } + None => { + if let Some(twin_sub) = self + .subscribers(SubscriberKind::Subscriber) + .values() + .find(|s| s.origin != Locality::SessionLocal && s.key_expr == *key_expr) + { + sub_state.remote_id = twin_sub.remote_id; + None + } else { + Some(key_expr.clone()) + } + } + } + }) + .flatten(); + + let sub_state = Arc::new(sub_state); + + self.subscribers_mut(SubscriberKind::Subscriber) + .insert(sub_state.id, sub_state.clone()); + for res in self + .local_resources + .values_mut() + .filter_map(Resource::as_node_mut) + { + if key_expr.intersects(&res.key_expr) { + res.subscribers_mut(SubscriberKind::Subscriber) + .push(sub_state.clone()); + } + } + for res in self + .remote_resources + .values_mut() + .filter_map(Resource::as_node_mut) + { + if key_expr.intersects(&res.key_expr) { + res.subscribers_mut(SubscriberKind::Subscriber) + .push(sub_state.clone()); + } + } + + (sub_state, declared_sub) + } } impl fmt::Debug for SessionState { @@ -1438,80 +1520,7 @@ impl SessionInner { let mut state = zwrite!(self.state); tracing::trace!("declare_subscriber({:?})", key_expr); let id = self.runtime.next_id(); - - let mut sub_state = SubscriberState { - id, - remote_id: id, - key_expr: key_expr.clone().into_owned(), - origin, - callback, - }; - - let declared_sub = origin != Locality::SessionLocal; - - let declared_sub = declared_sub - .then(|| { - match state - .aggregated_subscribers - .iter() - .find(|s| s.includes(key_expr)) - { - Some(join_sub) => { - if let Some(joined_sub) = state - .subscribers(SubscriberKind::Subscriber) - .values() - .find(|s| { - s.origin != Locality::SessionLocal && join_sub.includes(&s.key_expr) - }) - { - sub_state.remote_id = joined_sub.remote_id; - None - } else { - Some(join_sub.clone().into()) - } - } - None => { - if let Some(twin_sub) = state - .subscribers(SubscriberKind::Subscriber) - .values() - .find(|s| s.origin != Locality::SessionLocal && s.key_expr == *key_expr) - { - sub_state.remote_id = twin_sub.remote_id; - None - } else { - Some(key_expr.clone()) - } - } - } - }) - .flatten(); - - let sub_state = Arc::new(sub_state); - - state - .subscribers_mut(SubscriberKind::Subscriber) - .insert(sub_state.id, sub_state.clone()); - for res in state - .local_resources - .values_mut() - .filter_map(Resource::as_node_mut) - { - if key_expr.intersects(&res.key_expr) { - res.subscribers_mut(SubscriberKind::Subscriber) - .push(sub_state.clone()); - } - } - for res in state - .remote_resources - .values_mut() - .filter_map(Resource::as_node_mut) - { - if key_expr.intersects(&res.key_expr) { - res.subscribers_mut(SubscriberKind::Subscriber) - .push(sub_state.clone()); - } - } - + let (sub_state, declared_sub) = state.register_subscriber(id, key_expr, origin, callback); if let Some(key_expr) = declared_sub { let primitives = state.primitives()?; drop(state); @@ -1548,7 +1557,6 @@ impl SessionInner { wire_expr: key_expr.to_wire(self).to_owned(), }), }); - #[cfg(feature = "unstable")] { let state = zread!(self.state); @@ -1559,6 +1567,9 @@ impl SessionInner { true, ) } + } else if origin == Locality::SessionLocal { + #[cfg(feature = "unstable")] + self.update_matching_status(&state, &key_expr, MatchingStatusType::Subscribers, true) } Ok(sub_state) @@ -1592,25 +1603,31 @@ impl SessionInner { .retain(|sub| sub.id != sub_state.id); } - if sub_state.origin != Locality::SessionLocal && kind == SubscriberKind::Subscriber { - // Note: there might be several Subscribers on the same KeyExpr. - // Before calling forget_subscriber(key_expr), check if this was the last one. - if !state.subscribers(kind).values().any(|s| { - s.origin != Locality::SessionLocal && s.remote_id == sub_state.remote_id - }) { - drop(state); - primitives.send_declare(Declare { - interest_id: None, - ext_qos: declare::ext::QoSType::DECLARE, - ext_tstamp: None, - ext_nodeid: declare::ext::NodeIdType::DEFAULT, - body: DeclareBody::UndeclareSubscriber(UndeclareSubscriber { - id: sub_state.remote_id, - ext_wire_expr: WireExprType { - wire_expr: WireExpr::empty(), - }, - }), - }); + match kind { + SubscriberKind::Subscriber => { + if sub_state.origin != Locality::SessionLocal { + // Note: there might be several Subscribers on the same KeyExpr. + // Before calling forget_subscriber(key_expr), check if this was the last one. + if !state.subscribers(kind).values().any(|s| { + s.origin != Locality::SessionLocal && s.remote_id == sub_state.remote_id + }) { + drop(state); + primitives.send_declare(Declare { + interest_id: None, + ext_qos: declare::ext::QoSType::DECLARE, + ext_tstamp: None, + ext_nodeid: declare::ext::NodeIdType::DEFAULT, + body: DeclareBody::UndeclareSubscriber(UndeclareSubscriber { + id: sub_state.remote_id, + ext_wire_expr: WireExprType { + wire_expr: WireExpr::empty(), + }, + }), + }); + } + } else { + drop(state); + } #[cfg(feature = "unstable")] { let state = zread!(self.state); @@ -1622,21 +1639,22 @@ impl SessionInner { ) } } - } else { - #[cfg(feature = "unstable")] - if kind == SubscriberKind::LivelinessSubscriber { - let primitives = state.primitives()?; - drop(state); + SubscriberKind::LivelinessSubscriber => { + #[cfg(feature = "unstable")] + if kind == SubscriberKind::LivelinessSubscriber { + let primitives = state.primitives()?; + drop(state); - primitives.send_interest(Interest { - id: sub_state.id, - mode: InterestMode::Final, - options: InterestOptions::empty(), - wire_expr: None, - ext_qos: declare::ext::QoSType::DEFAULT, - ext_tstamp: None, - ext_nodeid: declare::ext::NodeIdType::DEFAULT, - }); + primitives.send_interest(Interest { + id: sub_state.id, + mode: InterestMode::Final, + options: InterestOptions::empty(), + wire_expr: None, + ext_qos: declare::ext::QoSType::DEFAULT, + ext_tstamp: None, + ext_nodeid: declare::ext::NodeIdType::DEFAULT, + }); + } } } @@ -1942,7 +1960,34 @@ impl SessionInner { } #[zenoh_macros::unstable] - pub(crate) fn matching_status( + fn matching_status_local( + &self, + key_expr: &KeyExpr, + matching_type: MatchingStatusType, + ) -> MatchingStatus { + let state = zread!(self.state); + let matching = match matching_type { + MatchingStatusType::Subscribers => state + .subscribers(SubscriberKind::Subscriber) + .values() + .any(|s| s.key_expr.intersects(key_expr)), + MatchingStatusType::Queryables(false) => state.queryables.values().any(|q| { + state + .local_wireexpr_to_expr(&q.key_expr) + .map_or(false, |ke| ke.intersects(key_expr)) + }), + MatchingStatusType::Queryables(true) => state.queryables.values().any(|q| { + q.complete + && state + .local_wireexpr_to_expr(&q.key_expr) + .map_or(false, |ke| ke.includes(key_expr)) + }), + }; + MatchingStatus { matching } + } + + #[zenoh_macros::unstable] + fn matching_status_remote( &self, key_expr: &KeyExpr, destination: Locality, @@ -1985,6 +2030,27 @@ impl SessionInner { Ok(MatchingStatus { matching }) } + #[zenoh_macros::unstable] + pub(crate) fn matching_status( + &self, + key_expr: &KeyExpr, + destination: Locality, + matching_type: MatchingStatusType, + ) -> ZResult { + match destination { + Locality::SessionLocal => Ok(self.matching_status_local(key_expr, matching_type)), + Locality::Remote => self.matching_status_remote(key_expr, destination, matching_type), + Locality::Any => { + let local_match = self.matching_status_local(key_expr, matching_type); + if local_match.matching() { + Ok(local_match) + } else { + self.matching_status_remote(key_expr, destination, matching_type) + } + } + } + } + #[zenoh_macros::unstable] pub(crate) fn update_matching_status( self: &Arc, diff --git a/zenoh/tests/matching.rs b/zenoh/tests/matching.rs index d92148ffde..1a4c03bd56 100644 --- a/zenoh/tests/matching.rs +++ b/zenoh/tests/matching.rs @@ -238,3 +238,50 @@ async fn zenoh_matching_status_local() -> ZResult<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn zenoh_matching_status_session_local() -> ZResult<()> { + zenoh_util::init_log_from_env_or("error"); + + let session = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); + + let publisher = + ztimeout!(session.declare_publisher("zenoh_matching_status_session_local_test")).unwrap(); + + let matching_listener = ztimeout!(publisher.matching_listener()).unwrap(); + + let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); + assert!(received_status.unwrap().is_none()); + + let matching_status = ztimeout!(publisher.matching_status()).unwrap(); + assert!(!matching_status.matching()); + + let sub = ztimeout!(session + .declare_subscriber("zenoh_matching_status_session_local_test") + .allowed_origin(Locality::SessionLocal)) + .unwrap(); + + let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); + assert!(received_status + .ok() + .flatten() + .map(|s| s.matching()) + .eq(&Some(true))); + + let matching_status = ztimeout!(publisher.matching_status()).unwrap(); + assert!(matching_status.matching()); + + ztimeout!(sub.undeclare()).unwrap(); + + let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); + assert!(received_status + .ok() + .flatten() + .map(|s| s.matching()) + .eq(&Some(false))); + + let matching_status = ztimeout!(publisher.matching_status()).unwrap(); + assert!(!matching_status.matching()); + + Ok(()) +} From 93685fe4e738602ac069309e45171b10c3de8a03 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 22 Nov 2024 00:15:36 +0100 Subject: [PATCH 16/30] Merge branch 'main' into querier --- Cargo.lock | 3 + Cargo.toml | 1 + DEFAULT_CONFIG.json5 | 5 + README.md | 2 +- commons/zenoh-config/src/lib.rs | 4 + io/zenoh-link-commons/Cargo.toml | 1 + io/zenoh-link-commons/src/tls.rs | 121 +++ io/zenoh-links/zenoh-link-quic/Cargo.toml | 1 + io/zenoh-links/zenoh-link-quic/src/lib.rs | 4 + io/zenoh-links/zenoh-link-quic/src/unicast.rs | 163 +++- io/zenoh-links/zenoh-link-quic/src/utils.rs | 62 +- io/zenoh-links/zenoh-link-tls/Cargo.toml | 1 + io/zenoh-links/zenoh-link-tls/src/lib.rs | 4 + io/zenoh-links/zenoh-link-tls/src/unicast.rs | 135 +++- io/zenoh-links/zenoh-link-tls/src/utils.rs | 58 +- zenoh/src/api/session.rs | 8 +- zenoh/src/net/routing/dispatcher/token.rs | 3 +- zenoh/src/net/routing/hat/client/interests.rs | 4 +- .../src/net/routing/hat/p2p_peer/interests.rs | 4 +- .../net/routing/interceptor/access_control.rs | 164 +++- .../net/routing/interceptor/authorization.rs | 9 + zenoh/tests/acl.rs | 733 +++++++++++++++++- zenoh/tests/liveliness.rs | 64 +- 23 files changed, 1415 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3fec7718ce..317af86fff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5664,6 +5664,7 @@ dependencies = [ "rustls", "rustls-webpki", "serde", + "time 0.3.36", "tokio", "tokio-util", "tracing", @@ -5688,6 +5689,7 @@ dependencies = [ "rustls-pki-types", "rustls-webpki", "secrecy", + "time 0.3.36", "tokio", "tokio-util", "tracing", @@ -5745,6 +5747,7 @@ dependencies = [ "rustls-webpki", "secrecy", "socket2 0.5.7", + "time 0.3.36", "tls-listener", "tokio", "tokio-rustls", diff --git a/Cargo.toml b/Cargo.toml index 721d1bced1..b5853f4897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,6 +167,7 @@ socket2 = { version = "0.5.7", features = ["all"] } stop-token = "0.7.0" syn = "2.0" tide = "0.16.0" +time = "0.3.36" token-cell = { version = "1.5.0", default-features = false } tokio = { version = "1.40.0", default-features = false } # Default features are disabled due to some crates' requirements tokio-util = "0.7.12" diff --git a/DEFAULT_CONFIG.json5 b/DEFAULT_CONFIG.json5 index bb083a7e31..7a3e614635 100644 --- a/DEFAULT_CONFIG.json5 +++ b/DEFAULT_CONFIG.json5 @@ -225,6 +225,7 @@ // "messages": [ // "put", "delete", "declare_subscriber", // "query", "reply", "declare_queryable", + // "liveliness_token", "liveliness_query", "declare_liveliness_subscriber", // ], // "flows":["egress","ingress"], // "permission": "allow", @@ -466,6 +467,10 @@ // This could be dangerous because your CA can have signed a server cert for foo.com, that's later being used to host a server at baz.com. If you wan't your // ca to verify that the server at baz.com is actually baz.com, let this be true (default). verify_name_on_connect: true, + // Whether or not to close links when remote certificates expires. + // If set to true, links that require certificates (tls/quic) will automatically disconnect when the time of expiration of the remote certificate chain is reached + // note that mTLS (client authentication) is required for a listener to disconnect a client on expiration + close_link_on_expiration: false, }, }, /// Shared memory configuration. diff --git a/README.md b/README.md index 2b44edba95..8020c2d0b2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![CI](https://github.com/eclipse-zenoh/zenoh/workflows/CI/badge.svg)](https://github.com/eclipse-zenoh/zenoh/actions?query=workflow%3A%22CI%22) +[![CI](https://github.com/eclipse-zenoh/zenoh/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/eclipse-zenoh/zenoh/actions?query=workflow%3ACI+branch%3Amain++) [![Documentation Status](https://readthedocs.org/projects/zenoh-rust/badge/?version=latest)](https://zenoh-rust.readthedocs.io/en/latest/?badge=latest) [![Discussion](https://img.shields.io/badge/discussion-on%20github-blue)](https://github.com/eclipse-zenoh/roadmap/discussions) [![Discord](https://img.shields.io/badge/chat-on%20discord-blue)](https://discord.gg/2GJ958VuHs) diff --git a/commons/zenoh-config/src/lib.rs b/commons/zenoh-config/src/lib.rs index ef09f6b3e9..b8316cc0f1 100644 --- a/commons/zenoh-config/src/lib.rs +++ b/commons/zenoh-config/src/lib.rs @@ -170,6 +170,9 @@ pub enum AclMessage { Query, DeclareQueryable, Reply, + LivelinessToken, + DeclareLivelinessSubscriber, + LivelinessQuery, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, Hash, PartialEq)] @@ -485,6 +488,7 @@ validated_struct::validator! { connect_private_key: Option, connect_certificate: Option, verify_name_on_connect: Option, + close_link_on_expiration: Option, // Skip serializing field because they contain secrets #[serde(skip_serializing)] root_ca_certificate_base64: Option, diff --git a/io/zenoh-link-commons/Cargo.toml b/io/zenoh-link-commons/Cargo.toml index 5f8f91aa61..a1bec81cbd 100644 --- a/io/zenoh-link-commons/Cargo.toml +++ b/io/zenoh-link-commons/Cargo.toml @@ -35,6 +35,7 @@ futures = { workspace = true } rustls = { workspace = true, optional = true } rustls-webpki = { workspace = true, optional = true } serde = { workspace = true, features = ["default"] } +time = { workspace = true } tokio = { workspace = true, features = [ "fs", "io-util", diff --git a/io/zenoh-link-commons/src/tls.rs b/io/zenoh-link-commons/src/tls.rs index 427880b812..4473607a0a 100644 --- a/io/zenoh-link-commons/src/tls.rs +++ b/io/zenoh-link-commons/src/tls.rs @@ -86,3 +86,124 @@ impl WebPkiVerifierAnyServerName { Self { roots } } } + +pub mod expiration { + use std::{ + net::SocketAddr, + sync::{atomic::AtomicBool, Weak}, + }; + + use async_trait::async_trait; + use time::OffsetDateTime; + use tokio::{sync::Mutex as AsyncMutex, task::JoinHandle}; + use tokio_util::sync::CancellationToken; + use zenoh_result::ZResult; + + #[async_trait] + pub trait LinkWithCertExpiration: Send + Sync { + async fn expire(&self) -> ZResult<()>; + } + + #[derive(Debug)] + pub struct LinkCertExpirationManager { + token: CancellationToken, + handle: AsyncMutex>>>, + /// Closing the link is a critical section that requires exclusive access to expiration_task + /// or the transport. `link_closing` is used to synchronize the access to this operation. + link_closing: AtomicBool, + } + + impl LinkCertExpirationManager { + pub fn new( + link: Weak, + src_addr: SocketAddr, + dst_addr: SocketAddr, + link_type: &'static str, + expiration_time: OffsetDateTime, + ) -> Self { + let token = CancellationToken::new(); + let handle = zenoh_runtime::ZRuntime::Acceptor.spawn(expiration_task( + link, + src_addr, + dst_addr, + link_type, + expiration_time, + token.clone(), + )); + Self { + token, + handle: AsyncMutex::new(Some(handle)), + link_closing: AtomicBool::new(false), + } + } + + /// Takes exclusive access to closing the link. + /// + /// Returns `true` if successful, `false` if another task is (or finished) closing the link. + pub fn set_closing(&self) -> bool { + !self + .link_closing + .swap(true, std::sync::atomic::Ordering::Relaxed) + } + + /// Sends cancelation signal to expiration_task + pub fn cancel_expiration_task(&self) { + self.token.cancel() + } + + /// Waits for expiration task to complete, returning its return value. + pub async fn wait_for_expiration_task(&self) -> ZResult<()> { + let mut lock = self.handle.lock().await; + let handle = lock.take().expect("handle should be set"); + handle.await? + } + } + + async fn expiration_task( + link: Weak, + src_addr: SocketAddr, + dst_addr: SocketAddr, + link_type: &'static str, + expiration_time: OffsetDateTime, + token: CancellationToken, + ) -> ZResult<()> { + tracing::trace!( + "Expiration task started for {} link {:?} => {:?}", + link_type.to_uppercase(), + src_addr, + dst_addr, + ); + tokio::select! { + _ = token.cancelled() => {}, + _ = sleep_until_date(expiration_time) => { + // expire the link + if let Some(link) = link.upgrade() { + tracing::warn!( + "Closing {} link {:?} => {:?} : remote certificate chain expired", + link_type.to_uppercase(), + src_addr, + dst_addr, + ); + return link.expire().await; + } + }, + } + Ok(()) + } + + async fn sleep_until_date(wakeup_time: OffsetDateTime) { + const MAX_SLEEP_DURATION: tokio::time::Duration = tokio::time::Duration::from_secs(600); + loop { + let now = OffsetDateTime::now_utc(); + if wakeup_time <= now { + break; + } + // next sleep duration is the minimum between MAX_SLEEP_DURATION and the duration till wakeup + // this mitigates the unsoundness of using `tokio::time::sleep` with long durations + let wakeup_duration = std::time::Duration::try_from(wakeup_time - now) + .expect("wakeup_time should be greater than now"); + let sleep_duration = tokio::time::Duration::min(MAX_SLEEP_DURATION, wakeup_duration); + tokio::time::sleep(sleep_duration).await; + } + } +} diff --git a/io/zenoh-links/zenoh-link-quic/Cargo.toml b/io/zenoh-links/zenoh-link-quic/Cargo.toml index b01f5e4261..6132abeaca 100644 --- a/io/zenoh-links/zenoh-link-quic/Cargo.toml +++ b/io/zenoh-links/zenoh-link-quic/Cargo.toml @@ -33,6 +33,7 @@ rustls-pemfile = { workspace = true } rustls-pki-types = { workspace = true } rustls-webpki = { workspace = true } secrecy = { workspace = true } +time = { workspace = true } tokio = { workspace = true, features = [ "fs", "io-util", diff --git a/io/zenoh-links/zenoh-link-quic/src/lib.rs b/io/zenoh-links/zenoh-link-quic/src/lib.rs index abaefd199c..70bf00d1ec 100644 --- a/io/zenoh-links/zenoh-link-quic/src/lib.rs +++ b/io/zenoh-links/zenoh-link-quic/src/lib.rs @@ -109,7 +109,11 @@ pub mod config { pub const TLS_CONNECT_CERTIFICATE_BASE64: &str = "connect_certificate_base64"; pub const TLS_ENABLE_MTLS: &str = "enable_mtls"; + pub const TLS_ENABLE_MTLS_DEFAULT: bool = false; pub const TLS_VERIFY_NAME_ON_CONNECT: &str = "verify_name_on_connect"; pub const TLS_VERIFY_NAME_ON_CONNECT_DEFAULT: bool = true; + + pub const TLS_CLOSE_LINK_ON_EXPIRATION: &str = "close_link_on_expiration"; + pub const TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT: bool = false; } diff --git a/io/zenoh-links/zenoh-link-quic/src/unicast.rs b/io/zenoh-links/zenoh-link-quic/src/unicast.rs index ea6ce646cc..3618a7a625 100644 --- a/io/zenoh-links/zenoh-link-quic/src/unicast.rs +++ b/io/zenoh-links/zenoh-link-quic/src/unicast.rs @@ -21,13 +21,16 @@ use std::{ use async_trait::async_trait; use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; +use time::OffsetDateTime; use tokio::sync::Mutex as AsyncMutex; use tokio_util::sync::CancellationToken; -use x509_parser::prelude::*; +use x509_parser::prelude::{FromDer, X509Certificate}; use zenoh_core::zasynclock; use zenoh_link_commons::{ - get_ip_interface_names, LinkAuthId, LinkAuthType, LinkManagerUnicastTrait, LinkUnicast, - LinkUnicastTrait, ListenersUnicastIP, NewLinkChannelSender, + get_ip_interface_names, + tls::expiration::{LinkCertExpirationManager, LinkWithCertExpiration}, + LinkAuthId, LinkAuthType, LinkManagerUnicastTrait, LinkUnicast, LinkUnicastTrait, + ListenersUnicastIP, NewLinkChannelSender, }; use zenoh_protocol::{ core::{EndPoint, Locator}, @@ -48,6 +51,7 @@ pub struct LinkUnicastQuic { send: AsyncMutex, recv: AsyncMutex, auth_identifier: LinkAuthId, + expiration_manager: Option, } impl LinkUnicastQuic { @@ -58,6 +62,7 @@ impl LinkUnicastQuic { send: quinn::SendStream, recv: quinn::RecvStream, auth_identifier: LinkAuthId, + expiration_manager: Option, ) -> LinkUnicastQuic { // Build the Quic object LinkUnicastQuic { @@ -68,12 +73,10 @@ impl LinkUnicastQuic { send: AsyncMutex::new(send), recv: AsyncMutex::new(recv), auth_identifier, + expiration_manager, } } -} -#[async_trait] -impl LinkUnicastTrait for LinkUnicastQuic { async fn close(&self) -> ZResult<()> { tracing::trace!("Closing QUIC link: {}", self); // Flush the QUIC stream @@ -84,6 +87,24 @@ impl LinkUnicastTrait for LinkUnicastQuic { self.connection.close(quinn::VarInt::from_u32(0), &[0]); Ok(()) } +} + +#[async_trait] +impl LinkUnicastTrait for LinkUnicastQuic { + async fn close(&self) -> ZResult<()> { + if let Some(expiration_manager) = &self.expiration_manager { + if !expiration_manager.set_closing() { + // expiration_task is closing link, return its returned ZResult to Transport + return expiration_manager.wait_for_expiration_task().await; + } + // cancel the expiration task and close link + expiration_manager.cancel_expiration_task(); + let res = self.close().await; + let _ = expiration_manager.wait_for_expiration_task().await; + return res; + } + self.close().await + } async fn write(&self, buffer: &[u8]) -> ZResult { let mut guard = zasynclock!(self.send); @@ -167,6 +188,21 @@ impl LinkUnicastTrait for LinkUnicastQuic { } } +#[async_trait] +impl LinkWithCertExpiration for LinkUnicastQuic { + async fn expire(&self) -> ZResult<()> { + let expiration_manager = self + .expiration_manager + .as_ref() + .expect("expiration_manager should be set"); + if expiration_manager.set_closing() { + return self.close().await; + } + // Transport is already closing the link + Ok(()) + } +} + impl Drop for LinkUnicastQuic { fn drop(&mut self) { self.connection.close(quinn::VarInt::from_u32(0), &[0]); @@ -219,17 +255,17 @@ impl LinkManagerUnicastTrait for LinkManagerUnicastQuic { .ok_or("Endpoints must be of the form quic/

:")?; let epconf = endpoint.config(); - let addr = get_quic_addr(&epaddr).await?; + let dst_addr = get_quic_addr(&epaddr).await?; // Initialize the QUIC connection let mut client_crypto = TlsClientConfig::new(&epconf) .await - .map_err(|e| zerror!("Cannot create a new QUIC client on {addr}: {e}"))?; + .map_err(|e| zerror!("Cannot create a new QUIC client on {dst_addr}: {e}"))?; client_crypto.client_config.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); - let ip_addr: IpAddr = if addr.is_ipv4() { + let ip_addr: IpAddr = if dst_addr.is_ipv4() { Ipv4Addr::UNSPECIFIED.into() } else { Ipv6Addr::UNSPECIFIED.into() @@ -248,7 +284,7 @@ impl LinkManagerUnicastTrait for LinkManagerUnicastQuic { .map_err(|e| zerror!("Can not create a new QUIC link bound to {}: {}", host, e))?; let quic_conn = quic_endpoint - .connect(addr, host) + .connect(dst_addr, host) .map_err(|e| zerror!("Can not create a new QUIC link bound to {}: {}", host, e))? .await .map_err(|e| zerror!("Can not create a new QUIC link bound to {}: {}", host, e))?; @@ -259,15 +295,31 @@ impl LinkManagerUnicastTrait for LinkManagerUnicastQuic { .map_err(|e| zerror!("Can not create a new QUIC link bound to {}: {}", host, e))?; let auth_id = get_cert_common_name(&quic_conn)?; - - let link = Arc::new(LinkUnicastQuic::new( - quic_conn, - src_addr, - endpoint.into(), - send, - recv, - auth_id.into(), - )); + let certchain_expiration_time = + get_cert_chain_expiration(&quic_conn)?.expect("server should have certificate chain"); + + let link = Arc::::new_cyclic(|weak_link| { + let mut expiration_manager = None; + if client_crypto.tls_close_link_on_expiration { + // setup expiration manager + expiration_manager = Some(LinkCertExpirationManager::new( + weak_link.clone(), + src_addr, + dst_addr, + QUIC_LOCATOR_PREFIX, + certchain_expiration_time, + )) + } + LinkUnicastQuic::new( + quic_conn, + src_addr, + endpoint.into(), + send, + recv, + auth_id.into(), + expiration_manager, + ) + }); Ok(LinkUnicast(link)) } @@ -337,7 +389,15 @@ impl LinkManagerUnicastTrait for LinkManagerUnicastQuic { let token = token.clone(); let manager = self.manager.clone(); - async move { accept_task(quic_endpoint, token, manager).await } + async move { + accept_task( + quic_endpoint, + token, + manager, + server_crypto.tls_close_link_on_expiration, + ) + .await + } }; // Initialize the QuicAcceptor @@ -369,6 +429,7 @@ async fn accept_task( quic_endpoint: quinn::Endpoint, token: CancellationToken, manager: NewLinkChannelSender, + tls_close_link_on_expiration: bool, ) -> ZResult<()> { async fn accept(acceptor: quinn::Accept<'_>) -> ZResult { let qc = acceptor @@ -416,19 +477,47 @@ async fn accept_task( } }; let dst_addr = quic_conn.remote_address(); + let dst_locator = Locator::new(QUIC_LOCATOR_PREFIX, dst_addr.to_string(), "")?; // Get Quic auth identifier let auth_id = get_cert_common_name(&quic_conn)?; + // Get certificate chain expiration + let mut maybe_expiration_time = None; + if tls_close_link_on_expiration { + match get_cert_chain_expiration(&quic_conn)? { + exp @ Some(_) => maybe_expiration_time = exp, + None => tracing::warn!( + "Cannot monitor expiration for QUIC link {:?} => {:?} : client does not have certificates", + src_addr, + dst_addr, + ), + } + } + tracing::debug!("Accepted QUIC connection on {:?}: {:?}", src_addr, dst_addr); // Create the new link object - let link = Arc::new(LinkUnicastQuic::new( - quic_conn, - src_addr, - Locator::new(QUIC_LOCATOR_PREFIX, dst_addr.to_string(), "")?, - send, - recv, - auth_id.into() - )); + let link = Arc::::new_cyclic(|weak_link| { + let mut expiration_manager = None; + if let Some(certchain_expiration_time) = maybe_expiration_time { + // setup expiration manager + expiration_manager = Some(LinkCertExpirationManager::new( + weak_link.clone(), + src_addr, + dst_addr, + QUIC_LOCATOR_PREFIX, + certchain_expiration_time, + )); + } + LinkUnicastQuic::new( + quic_conn, + src_addr, + dst_locator, + send, + recv, + auth_id.into(), + expiration_manager, + ) + }); // Communicate the new link to the initial transport manager if let Err(e) = manager.send_async(LinkUnicast(link)).await { @@ -475,6 +564,24 @@ fn get_cert_common_name(conn: &quinn::Connection) -> ZResult { Ok(auth_id) } +/// Returns the minimum value of the `not_after` field in the remote certificate chain. +/// Returns `None` if the remote certificate chain is empty +fn get_cert_chain_expiration(conn: &quinn::Connection) -> ZResult> { + let mut link_expiration: Option = None; + if let Some(pi) = conn.peer_identity() { + if let Ok(remote_certs) = pi.downcast::>() { + for cert in *remote_certs { + let (_, cert) = X509Certificate::from_der(cert.as_ref())?; + let cert_expiration = cert.validity().not_after.to_datetime(); + link_expiration = link_expiration + .map(|current_min| current_min.min(cert_expiration)) + .or(Some(cert_expiration)); + } + } + } + Ok(link_expiration) +} + #[derive(Debug, Clone)] struct QuicAuthId { auth_value: Option, diff --git a/io/zenoh-links/zenoh-link-quic/src/utils.rs b/io/zenoh-links/zenoh-link-quic/src/utils.rs index afd361f634..c00fb8e3de 100644 --- a/io/zenoh-links/zenoh-link-quic/src/utils.rs +++ b/io/zenoh-links/zenoh-link-quic/src/utils.rs @@ -94,11 +94,9 @@ impl ConfigurationInspector for TlsConfigurator { _ => {} } - if let Some(client_auth) = c.enable_mtls() { - match client_auth { - true => ps.push((TLS_ENABLE_MTLS, "true")), - false => ps.push((TLS_ENABLE_MTLS, "false")), - }; + match c.enable_mtls().unwrap_or(TLS_ENABLE_MTLS_DEFAULT) { + true => ps.push((TLS_ENABLE_MTLS, "true")), + false => ps.push((TLS_ENABLE_MTLS, "false")), } match (c.connect_private_key(), c.connect_private_key_base64()) { @@ -141,12 +139,21 @@ impl ConfigurationInspector for TlsConfigurator { false => ps.push((TLS_VERIFY_NAME_ON_CONNECT, "false")), }; + match c + .close_link_on_expiration() + .unwrap_or(TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT) + { + true => ps.push((TLS_CLOSE_LINK_ON_EXPIRATION, "true")), + false => ps.push((TLS_CLOSE_LINK_ON_EXPIRATION, "false")), + } + Ok(parameters::from_iter(ps.drain(..))) } } pub(crate) struct TlsServerConfig { pub(crate) server_config: ServerConfig, + pub(crate) tls_close_link_on_expiration: bool, } impl TlsServerConfig { @@ -155,7 +162,13 @@ impl TlsServerConfig { Some(s) => s .parse() .map_err(|_| zerror!("Unknown enable mTLS argument: {}", s))?, - None => false, + None => TLS_ENABLE_MTLS_DEFAULT, + }; + let tls_close_link_on_expiration: bool = match config.get(TLS_CLOSE_LINK_ON_EXPIRATION) { + Some(s) => s + .parse() + .map_err(|_| zerror!("Unknown close on expiration argument: {}", s))?, + None => TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT, }; let tls_server_private_key = TlsServerConfig::load_tls_private_key(config).await?; let tls_server_certificate = TlsServerConfig::load_tls_certificate(config).await?; @@ -215,7 +228,10 @@ impl TlsServerConfig { .with_single_cert(certs, keys.remove(0)) .map_err(|e| zerror!(e))? }; - Ok(TlsServerConfig { server_config: sc }) + Ok(TlsServerConfig { + server_config: sc, + tls_close_link_on_expiration, + }) } async fn load_tls_private_key(config: &Config<'_>) -> ZResult> { @@ -241,6 +257,7 @@ impl TlsServerConfig { pub(crate) struct TlsClientConfig { pub(crate) client_config: ClientConfig, + pub(crate) tls_close_link_on_expiration: bool, } impl TlsClientConfig { @@ -249,20 +266,24 @@ impl TlsClientConfig { Some(s) => s .parse() .map_err(|_| zerror!("Unknown enable mTLS argument: {}", s))?, - None => false, + None => TLS_ENABLE_MTLS_DEFAULT, }; let tls_server_name_verification: bool = match config.get(TLS_VERIFY_NAME_ON_CONNECT) { - Some(s) => { - let s: bool = s - .parse() - .map_err(|_| zerror!("Unknown server name verification argument: {}", s))?; - if s { - tracing::warn!("Skipping name verification of servers"); - } - s - } - None => false, + Some(s) => s + .parse() + .map_err(|_| zerror!("Unknown server name verification argument: {}", s))?, + None => TLS_VERIFY_NAME_ON_CONNECT_DEFAULT, + }; + if !tls_server_name_verification { + tracing::warn!("Skipping name verification of QUIC server"); + } + + let tls_close_link_on_expiration: bool = match config.get(TLS_CLOSE_LINK_ON_EXPIRATION) { + Some(s) => s + .parse() + .map_err(|_| zerror!("Unknown close on expiration argument: {}", s))?, + None => TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT, }; // Allows mixed user-generated CA and webPKI CA @@ -351,7 +372,10 @@ impl TlsClientConfig { .with_no_client_auth() } }; - Ok(TlsClientConfig { client_config: cc }) + Ok(TlsClientConfig { + client_config: cc, + tls_close_link_on_expiration, + }) } async fn load_tls_private_key(config: &Config<'_>) -> ZResult> { diff --git a/io/zenoh-links/zenoh-link-tls/Cargo.toml b/io/zenoh-links/zenoh-link-tls/Cargo.toml index 5fc8d3ad69..281d890beb 100644 --- a/io/zenoh-links/zenoh-link-tls/Cargo.toml +++ b/io/zenoh-links/zenoh-link-tls/Cargo.toml @@ -36,6 +36,7 @@ socket2 = { workspace = true } tokio = { workspace = true, features = ["fs", "io-util", "net", "sync"] } tokio-rustls = { workspace = true } tokio-util = { workspace = true, features = ["rt"] } +time = { workspace = true } tracing = { workspace = true } x509-parser = { workspace = true } webpki-roots = { workspace = true } diff --git a/io/zenoh-links/zenoh-link-tls/src/lib.rs b/io/zenoh-links/zenoh-link-tls/src/lib.rs index a547c5d77f..c82d31a8f5 100644 --- a/io/zenoh-links/zenoh-link-tls/src/lib.rs +++ b/io/zenoh-links/zenoh-link-tls/src/lib.rs @@ -105,10 +105,14 @@ pub mod config { pub const TLS_CONNECT_CERTIFICATE_BASE64: &str = "connect_certificate_base64"; pub const TLS_ENABLE_MTLS: &str = "enable_mtls"; + pub const TLS_ENABLE_MTLS_DEFAULT: bool = false; pub const TLS_VERIFY_NAME_ON_CONNECT: &str = "verify_name_on_connect"; pub const TLS_VERIFY_NAME_ON_CONNECT_DEFAULT: bool = true; + pub const TLS_CLOSE_LINK_ON_EXPIRATION: &str = "close_link_on_expiration"; + pub const TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT: bool = false; + /// The time duration in milliseconds to wait for the TLS handshake to complete. pub const TLS_HANDSHAKE_TIMEOUT_MS: &str = "tls_handshake_timeout_ms"; pub const TLS_HANDSHAKE_TIMEOUT_MS_DEFAULT: u64 = 10_000; diff --git a/io/zenoh-links/zenoh-link-tls/src/unicast.rs b/io/zenoh-links/zenoh-link-tls/src/unicast.rs index 3654e7800f..046288800e 100644 --- a/io/zenoh-links/zenoh-link-tls/src/unicast.rs +++ b/io/zenoh-links/zenoh-link-tls/src/unicast.rs @@ -14,6 +14,7 @@ use std::{cell::UnsafeCell, convert::TryInto, fmt, net::SocketAddr, sync::Arc, time::Duration}; use async_trait::async_trait; +use time::OffsetDateTime; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, @@ -21,11 +22,13 @@ use tokio::{ }; use tokio_rustls::{TlsAcceptor, TlsConnector, TlsStream}; use tokio_util::sync::CancellationToken; -use x509_parser::prelude::*; +use x509_parser::prelude::{FromDer, X509Certificate}; use zenoh_core::zasynclock; use zenoh_link_commons::{ - get_ip_interface_names, LinkAuthId, LinkAuthType, LinkManagerUnicastTrait, LinkUnicast, - LinkUnicastTrait, ListenersUnicastIP, NewLinkChannelSender, + get_ip_interface_names, + tls::expiration::{LinkCertExpirationManager, LinkWithCertExpiration}, + LinkAuthId, LinkAuthType, LinkManagerUnicastTrait, LinkUnicast, LinkUnicastTrait, + ListenersUnicastIP, NewLinkChannelSender, }; use zenoh_protocol::{ core::{EndPoint, Locator}, @@ -62,6 +65,7 @@ pub struct LinkUnicastTls { read_mtx: AsyncMutex<()>, auth_identifier: LinkAuthId, mtu: BatchSize, + expiration_manager: Option, } unsafe impl Send for LinkUnicastTls {} @@ -73,6 +77,7 @@ impl LinkUnicastTls { src_addr: SocketAddr, dst_addr: SocketAddr, auth_identifier: LinkAuthId, + expiration_manager: Option, ) -> LinkUnicastTls { let (tcp_stream, _) = socket.get_ref(); // Set the TLS nodelay option @@ -131,6 +136,7 @@ impl LinkUnicastTls { read_mtx: AsyncMutex::new(()), auth_identifier, mtu, + expiration_manager, } } @@ -141,10 +147,7 @@ impl LinkUnicastTls { fn get_mut_socket(&self) -> &mut TlsStream { unsafe { &mut *self.inner.get() } } -} -#[async_trait] -impl LinkUnicastTrait for LinkUnicastTls { async fn close(&self) -> ZResult<()> { tracing::trace!("Closing TLS link: {}", self); // Flush the TLS stream @@ -158,6 +161,24 @@ impl LinkUnicastTrait for LinkUnicastTls { tracing::trace!("TLS link shutdown {}: {:?}", self, res); res.map_err(|e| zerror!(e).into()) } +} + +#[async_trait] +impl LinkUnicastTrait for LinkUnicastTls { + async fn close(&self) -> ZResult<()> { + if let Some(expiration_manager) = &self.expiration_manager { + if !expiration_manager.set_closing() { + // expiration_task is closing link, return its returned ZResult to Transport + return expiration_manager.wait_for_expiration_task().await; + } + // cancel the expiration task and close link + expiration_manager.cancel_expiration_task(); + let res = self.close().await; + let _ = expiration_manager.wait_for_expiration_task().await; + return res; + } + self.close().await + } async fn write(&self, buffer: &[u8]) -> ZResult { let _guard = zasynclock!(self.write_mtx); @@ -232,6 +253,21 @@ impl LinkUnicastTrait for LinkUnicastTls { } } +#[async_trait] +impl LinkWithCertExpiration for LinkUnicastTls { + async fn expire(&self) -> ZResult<()> { + let expiration_manager = self + .expiration_manager + .as_ref() + .expect("expiration_manager should be set"); + if expiration_manager.set_closing() { + return self.close().await; + } + // Transport is already closing the link + Ok(()) + } +} + impl Drop for LinkUnicastTls { fn drop(&mut self) { // Close the underlying TCP stream @@ -326,15 +362,31 @@ impl LinkManagerUnicastTrait for LinkManagerUnicastTls { let (_, tls_conn) = tls_stream.get_ref(); let auth_identifier = get_server_cert_common_name(tls_conn)?; + let certchain_expiration_time = get_cert_chain_expiration(&tls_conn.peer_certificates())? + .expect("server should have certificate chain"); let tls_stream = TlsStream::Client(tls_stream); - let link = Arc::new(LinkUnicastTls::new( - tls_stream, - src_addr, - dst_addr, - auth_identifier.into(), - )); + let link = Arc::::new_cyclic(|weak_link| { + let mut expiration_manager = None; + if client_config.tls_close_link_on_expiration { + // setup expiration manager + expiration_manager = Some(LinkCertExpirationManager::new( + weak_link.clone(), + src_addr, + dst_addr, + TLS_LOCATOR_PREFIX, + certchain_expiration_time, + )) + } + LinkUnicastTls::new( + tls_stream, + dst_addr, + src_addr, + auth_identifier.into(), + expiration_manager, + ) + }); Ok(LinkUnicast(link)) } @@ -376,6 +428,7 @@ impl LinkManagerUnicastTrait for LinkManagerUnicastTls { token, manager, tls_server_config.tls_handshake_timeout, + tls_server_config.tls_close_link_on_expiration, ) .await } @@ -416,6 +469,7 @@ async fn accept_task( token: CancellationToken, manager: NewLinkChannelSender, tls_handshake_timeout: Duration, + tls_close_link_on_expiration: bool, ) -> ZResult<()> { let src_addr = socket.local_addr().map_err(|e| { let e = zerror!("Can not accept TLS connections: {}", e); @@ -445,14 +499,41 @@ async fn accept_task( }; let auth_identifier = get_client_cert_common_name(tls_conn)?; + // Get certificate chain expiration + let mut maybe_expiration_time = None; + if tls_close_link_on_expiration { + match get_cert_chain_expiration(&tls_conn.peer_certificates())? { + exp @ Some(_) => maybe_expiration_time = exp, + None => tracing::warn!( + "Cannot monitor expiration for TLS link {:?} => {:?} : client does not have certificates", + src_addr, + dst_addr, + ), + } + } + tracing::debug!("Accepted TLS connection on {:?}: {:?}", src_addr, dst_addr); // Create the new link object - let link = Arc::new(LinkUnicastTls::new( - tokio_rustls::TlsStream::Server(tls_stream), - src_addr, - dst_addr, - auth_identifier.into(), - )); + let link = Arc::::new_cyclic(|weak_link| { + let mut expiration_manager = None; + if let Some(certchain_expiration_time) = maybe_expiration_time { + // setup expiration manager + expiration_manager = Some(LinkCertExpirationManager::new( + weak_link.clone(), + src_addr, + dst_addr, + TLS_LOCATOR_PREFIX, + certchain_expiration_time, + )); + } + LinkUnicastTls::new( + tokio_rustls::TlsStream::Server(tls_stream), + dst_addr, + src_addr, + auth_identifier.into(), + expiration_manager, + ) + }); // Communicate the new link to the initial transport manager if let Err(e) = manager.send_async(LinkUnicast(link)).await { @@ -517,6 +598,24 @@ fn get_server_cert_common_name(tls_conn: &rustls::ClientConnection) -> ZResult, +) -> ZResult> { + let mut link_expiration: Option = None; + if let Some(remote_certs) = cert_chain { + for cert in *remote_certs { + let (_, cert) = X509Certificate::from_der(cert.as_ref())?; + let cert_expiration = cert.validity().not_after.to_datetime(); + link_expiration = link_expiration + .map(|current_min| current_min.min(cert_expiration)) + .or(Some(cert_expiration)); + } + } + Ok(link_expiration) +} + struct TlsAuthId { auth_value: Option, } diff --git a/io/zenoh-links/zenoh-link-tls/src/utils.rs b/io/zenoh-links/zenoh-link-tls/src/utils.rs index b6e2c69578..74e7cc9e51 100644 --- a/io/zenoh-links/zenoh-link-tls/src/utils.rs +++ b/io/zenoh-links/zenoh-link-tls/src/utils.rs @@ -97,11 +97,9 @@ impl ConfigurationInspector for TlsConfigurator { _ => {} } - if let Some(client_auth) = c.enable_mtls() { - match client_auth { - true => ps.push((TLS_ENABLE_MTLS, "true")), - false => ps.push((TLS_ENABLE_MTLS, "false")), - }; + match c.enable_mtls().unwrap_or(TLS_ENABLE_MTLS_DEFAULT) { + true => ps.push((TLS_ENABLE_MTLS, "true")), + false => ps.push((TLS_ENABLE_MTLS, "false")), } match (c.connect_private_key(), c.connect_private_key_base64()) { @@ -144,6 +142,14 @@ impl ConfigurationInspector for TlsConfigurator { false => ps.push((TLS_VERIFY_NAME_ON_CONNECT, "false")), }; + match c + .close_link_on_expiration() + .unwrap_or(TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT) + { + true => ps.push((TLS_CLOSE_LINK_ON_EXPIRATION, "true")), + false => ps.push((TLS_CLOSE_LINK_ON_EXPIRATION, "false")), + } + Ok(parameters::from_iter(ps.drain(..))) } } @@ -151,6 +157,7 @@ impl ConfigurationInspector for TlsConfigurator { pub(crate) struct TlsServerConfig { pub(crate) server_config: ServerConfig, pub(crate) tls_handshake_timeout: Duration, + pub(crate) tls_close_link_on_expiration: bool, } impl TlsServerConfig { @@ -159,7 +166,13 @@ impl TlsServerConfig { Some(s) => s .parse() .map_err(|_| zerror!("Unknown enable mTLS argument: {}", s))?, - None => false, + None => TLS_ENABLE_MTLS_DEFAULT, + }; + let tls_close_link_on_expiration: bool = match config.get(TLS_CLOSE_LINK_ON_EXPIRATION) { + Some(s) => s + .parse() + .map_err(|_| zerror!("Unknown close on expiration argument: {}", s))?, + None => TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT, }; let tls_server_private_key = TlsServerConfig::load_tls_private_key(config).await?; let tls_server_certificate = TlsServerConfig::load_tls_certificate(config).await?; @@ -231,6 +244,7 @@ impl TlsServerConfig { Ok(TlsServerConfig { server_config: sc, tls_handshake_timeout, + tls_close_link_on_expiration, }) } @@ -257,6 +271,7 @@ impl TlsServerConfig { pub(crate) struct TlsClientConfig { pub(crate) client_config: ClientConfig, + pub(crate) tls_close_link_on_expiration: bool, } impl TlsClientConfig { @@ -265,20 +280,24 @@ impl TlsClientConfig { Some(s) => s .parse() .map_err(|_| zerror!("Unknown enable mTLS auth argument: {}", s))?, - None => false, + None => TLS_ENABLE_MTLS_DEFAULT, }; let tls_server_name_verification: bool = match config.get(TLS_VERIFY_NAME_ON_CONNECT) { - Some(s) => { - let s: bool = s - .parse() - .map_err(|_| zerror!("Unknown server name verification argument: {}", s))?; - if s { - tracing::warn!("Skipping name verification of servers"); - } - s - } - None => false, + Some(s) => s + .parse() + .map_err(|_| zerror!("Unknown server name verification argument: {}", s))?, + None => TLS_VERIFY_NAME_ON_CONNECT_DEFAULT, + }; + if !tls_server_name_verification { + tracing::warn!("Skipping name verification of TLS server"); + } + + let tls_close_link_on_expiration: bool = match config.get(TLS_CLOSE_LINK_ON_EXPIRATION) { + Some(s) => s + .parse() + .map_err(|_| zerror!("Unknown close on expiration argument: {}", s))?, + None => TLS_CLOSE_LINK_ON_EXPIRATION_DEFAULT, }; // Allows mixed user-generated CA and webPKI CA @@ -367,7 +386,10 @@ impl TlsClientConfig { .with_no_client_auth() } }; - Ok(TlsClientConfig { client_config: cc }) + Ok(TlsClientConfig { + client_config: cc, + tls_close_link_on_expiration, + }) } async fn load_tls_private_key(config: &Config<'_>) -> ZResult> { diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index b9df96c6b7..22184c326a 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -1440,7 +1440,9 @@ impl SessionInner { primitives.send_interest(Interest { id: pub_state.remote_id, mode: InterestMode::Final, - options: InterestOptions::empty(), + // Note: InterestMode::Final options are undefined in the current protocol specification, + // they are initialized here for internal use by local egress interceptors. + options: InterestOptions::SUBSCRIBERS, wire_expr: None, ext_qos: declare::ext::QoSType::DEFAULT, ext_tstamp: None, @@ -1648,7 +1650,9 @@ impl SessionInner { primitives.send_interest(Interest { id: sub_state.id, mode: InterestMode::Final, - options: InterestOptions::empty(), + // Note: InterestMode::Final options are undefined in the current protocol specification, + // they are initialized here for internal use by local egress interceptors. + options: InterestOptions::TOKENS, wire_expr: None, ext_qos: declare::ext::QoSType::DEFAULT, ext_tstamp: None, diff --git a/zenoh/src/net/routing/dispatcher/token.rs b/zenoh/src/net/routing/dispatcher/token.rs index a34e35af68..397f2304f3 100644 --- a/zenoh/src/net/routing/dispatcher/token.rs +++ b/zenoh/src/net/routing/dispatcher/token.rs @@ -156,6 +156,7 @@ pub(crate) fn undeclare_token( { tracing::debug!("{} Undeclare token {} ({})", face, id, res.expr()); } else { - tracing::error!("{} Undeclare unknown token {}", face, id); + // NOTE: This is expected behavior if liveliness tokens are denied with ingress ACL interceptor. + tracing::debug!("{} Undeclare unknown token {}", face, id); } } diff --git a/zenoh/src/net/routing/hat/client/interests.rs b/zenoh/src/net/routing/hat/client/interests.rs index c534c65bad..0f900d5958 100644 --- a/zenoh/src/net/routing/hat/client/interests.rs +++ b/zenoh/src/net/routing/hat/client/interests.rs @@ -212,7 +212,9 @@ impl HatInterestTrait for HatCode { Interest { id, mode: InterestMode::Final, - options: InterestOptions::empty(), + // Note: InterestMode::Final options are undefined in the current protocol specification, + // they are initialized here for internal use by local egress interceptors. + options: interest.options, wire_expr: None, ext_qos: ext::QoSType::DECLARE, ext_tstamp: None, diff --git a/zenoh/src/net/routing/hat/p2p_peer/interests.rs b/zenoh/src/net/routing/hat/p2p_peer/interests.rs index b0d7c98426..abe1108f78 100644 --- a/zenoh/src/net/routing/hat/p2p_peer/interests.rs +++ b/zenoh/src/net/routing/hat/p2p_peer/interests.rs @@ -238,7 +238,9 @@ impl HatInterestTrait for HatCode { Interest { id, mode: InterestMode::Final, - options: InterestOptions::empty(), + // Note: InterestMode::Final options are undefined in the current protocol specification, + // they are initialized here for internal use by local egress interceptors. + options: interest.options, wire_expr: None, ext_qos: ext::QoSType::DECLARE, ext_tstamp: None, diff --git a/zenoh/src/net/routing/interceptor/access_control.rs b/zenoh/src/net/routing/interceptor/access_control.rs index 2142d3e0cc..170771d541 100644 --- a/zenoh/src/net/routing/interceptor/access_control.rs +++ b/zenoh/src/net/routing/interceptor/access_control.rs @@ -26,7 +26,10 @@ use zenoh_config::{ }; use zenoh_protocol::{ core::ZenohIdProto, - network::{Declare, DeclareBody, NetworkBody, NetworkMessage, Push, Request, Response}, + network::{ + interest::InterestMode, Declare, DeclareBody, Interest, NetworkBody, NetworkMessage, Push, + Request, Response, + }, zenoh::{PushBody, RequestBody}, }; use zenoh_result::ZResult; @@ -334,6 +337,75 @@ impl InterceptorTrait for IngressAclEnforcer { } } } + NetworkBody::Declare(Declare { + body: DeclareBody::DeclareToken(_), + .. + }) => { + if self.action( + AclMessage::LivelinessToken, + "Liveliness Token (ingress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + + NetworkBody::Declare(Declare { + body: DeclareBody::UndeclareToken(_), + .. + }) => { + // Undeclaration filtering diverges between ingress and egress: + // Undeclarations in ingress are only filtered if the ext_wire_expr is set. + // If it's not set, we let the undeclaration pass, it will be rejected by the routing logic + // if its associated declaration was denied. + if let Some(key_expr) = key_expr { + if !key_expr.is_empty() + && self.action( + AclMessage::LivelinessToken, + "Undeclare Liveliness Token (ingress)", + key_expr, + ) == Permission::Deny + { + return None; + } + } + } + NetworkBody::Interest(Interest { + mode: InterestMode::Current, + options, + .. + }) if options.tokens() => { + if self.action( + AclMessage::LivelinessQuery, + "Liveliness Query (ingress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + NetworkBody::Interest(Interest { + mode: InterestMode::Future | InterestMode::CurrentFuture, + options, + .. + }) if options.tokens() => { + if self.action( + AclMessage::DeclareLivelinessSubscriber, + "Declare Liveliness Subscriber (ingress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + NetworkBody::Interest(Interest { + mode: InterestMode::Final, + .. + }) => { + // InterestMode::Final filtering diverges between ingress and egress: + // InterestMode::Final ingress is always allowed, it will be rejected by routing logic if its associated Interest was denied + } // Unfiltered Declare messages NetworkBody::Declare(Declare { body: DeclareBody::DeclareKeyExpr(_), @@ -342,19 +414,11 @@ impl InterceptorTrait for IngressAclEnforcer { | NetworkBody::Declare(Declare { body: DeclareBody::DeclareFinal(_), .. - }) - | NetworkBody::Declare(Declare { - body: DeclareBody::DeclareToken(_), - .. }) => {} // Unfiltered Undeclare messages NetworkBody::Declare(Declare { body: DeclareBody::UndeclareKeyExpr(_), .. - }) - | NetworkBody::Declare(Declare { - body: DeclareBody::UndeclareToken(_), - .. }) => {} // Unfiltered remaining message types NetworkBody::Interest(_) | NetworkBody::OAM(_) | NetworkBody::ResponseFinal(_) => {} @@ -470,6 +534,80 @@ impl InterceptorTrait for EgressAclEnforcer { return None; } } + NetworkBody::Declare(Declare { + body: DeclareBody::DeclareToken(_), + .. + }) => { + if self.action( + AclMessage::LivelinessToken, + "Liveliness Token (egress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + NetworkBody::Declare(Declare { + body: DeclareBody::UndeclareToken(_), + .. + }) => { + // Undeclaration filtering diverges between ingress and egress: + // in egress the keyexpr has to be provided in the RoutingContext + if self.action( + AclMessage::LivelinessToken, + "Undeclare Liveliness Token (egress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + NetworkBody::Interest(Interest { + mode: InterestMode::Current, + options, + .. + }) if options.tokens() => { + if self.action( + AclMessage::LivelinessQuery, + "Liveliness Query (egress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + NetworkBody::Interest(Interest { + mode: InterestMode::Future | InterestMode::CurrentFuture, + options, + .. + }) if options.tokens() => { + if self.action( + AclMessage::DeclareLivelinessSubscriber, + "Declare Liveliness Subscriber (egress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } + NetworkBody::Interest(Interest { + mode: InterestMode::Final, + options, + .. + }) if options.tokens() => { + // Note: options are set for InterestMode::Final for internal use only by egress interceptors. + + // InterestMode::Final filtering diverges between ingress and egress: + // in egress the keyexpr has to be provided in the RoutingContext + if self.action( + AclMessage::DeclareLivelinessSubscriber, + "Undeclare Liveliness Subscriber (egress)", + key_expr?, + ) == Permission::Deny + { + return None; + } + } // Unfiltered Declare messages NetworkBody::Declare(Declare { body: DeclareBody::DeclareKeyExpr(_), @@ -478,19 +616,11 @@ impl InterceptorTrait for EgressAclEnforcer { | NetworkBody::Declare(Declare { body: DeclareBody::DeclareFinal(_), .. - }) - | NetworkBody::Declare(Declare { - body: DeclareBody::DeclareToken(_), - .. }) => {} // Unfiltered Undeclare messages NetworkBody::Declare(Declare { body: DeclareBody::UndeclareKeyExpr(_), .. - }) - | NetworkBody::Declare(Declare { - body: DeclareBody::UndeclareToken(_), - .. }) => {} // Unfiltered remaining message types NetworkBody::Interest(_) | NetworkBody::OAM(_) | NetworkBody::ResponseFinal(_) => {} diff --git a/zenoh/src/net/routing/interceptor/authorization.rs b/zenoh/src/net/routing/interceptor/authorization.rs index ae70f11a46..3f09d90fb1 100644 --- a/zenoh/src/net/routing/interceptor/authorization.rs +++ b/zenoh/src/net/routing/interceptor/authorization.rs @@ -188,6 +188,9 @@ struct ActionPolicy { declare_subscriber: PermissionPolicy, declare_queryable: PermissionPolicy, reply: PermissionPolicy, + liveliness_token: PermissionPolicy, + declare_liveliness_sub: PermissionPolicy, + liveliness_query: PermissionPolicy, } impl ActionPolicy { @@ -199,6 +202,9 @@ impl ActionPolicy { AclMessage::Delete => &self.delete, AclMessage::DeclareSubscriber => &self.declare_subscriber, AclMessage::DeclareQueryable => &self.declare_queryable, + AclMessage::LivelinessToken => &self.liveliness_token, + AclMessage::DeclareLivelinessSubscriber => &self.declare_liveliness_sub, + AclMessage::LivelinessQuery => &self.liveliness_query, } } fn action_mut(&mut self, action: AclMessage) -> &mut PermissionPolicy { @@ -209,6 +215,9 @@ impl ActionPolicy { AclMessage::Delete => &mut self.delete, AclMessage::DeclareSubscriber => &mut self.declare_subscriber, AclMessage::DeclareQueryable => &mut self.declare_queryable, + AclMessage::LivelinessToken => &mut self.liveliness_token, + AclMessage::DeclareLivelinessSubscriber => &mut self.declare_liveliness_sub, + AclMessage::LivelinessQuery => &mut self.liveliness_query, } } } diff --git a/zenoh/tests/acl.rs b/zenoh/tests/acl.rs index da55c191b0..68293f4ad7 100644 --- a/zenoh/tests/acl.rs +++ b/zenoh/tests/acl.rs @@ -16,7 +16,7 @@ #![cfg(target_family = "unix")] mod test { use std::{ - sync::{Arc, Mutex}, + sync::{atomic::AtomicBool, Arc, Mutex}, time::Duration, }; @@ -56,6 +56,23 @@ mod test { test_reply_allow_then_deny(27449).await; } + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_acl_liveliness() { + zenoh::init_log_from_env_or("error"); + + test_liveliness_allow(27450).await; + test_liveliness_deny(27450).await; + + test_liveliness_allow_deny_token(27450).await; + test_liveliness_deny_allow_token(27450).await; + + test_liveliness_allow_deny_sub(27450).await; + test_liveliness_deny_allow_sub(27450).await; + + test_liveliness_allow_deny_query(27450).await; + test_liveliness_deny_allow_query(27450).await; + } + async fn get_basic_router_config(port: u16) -> Config { let mut config = Config::default(); config.set_mode(Some(WhatAmI::Router)).unwrap(); @@ -805,4 +822,718 @@ mod test { close_sessions(get_session, qbl_session).await; close_router_session(session).await; } + + async fn test_liveliness_deny(port: u16) { + println!("test_liveliness_deny"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "deny", + "rules": [], + "subjects": [], + "policies": [], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_allow(port: u16) { + println!("test_liveliness_allow"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "allow", + "rules": [], + "subjects": [], + "policies": [], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_allow_deny_token(port: u16) { + println!("test_liveliness_allow_deny_token"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "allow", + "rules": [ + { + id: "filter token", + permission: "deny", + messages: ["liveliness_token"], + flows: ["ingress", "egress"], + key_exprs: ["test/demo"], + }, + ], + "subjects": [ + { id: "all" }, + ], + "policies": [ + { + "subjects": ["all"], + "rules": ["filter token"], + }, + ], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_deny_allow_token(port: u16) { + println!("test_liveliness_deny_allow_token"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "deny", + "rules": [ + { + id: "filter token", + permission: "allow", + messages: ["liveliness_token"], + flows: ["ingress", "egress"], + key_exprs: ["test/demo"], + }, + ], + "subjects": [ + { id: "all" }, + ], + "policies": [ + { + "subjects": ["all"], + "rules": ["filter token"], + }, + ], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_allow_deny_sub(port: u16) { + println!("test_liveliness_allow_deny_sub"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "allow", + "rules": [ + { + id: "filter sub", + permission: "deny", + messages: ["declare_liveliness_subscriber"], + flows: ["ingress", "egress"], + key_exprs: ["test/demo"], + }, + ], + "subjects": [ + { id: "all" }, + ], + "policies": [ + { + "subjects": ["all"], + "rules": ["filter sub"], + }, + ], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_deny_allow_sub(port: u16) { + println!("test_liveliness_deny_allow_sub"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "deny", + "rules": [ + { + id: "filter sub", + permission: "allow", + messages: ["declare_liveliness_subscriber", "liveliness_token"], + flows: ["ingress", "egress"], + key_exprs: ["test/demo"], + }, + ], + "subjects": [ + { id: "all" }, + ], + "policies": [ + { + "subjects": ["all"], + "rules": ["filter sub"], + }, + ], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_allow_deny_query(port: u16) { + println!("test_liveliness_allow_deny_query"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "allow", + "rules": [ + { + id: "filter query", + permission: "deny", + messages: ["liveliness_query"], + flows: ["ingress", "egress"], + key_exprs: ["test/demo"], + }, + ], + "subjects": [ + { id: "all" }, + ], + "policies": [ + { + "subjects": ["all"], + "rules": ["filter query"], + }, + ], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } + + async fn test_liveliness_deny_allow_query(port: u16) { + println!("test_liveliness_deny_allow_query"); + + let mut config_router = get_basic_router_config(port).await; + config_router + .insert_json5( + "access_control", + r#"{ + "enabled": true, + "default_permission": "deny", + "rules": [ + { + id: "filter query", + permission: "allow", + messages: ["liveliness_query", "liveliness_token"], + flows: ["ingress", "egress"], + key_exprs: ["test/demo"], + }, + ], + "subjects": [ + { id: "all" }, + ], + "policies": [ + { + "subjects": ["all"], + "rules": ["filter query"], + }, + ], + }"#, + ) + .unwrap(); + println!("Opening router session"); + + let session = ztimeout!(zenoh::open(config_router)).unwrap(); + let (reader_session, writer_session) = get_client_sessions(port).await; + + let received_token = Arc::new(AtomicBool::new(false)); + let dropped_token = Arc::new(AtomicBool::new(false)); + let received_token_reply = Arc::new(AtomicBool::new(false)); + + let cloned_received_token = received_token.clone(); + let cloned_dropped_token = dropped_token.clone(); + let cloned_received_token_reply = received_token_reply.clone(); + + let subscriber = reader_session + .liveliness() + .declare_subscriber(KEY_EXPR) + .callback(move |sample| { + if sample.kind() == SampleKind::Put { + cloned_received_token.store(true, std::sync::atomic::Ordering::Relaxed); + } else if sample.kind() == SampleKind::Delete { + cloned_dropped_token.store(true, std::sync::atomic::Ordering::Relaxed); + } + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + + // test if sub receives token declaration + let liveliness = writer_session + .liveliness() + .declare_token(KEY_EXPR) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!received_token.load(std::sync::atomic::Ordering::Relaxed)); + + // test if query receives token reply + reader_session + .liveliness() + .get(KEY_EXPR) + .timeout(TIMEOUT) + .callback(move |reply| match reply.result() { + Ok(_) => { + cloned_received_token_reply.store(true, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => println!("Error : {:?}", e), + }) + .await + .unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(received_token_reply.load(std::sync::atomic::Ordering::Relaxed)); + + // test if sub receives token undeclaration + ztimeout!(liveliness.undeclare()).unwrap(); + tokio::time::sleep(SLEEP).await; + assert!(!dropped_token.load(std::sync::atomic::Ordering::Relaxed)); + + ztimeout!(subscriber.undeclare()).unwrap(); + close_sessions(reader_session, writer_session).await; + close_router_session(session).await; + } } diff --git a/zenoh/tests/liveliness.rs b/zenoh/tests/liveliness.rs index 805f3ab4b2..b78292c148 100644 --- a/zenoh/tests/liveliness.rs +++ b/zenoh/tests/liveliness.rs @@ -1625,8 +1625,8 @@ async fn test_liveliness_subscriber_double_router_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47464"; - const ROUTER_SUB_ENDPOINT: &str = "tcp/localhost:47465"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30464"; + const ROUTER_SUB_ENDPOINT: &str = "tcp/localhost:30465"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subscriber/double/router/before"; zenoh_util::init_log_from_env_or("error"); @@ -2733,8 +2733,8 @@ async fn test_liveliness_subscriber_double_clientviapeer_history_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47484"; - const PEER_DUMMY_ENDPOINT: &str = "tcp/localhost:47485"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30484"; + const PEER_DUMMY_ENDPOINT: &str = "tcp/localhost:30485"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subscriber/double/clientviapeer/history/middle"; @@ -2856,8 +2856,8 @@ async fn test_liveliness_subscriber_double_clientviapeer_history_after() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47486"; - const PEER_DUMMY_ENDPOINT: &str = "tcp/localhost:47487"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30486"; + const PEER_DUMMY_ENDPOINT: &str = "tcp/localhost:30487"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subscriber/double/clientviapeer/history/after"; @@ -2982,7 +2982,7 @@ async fn test_liveliness_subget_client_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47488"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30488"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/client/before"; zenoh_util::init_log_from_env_or("error"); @@ -3075,7 +3075,7 @@ async fn test_liveliness_subget_client_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47489"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30489"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/client/middle"; zenoh_util::init_log_from_env_or("error"); @@ -3173,7 +3173,7 @@ async fn test_liveliness_subget_client_history_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47490"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30490"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/client/history/before"; zenoh_util::init_log_from_env_or("error"); @@ -3270,7 +3270,7 @@ async fn test_liveliness_subget_client_history_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47491"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30491"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/client/history/middle"; zenoh_util::init_log_from_env_or("error"); @@ -3373,7 +3373,7 @@ async fn test_liveliness_subget_peer_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47492"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30492"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/peer/before"; zenoh_util::init_log_from_env_or("error"); @@ -3466,7 +3466,7 @@ async fn test_liveliness_subget_peer_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47493"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30493"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/peer/middle"; zenoh_util::init_log_from_env_or("error"); @@ -3564,7 +3564,7 @@ async fn test_liveliness_subget_peer_history_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47494"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30494"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/peer/history/before"; zenoh_util::init_log_from_env_or("error"); @@ -3661,7 +3661,7 @@ async fn test_liveliness_subget_peer_history_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47495"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30495"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/peer/history/middle"; zenoh_util::init_log_from_env_or("error"); @@ -3764,7 +3764,7 @@ async fn test_liveliness_subget_router_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47496"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30496"; const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:47497"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/router/before"; @@ -3862,8 +3862,8 @@ async fn test_liveliness_subget_router_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47498"; - const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:47499"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30498"; + const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:30499"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/router/middle"; zenoh_util::init_log_from_env_or("error"); @@ -3965,8 +3965,8 @@ async fn test_liveliness_subget_router_history_before() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47500"; - const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:47501"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30500"; + const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:30501"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/router/history/before"; zenoh_util::init_log_from_env_or("error"); @@ -4067,8 +4067,8 @@ async fn test_liveliness_subget_router_history_middle() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47502"; - const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:47503"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30502"; + const ROUTER_SUBGET_ENDPOINT: &str = "tcp/localhost:30503"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/subget/router/history/middle"; zenoh_util::init_log_from_env_or("error"); @@ -4171,8 +4171,8 @@ async fn test_liveliness_regression_1() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47504"; - const PEER_TOK_ENDPOINT: &str = "tcp/localhost:47505"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30504"; + const PEER_TOK_ENDPOINT: &str = "tcp/localhost:30505"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/regression/1"; zenoh_util::init_log_from_env_or("error"); @@ -4255,8 +4255,8 @@ async fn test_liveliness_regression_2() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const PEER_TOK1_ENDPOINT: &str = "tcp/localhost:47506"; - const PEER_SUB_ENDPOINT: &str = "tcp/localhost:47507"; + const PEER_TOK1_ENDPOINT: &str = "tcp/localhost:30506"; + const PEER_SUB_ENDPOINT: &str = "tcp/localhost:30507"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/regression/2"; zenoh_util::init_log_from_env_or("error"); @@ -4349,8 +4349,8 @@ async fn test_liveliness_regression_2_history() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const PEER_TOK1_ENDPOINT: &str = "tcp/localhost:47508"; - const PEER_SUB_ENDPOINT: &str = "tcp/localhost:47509"; + const PEER_TOK1_ENDPOINT: &str = "tcp/localhost:30508"; + const PEER_SUB_ENDPOINT: &str = "tcp/localhost:30509"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/regression/2/history"; zenoh_util::init_log_from_env_or("error"); @@ -4450,8 +4450,8 @@ async fn test_liveliness_regression_3() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER_ENDPOINT: &str = "tcp/localhost:47510"; - const PEER_TOK_ENDPOINT: &str = "tcp/localhost:47511"; + const ROUTER_ENDPOINT: &str = "tcp/localhost:30510"; + const PEER_TOK_ENDPOINT: &str = "tcp/localhost:30511"; const LIVELINESS_KEYEXPR: &str = "test/liveliness/regression/3"; zenoh_util::init_log_from_env_or("error"); @@ -4557,9 +4557,9 @@ async fn test_liveliness_issue_1470() { const TIMEOUT: Duration = Duration::from_secs(60); const SLEEP: Duration = Duration::from_secs(1); - const ROUTER0_ENDPOINT: &str = "tcp/localhost:47512"; - const ROUTER1_ENDPOINT: &str = "tcp/localhost:47513"; - const PEER_ENDPOINT: &str = "tcp/localhost:47514"; + const ROUTER0_ENDPOINT: &str = "tcp/localhost:30512"; + const ROUTER1_ENDPOINT: &str = "tcp/localhost:30513"; + const PEER_ENDPOINT: &str = "tcp/localhost:30514"; const LIVELINESS_KEYEXPR_PREFIX: &str = "test/liveliness/issue/1470/*"; const LIVELINESS_KEYEXPR_ROUTER0: &str = "test/liveliness/issue/1470/a0"; const LIVELINESS_KEYEXPR_ROUTER1: &str = "test/liveliness/issue/1470/a1"; From f1493665a2d20759e84b0ce12365c6b01489e065 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 22 Nov 2024 00:22:29 +0100 Subject: [PATCH 17/30] clippy fix --- zenoh/src/api/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 22184c326a..93d4ced556 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -1571,7 +1571,7 @@ impl SessionInner { } } else if origin == Locality::SessionLocal { #[cfg(feature = "unstable")] - self.update_matching_status(&state, &key_expr, MatchingStatusType::Subscribers, true) + self.update_matching_status(&state, key_expr, MatchingStatusType::Subscribers, true) } Ok(sub_state) From 7f380e7d44642f363d15f8dc24428e173a94645b Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 22 Nov 2024 11:49:46 +0100 Subject: [PATCH 18/30] fix review comments --- zenoh/src/api/publisher.rs | 2 +- zenoh/src/api/querier.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index 51b8a342f2..1c0376f486 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -534,7 +534,7 @@ pub(crate) enum MatchingStatusType { #[zenoh_macros::unstable] impl MatchingStatus { - /// Return true if there exist Subscribers matching the Publisher's key expression. + /// Return true if there exist entities matching the target (i.e either Subscribers matching Publisher's key expression or Queryables matching Querier's key expression and target). /// /// # Examples /// ``` diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index 429379495b..b20dde5212 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -1,3 +1,17 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + use core::fmt; use std::{ future::{IntoFuture, Ready}, From eced4f3880f34cfe5687dcf9e11fc62ec8685094 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 22 Nov 2024 12:03:01 +0100 Subject: [PATCH 19/30] explain #[allow(unused_mut)] --- zenoh/src/api/builders/querier.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zenoh/src/api/builders/querier.rs b/zenoh/src/api/builders/querier.rs index 20daf9546b..a5b38f9f07 100644 --- a/zenoh/src/api/builders/querier.rs +++ b/zenoh/src/api/builders/querier.rs @@ -411,6 +411,7 @@ where let (callback, receiver) = self.handler.into_handler(); #[allow(unused_mut)] + // mut is only needed when building with "unstable" feature, which might add extra internal parameters on top of the user-provided ones let mut parameters = self.parameters.clone(); #[cfg(feature = "unstable")] if self.querier.accept_replies() == ReplyKeyExpr::Any { From 65e6e13500de7811a8f447a80f3063bdae0a539c Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 22 Nov 2024 12:51:17 +0100 Subject: [PATCH 20/30] explain behaviour of keyexpr_intersect and keyexpr_include in case of conversion failure --- zenoh/src/api/key_expr.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zenoh/src/api/key_expr.rs b/zenoh/src/api/key_expr.rs index 64703d77fa..a1daea6c0c 100644 --- a/zenoh/src/api/key_expr.rs +++ b/zenoh/src/api/key_expr.rs @@ -273,6 +273,8 @@ impl<'a> KeyExpr<'a> { } } + /// Will return false in case of TryInto failure. + #[inline] pub(crate) fn keyexpr_intersect<'b, L, R>(left: L, right: R) -> bool where L: TryInto>, @@ -285,6 +287,8 @@ impl<'a> KeyExpr<'a> { } } + /// Will return false in case of TryInto failure. + #[inline] pub(crate) fn keyexpr_include<'b, L, R>(left: L, right: R) -> bool where L: TryInto>, From c54ed52a892c01521b36c6bbee2a820f523fde1c Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 25 Nov 2024 15:55:23 +0100 Subject: [PATCH 21/30] log error when keyexpr_intersect/includes fails keyexpr conversion --- zenoh/src/api/key_expr.rs | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/zenoh/src/api/key_expr.rs b/zenoh/src/api/key_expr.rs index a1daea6c0c..23d933a2e5 100644 --- a/zenoh/src/api/key_expr.rs +++ b/zenoh/src/api/key_expr.rs @@ -273,32 +273,54 @@ impl<'a> KeyExpr<'a> { } } - /// Will return false in case of TryInto failure. + /// Will return false and log a error in case of TryInto failure. #[inline] pub(crate) fn keyexpr_intersect<'b, L, R>(left: L, right: R) -> bool where L: TryInto>, R: TryInto>, + L::Error: std::fmt::Display, + R::Error: std::fmt::Display, { - if let (Ok(l), Ok(r)) = (left.try_into(), right.try_into()) { - l.intersects(&r) - } else { - false + match left.try_into() { + Ok(l) => match right.try_into() { + Ok(r) => { + return l.intersects(&r); + } + Err(e) => { + tracing::error!("{e}"); + } + }, + Err(e) => { + tracing::error!("{e}"); + } } + false } - /// Will return false in case of TryInto failure. + /// Will return false and log a error in case of TryInto failure. #[inline] pub(crate) fn keyexpr_include<'b, L, R>(left: L, right: R) -> bool where L: TryInto>, R: TryInto>, + L::Error: std::fmt::Display, + R::Error: std::fmt::Display, { - if let (Ok(l), Ok(r)) = (left.try_into(), right.try_into()) { - l.includes(&r) - } else { - false + match left.try_into() { + Ok(l) => match right.try_into() { + Ok(r) => { + return l.includes(&r); + } + Err(e) => { + tracing::error!("{e}"); + } + }, + Err(e) => { + tracing::error!("{e}"); + } } + false } } From 4a0f968dd6197390974c5d80ed3bbd1fe6f2f957 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 25 Nov 2024 16:42:25 +0100 Subject: [PATCH 22/30] add matching listener to z_pub example; add flag to enable/disable matching listener in the z_pub and z_querier examples; --- examples/examples/z_pub.rs | 38 ++++++++++++++++++++++++++++--- examples/examples/z_querier.rs | 41 ++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/examples/examples/z_pub.rs b/examples/examples/z_pub.rs index 25d6eacdee..eac7236230 100644 --- a/examples/examples/z_pub.rs +++ b/examples/examples/z_pub.rs @@ -22,7 +22,10 @@ async fn main() { // Initiate logging zenoh::init_log_from_env_or("error"); - let (config, key_expr, payload, attachment) = parse_args(); + #[cfg(feature = "unstable")] + let (config, key_expr, payload, attachment, add_matching_listener) = parse_args(); + #[cfg(not(feature = "unstable"))] + let (config, key_expr, payload, attachment, _) = parse_args(); println!("Opening session..."); let session = zenoh::open(config).await.unwrap(); @@ -30,6 +33,22 @@ async fn main() { println!("Declaring Publisher on '{key_expr}'..."); let publisher = session.declare_publisher(&key_expr).await.unwrap(); + #[cfg(feature = "unstable")] + if add_matching_listener { + publisher + .matching_listener() + .callback(|matching_status| { + if matching_status.matching() { + println!("Publisher has matching subscribers."); + } else { + println!("Publisher has NO MORE matching subscribers."); + } + }) + .background() + .await + .unwrap(); + } + println!("Press CTRL-C to quit..."); for idx in 0..u32::MAX { tokio::time::sleep(Duration::from_secs(1)).await; @@ -56,11 +75,24 @@ struct Args { #[arg(short, long)] /// The attachments to add to each put. attach: Option, + /// Enable matching listener. + #[cfg(feature = "unstable")] + #[arg(long)] + add_matching_listener: bool, #[command(flatten)] common: CommonArgs, } -fn parse_args() -> (Config, KeyExpr<'static>, String, Option) { +fn parse_args() -> (Config, KeyExpr<'static>, String, Option, bool) { let args = Args::parse(); - (args.common.into(), args.key, args.payload, args.attach) + ( + args.common.into(), + args.key, + args.payload, + args.attach, + #[cfg(feature = "unstable")] + args.add_matching_listener, + #[cfg(not(feature = "unstable"))] + false, + ) } diff --git a/examples/examples/z_querier.rs b/examples/examples/z_querier.rs index 19f610e69e..069b72c3a2 100644 --- a/examples/examples/z_querier.rs +++ b/examples/examples/z_querier.rs @@ -21,8 +21,10 @@ use zenoh_examples::CommonArgs; async fn main() { // initiate logging zenoh::init_log_from_env_or("error"); - - let (config, keyexpr, payload, target, timeout) = parse_args(); + #[cfg(feature = "unstable")] + let (config, keyexpr, payload, target, timeout, add_matching_listener) = parse_args(); + #[cfg(not(feature = "unstable"))] + let (config, keyexpr, payload, target, timeout, _) = parse_args(); println!("Opening session..."); let session = zenoh::open(config).await.unwrap(); @@ -36,18 +38,20 @@ async fn main() { .unwrap(); #[cfg(feature = "unstable")] - querier - .matching_listener() - .callback(|matching_status| { - if matching_status.matching() { - println!("Querier has matching queryables."); - } else { - println!("Querier has NO MORE matching queryables."); - } - }) - .background() - .await - .unwrap(); + if add_matching_listener { + querier + .matching_listener() + .callback(|matching_status| { + if matching_status.matching() { + println!("Querier has matching queryables."); + } else { + println!("Querier has NO MORE matching queryables."); + } + }) + .background() + .await + .unwrap(); + } println!("Press CTRL-C to quit..."); for idx in 0..u32::MAX { @@ -116,6 +120,10 @@ struct Args { #[arg(short = 'o', long, default_value = "10000")] /// The query timeout in milliseconds. timeout: u64, + /// Enable matching listener. + #[cfg(feature = "unstable")] + #[arg(long)] + add_matching_listener: bool, #[command(flatten)] common: CommonArgs, } @@ -126,6 +134,7 @@ fn parse_args() -> ( Option, QueryTarget, Duration, + bool, ) { let args = Args::parse(); ( @@ -138,5 +147,9 @@ fn parse_args() -> ( Qt::AllComplete => QueryTarget::AllComplete, }, Duration::from_millis(args.timeout), + #[cfg(feature = "unstable")] + args.add_matching_listener, + #[cfg(not(feature = "unstable"))] + false, ) } From 280de0448352b35636ffcc6df7233399d22cc350 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Tue, 26 Nov 2024 15:14:37 +0100 Subject: [PATCH 23/30] add test for querier --- zenoh/tests/session.rs | 110 +++++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/zenoh/tests/session.rs b/zenoh/tests/session.rs index c94e681181..0aff08b7f7 100644 --- a/zenoh/tests/session.rs +++ b/zenoh/tests/session.rs @@ -26,7 +26,9 @@ use std::{ use zenoh::internal::runtime::{Runtime, RuntimeBuilder}; #[cfg(feature = "unstable")] use zenoh::qos::Reliability; -use zenoh::{key_expr::KeyExpr, qos::CongestionControl, sample::SampleKind, Session}; +use zenoh::{ + key_expr::KeyExpr, qos::CongestionControl, query::Querier, sample::SampleKind, Session, +}; use zenoh_core::ztimeout; #[cfg(not(feature = "unstable"))] use zenoh_protocol::core::Reliability; @@ -159,30 +161,60 @@ async fn test_session_pubsub(peer01: &Session, peer02: &Session, reliability: Re } } -async fn test_session_qryrep(peer01: &Session, peer02: &Session, reliability: Reliability) { - let key_expr = "test/session"; +trait HasGet { + async fn get(&self, params: &str) -> zenoh::handlers::FifoChannelHandler; +} + +struct SessionGetter<'a, 'b> { + session: &'a Session, + key_expr: &'b str, +} + +impl HasGet for SessionGetter<'_, '_> { + async fn get(&self, params: &str) -> zenoh::handlers::FifoChannelHandler { + let selector = format!("{}?{}", self.key_expr, params); + ztimeout!(self.session.get(selector)).unwrap() + } +} + +struct QuerierGetter<'a> { + querier: Querier<'a>, +} + +impl HasGet for QuerierGetter<'_> { + async fn get(&self, params: &str) -> zenoh::handlers::FifoChannelHandler { + ztimeout!(self.querier.get().parameters(params)).unwrap() + } +} + +async fn test_session_query_reply_internal( + peer: &Session, + key_expr: &str, + reliability: Reliability, + log_id: &str, + getter: &Getter, +) { let msg_count = match reliability { Reliability::Reliable => MSG_COUNT, Reliability::BestEffort => 1, }; - let msgs = Arc::new(AtomicUsize::new(0)); + let msgs = Arc::new(AtomicUsize::new(0)); for size in MSG_SIZE { msgs.store(0, Ordering::Relaxed); // Queryable to data - println!("[QR][01c] Queryable on peer01 session"); + println!("[{log_id}][01c] Queryable on peer01 session"); let c_msgs = msgs.clone(); - let qbl = ztimeout!(peer01.declare_queryable(key_expr).callback(move |query| { + let ke = key_expr.to_owned(); + let qbl = ztimeout!(peer.declare_queryable(key_expr).callback(move |query| { c_msgs.fetch_add(1, Ordering::Relaxed); match query.parameters().as_str() { "ok_put" => { tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async { - ztimeout!(query.reply( - KeyExpr::try_from(key_expr).unwrap(), - vec![0u8; size].to_vec() - )) + ztimeout!(query + .reply(KeyExpr::try_from(&ke).unwrap(), vec![0u8; size].to_vec())) .unwrap() }) }); @@ -190,7 +222,7 @@ async fn test_session_qryrep(peer01: &Session, peer02: &Session, reliability: Re "ok_del" => { tokio::task::block_in_place(|| { tokio::runtime::Handle::current() - .block_on(async { ztimeout!(query.reply_del(key_expr)).unwrap() }) + .block_on(async { ztimeout!(query.reply_del(&ke)).unwrap() }) }); } "err" => { @@ -209,11 +241,10 @@ async fn test_session_qryrep(peer01: &Session, peer02: &Session, reliability: Re tokio::time::sleep(SLEEP).await; // Get data - println!("[QR][02c] Getting Ok(Put) on peer02 session. {msg_count} msgs."); + println!("[{log_id}][02c] Getting Ok(Put) on peer02 session. {msg_count} msgs."); let mut cnt = 0; for _ in 0..msg_count { - let selector = format!("{}?ok_put", key_expr); - let rs = ztimeout!(peer02.get(selector)).unwrap(); + let rs = getter.get("ok_put").await; while let Ok(s) = ztimeout!(rs.recv_async()) { let s = s.result().unwrap(); assert_eq!(s.kind(), SampleKind::Put); @@ -221,17 +252,16 @@ async fn test_session_qryrep(peer01: &Session, peer02: &Session, reliability: Re cnt += 1; } } - println!("[QR][02c] Got on peer02 session. {cnt}/{msg_count} msgs."); + println!("[{log_id}][02c] Got on peer02 session. {cnt}/{msg_count} msgs."); assert_eq!(msgs.load(Ordering::Relaxed), msg_count); assert_eq!(cnt, msg_count); msgs.store(0, Ordering::Relaxed); - println!("[QR][03c] Getting Ok(Delete) on peer02 session. {msg_count} msgs."); + println!("[{log_id}][03c] Getting Ok(Delete) on peer02 session. {msg_count} msgs."); let mut cnt = 0; for _ in 0..msg_count { - let selector = format!("{}?ok_del", key_expr); - let rs = ztimeout!(peer02.get(selector)).unwrap(); + let rs = getter.get("ok_del").await; while let Ok(s) = ztimeout!(rs.recv_async()) { let s = s.result().unwrap(); assert_eq!(s.kind(), SampleKind::Delete); @@ -239,28 +269,27 @@ async fn test_session_qryrep(peer01: &Session, peer02: &Session, reliability: Re cnt += 1; } } - println!("[QR][03c] Got on peer02 session. {cnt}/{msg_count} msgs."); + println!("[{log_id}][03c] Got on peer02 session. {cnt}/{msg_count} msgs."); assert_eq!(msgs.load(Ordering::Relaxed), msg_count); assert_eq!(cnt, msg_count); msgs.store(0, Ordering::Relaxed); - println!("[QR][04c] Getting Err() on peer02 session. {msg_count} msgs."); + println!("[{log_id}][04c] Getting Err() on peer02 session. {msg_count} msgs."); let mut cnt = 0; for _ in 0..msg_count { - let selector = format!("{}?err", key_expr); - let rs = ztimeout!(peer02.get(selector)).unwrap(); + let rs = getter.get("err").await; while let Ok(s) = ztimeout!(rs.recv_async()) { let e = s.result().unwrap_err(); assert_eq!(e.payload().len(), size); cnt += 1; } } - println!("[QR][04c] Got on peer02 session. {cnt}/{msg_count} msgs."); + println!("[{log_id}][04c] Got on peer02 session. {cnt}/{msg_count} msgs."); assert_eq!(msgs.load(Ordering::Relaxed), msg_count); assert_eq!(cnt, msg_count); - println!("[PS][03c] Unqueryable on peer01 session"); + println!("[{log_id}][03c] Unqueryable on peer01 session"); ztimeout!(qbl.undeclare()).unwrap(); // Wait for the declaration to propagate @@ -268,12 +297,43 @@ async fn test_session_qryrep(peer01: &Session, peer02: &Session, reliability: Re } } +async fn test_session_getrep(peer01: &Session, peer02: &Session, reliability: Reliability) { + let key_expr = "test/session"; + let querier = SessionGetter { + session: peer02, + key_expr, + }; + ztimeout!(test_session_query_reply_internal( + peer01, + key_expr, + reliability, + "QR", + &querier + )) +} + +async fn test_session_qrrep(peer01: &Session, peer02: &Session, reliability: Reliability) { + let key_expr = "test/session"; + println!("[QQ][00c] Declaring Querier on peer02 session"); + let querier = QuerierGetter { + querier: ztimeout!(peer02.declare_querier(key_expr)).unwrap(), + }; + ztimeout!(test_session_query_reply_internal( + peer01, + key_expr, + reliability, + "QQ", + &querier + )) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn zenoh_session_unicast() { zenoh::init_log_from_env_or("error"); let (peer01, peer02) = open_session_unicast(&["tcp/127.0.0.1:17447"]).await; test_session_pubsub(&peer01, &peer02, Reliability::Reliable).await; - test_session_qryrep(&peer01, &peer02, Reliability::Reliable).await; + test_session_getrep(&peer01, &peer02, Reliability::Reliable).await; + test_session_qrrep(&peer01, &peer02, Reliability::Reliable).await; close_session(peer01, peer02).await; } From 4fb909134b81e1089a55bc12ca21c466f5ef6e4e Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Wed, 27 Nov 2024 23:25:01 +0100 Subject: [PATCH 24/30] add test for matching listener/status --- zenoh/src/api/session.rs | 2 +- zenoh/tests/matching.rs | 373 ++++++++++++++++++--------------------- 2 files changed, 168 insertions(+), 207 deletions(-) diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 93d4ced556..5a417e1f57 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -2078,7 +2078,7 @@ impl SessionInner { if let Ok(status) = session.matching_status( &msub.key_expr, msub.destination, - match_type, + msub.match_type, ) { if status.matching() == status_value { *current = status_value; diff --git a/zenoh/tests/matching.rs b/zenoh/tests/matching.rs index 1a4c03bd56..077e9f4f48 100644 --- a/zenoh/tests/matching.rs +++ b/zenoh/tests/matching.rs @@ -15,7 +15,12 @@ use std::time::Duration; -use zenoh::{sample::Locality, Result as ZResult, Session}; +use zenoh::{ + handlers::FifoChannelHandler, + pubsub::{MatchingListener, MatchingStatus}, + sample::Locality, + Result as ZResult, Session, +}; use zenoh_config::{ModeDependentValue, WhatAmI}; use zenoh_core::ztimeout; @@ -45,243 +50,199 @@ async fn create_session_pair(locator: &str) -> (Session, Session) { (session1, session2) } -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn zenoh_matching_status_any() -> ZResult<()> { - zenoh_util::init_log_from_env_or("error"); - let (session1, session2) = create_session_pair("tcp/127.0.0.1:18001").await; - - let publisher1 = ztimeout!(session1 - .declare_publisher("zenoh_matching_status_any_test") - .allowed_destination(Locality::Any)) - .unwrap(); - - let matching_listener = ztimeout!(publisher1.matching_listener()).unwrap(); - +fn get_matching_listener_status( + matching_listener: &MatchingListener>, +) -> Option { let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - let sub = ztimeout!(session1.declare_subscriber("zenoh_matching_status_any_test")).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(true))); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching()); - - ztimeout!(sub.undeclare()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(false))); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - let sub = ztimeout!(session2.declare_subscriber("zenoh_matching_status_any_test")).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(true))); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching()); - - ztimeout!(sub.undeclare()).unwrap(); + received_status.ok().flatten().map(|s| s.matching()) +} - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(false))); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - Ok(()) +fn is_locality_compatible(locality: Locality, same_session: bool) -> bool { + match locality { + Locality::SessionLocal => same_session, + Locality::Remote => !same_session, + Locality::Any => true, + } } -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn zenoh_matching_status_remote() -> ZResult<()> { +async fn zenoh_querier_matching_status_inner(querier_locality: Locality, same_session: bool) { + println!( + "Querier origin :{:?}, same session: {same_session}", + querier_locality + ); zenoh_util::init_log_from_env_or("error"); + let key_expr = match querier_locality { + Locality::SessionLocal => "zenoh_querier_matching_status_local_test", + Locality::Remote => "zenoh_querier_matching_status_remote_test", + Locality::Any => "zenoh_querier_matching_status_any_test", + }; - let session1 = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); - let session2 = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); + let (session1, session2) = match same_session { + false => create_session_pair("tcp/127.0.0.1:18002").await, + true => { + let s1 = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); + let s2 = s1.clone(); + (s1, s2) + } + }; + let locality_compatible = is_locality_compatible(querier_locality, same_session); - let publisher1 = ztimeout!(session1 - .declare_publisher("zenoh_matching_status_remote_test") - .allowed_destination(Locality::Remote)) + let querier1 = ztimeout!(session1 + .declare_querier(format!("{key_expr}/value")) + .target(zenoh::query::QueryTarget::BestMatching) + .allowed_destination(querier_locality)) .unwrap(); - let matching_listener = ztimeout!(publisher1.matching_listener()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - let sub = ztimeout!(session1.declare_subscriber("zenoh_matching_status_remote_test")).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - ztimeout!(sub.undeclare()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - let sub = ztimeout!(session2.declare_subscriber("zenoh_matching_status_remote_test")).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(true))); + let querier2 = ztimeout!(session1 + .declare_querier(format!("{key_expr}/*")) + .target(zenoh::query::QueryTarget::AllComplete) + .allowed_destination(querier_locality)) + .unwrap(); - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching()); + let matching_listener1 = ztimeout!(querier1.matching_listener()).unwrap(); + let matching_listener2 = ztimeout!(querier2.matching_listener()).unwrap(); - ztimeout!(sub.undeclare()).unwrap(); + assert!(!ztimeout!(querier1.matching_status()).unwrap().matching()); + assert!(!ztimeout!(querier2.matching_status()).unwrap().matching()); - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(false))); + assert_eq!(get_matching_listener_status(&matching_listener1), None); + assert_eq!(get_matching_listener_status(&matching_listener2), None); - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); + let qbl1 = ztimeout!(session2 + .declare_queryable(format!("{key_expr}/value")) + .complete(false)) + .unwrap(); + // There is a bug that hats do not distinguish whether they register/unregister complete or incomplete queryable, + // if there are more than one with the same keyexpr on the same face + //let qbl2 = ztimeout!(session2 + // .declare_queryable(format!("{key_expr}/*")) + // .complete(false)) + //.unwrap(); + + let _qbl3 = ztimeout!(session2 + .declare_queryable(format!("{key_expr}/value3")) + .complete(true)) + .unwrap(); - Ok(()) + assert_eq!( + get_matching_listener_status(&matching_listener1), + locality_compatible.then_some(true) + ); + assert_eq!(get_matching_listener_status(&matching_listener2), None); + + assert_eq!( + ztimeout!(querier1.matching_status()).unwrap().matching(), + locality_compatible + ); + assert!(!ztimeout!(querier2.matching_status()).unwrap().matching()); + + let qbl4 = ztimeout!(session2 + .declare_queryable(format!("{key_expr}/*")) + .complete(true)) + .unwrap(); + assert_eq!( + get_matching_listener_status(&matching_listener2), + locality_compatible.then_some(true) + ); + assert_eq!( + ztimeout!(querier2.matching_status()).unwrap().matching(), + locality_compatible + ); + + ztimeout!(qbl4.undeclare()).unwrap(); + + assert_eq!(get_matching_listener_status(&matching_listener1), None); + assert_eq!( + get_matching_listener_status(&matching_listener2), + locality_compatible.then_some(false) + ); + assert_eq!( + ztimeout!(querier1.matching_status()).unwrap().matching(), + locality_compatible + ); + assert!(!ztimeout!(querier2.matching_status()).unwrap().matching()); + + ztimeout!(qbl1.undeclare()).unwrap(); + // ztimeout!(qbl2.undeclare()).unwrap(); + assert_eq!( + get_matching_listener_status(&matching_listener1), + locality_compatible.then_some(false) + ); + assert_eq!(get_matching_listener_status(&matching_listener2), None); + assert!(!ztimeout!(querier1.matching_status()).unwrap().matching()); + assert!(!ztimeout!(querier2.matching_status()).unwrap().matching()); } -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn zenoh_matching_status_local() -> ZResult<()> { +async fn zenoh_publisher_matching_status_inner(publisher_locality: Locality, same_session: bool) { + println!( + "Publisher origin: {:?}, same session: {same_session}", + publisher_locality + ); zenoh_util::init_log_from_env_or("error"); + let key_expr = match publisher_locality { + Locality::SessionLocal => "zenoh_publisher_matching_status_local_test", + Locality::Remote => "zenoh_publisher_matching_status_remote_test", + Locality::Any => "zenoh_publisher_matching_status_any_test", + }; - let session1 = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); - let session2 = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); + let (session1, session2) = match same_session { + false => create_session_pair("tcp/127.0.0.1:18001").await, + true => { + let s1 = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); + let s2 = s1.clone(); + (s1, s2) + } + }; + let locality_compatible = is_locality_compatible(publisher_locality, same_session); - let publisher1 = ztimeout!(session1 - .declare_publisher("zenoh_matching_status_local_test") - .allowed_destination(Locality::SessionLocal)) + let publisher = ztimeout!(session1 + .declare_publisher(format!("{key_expr}/*")) + .allowed_destination(publisher_locality)) .unwrap(); - let matching_listener = ztimeout!(publisher1.matching_listener()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - let sub = ztimeout!(session1.declare_subscriber("zenoh_matching_status_local_test")).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(true))); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(matching_status.matching()); - - ztimeout!(sub.undeclare()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(false))); + let matching_listener = ztimeout!(publisher.matching_listener()).unwrap(); - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); + assert_eq!(get_matching_listener_status(&matching_listener), None); - let sub = ztimeout!(session2.declare_subscriber("zenoh_matching_status_local_test")).unwrap(); + let sub = ztimeout!(session2.declare_subscriber(format!("{key_expr}/value"))).unwrap(); - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); + assert_eq!( + get_matching_listener_status(&matching_listener), + locality_compatible.then_some(true) + ); + assert_eq!( + ztimeout!(publisher.matching_status()).unwrap().matching(), + locality_compatible + ); ztimeout!(sub.undeclare()).unwrap(); - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher1.matching_status()).unwrap(); - assert!(!matching_status.matching()); + assert_eq!( + get_matching_listener_status(&matching_listener), + locality_compatible.then_some(false) + ); +} +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn zenoh_querier_matching_status() -> ZResult<()> { + zenoh_util::init_log_from_env_or("error"); + zenoh_querier_matching_status_inner(Locality::Any, true).await; + zenoh_querier_matching_status_inner(Locality::Any, false).await; + zenoh_querier_matching_status_inner(Locality::Remote, true).await; + zenoh_querier_matching_status_inner(Locality::Remote, false).await; + zenoh_querier_matching_status_inner(Locality::SessionLocal, true).await; + zenoh_querier_matching_status_inner(Locality::SessionLocal, false).await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn zenoh_matching_status_session_local() -> ZResult<()> { +async fn zenoh_publisher_matching_status() -> ZResult<()> { zenoh_util::init_log_from_env_or("error"); - - let session = ztimeout!(zenoh::open(zenoh::Config::default())).unwrap(); - - let publisher = - ztimeout!(session.declare_publisher("zenoh_matching_status_session_local_test")).unwrap(); - - let matching_listener = ztimeout!(publisher.matching_listener()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status.unwrap().is_none()); - - let matching_status = ztimeout!(publisher.matching_status()).unwrap(); - assert!(!matching_status.matching()); - - let sub = ztimeout!(session - .declare_subscriber("zenoh_matching_status_session_local_test") - .allowed_origin(Locality::SessionLocal)) - .unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(true))); - - let matching_status = ztimeout!(publisher.matching_status()).unwrap(); - assert!(matching_status.matching()); - - ztimeout!(sub.undeclare()).unwrap(); - - let received_status = matching_listener.recv_timeout(RECV_TIMEOUT); - assert!(received_status - .ok() - .flatten() - .map(|s| s.matching()) - .eq(&Some(false))); - - let matching_status = ztimeout!(publisher.matching_status()).unwrap(); - assert!(!matching_status.matching()); - + zenoh_publisher_matching_status_inner(Locality::Any, true).await; + zenoh_publisher_matching_status_inner(Locality::Any, false).await; + zenoh_publisher_matching_status_inner(Locality::Remote, true).await; + zenoh_publisher_matching_status_inner(Locality::Remote, false).await; + zenoh_publisher_matching_status_inner(Locality::SessionLocal, true).await; + zenoh_publisher_matching_status_inner(Locality::SessionLocal, false).await; Ok(()) } From 3c2281bc2836aefd0cb890ddbfb18d3e7c3d70bf Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 28 Nov 2024 10:49:19 +0100 Subject: [PATCH 25/30] simplify MatchingListenerBuilder::with --- zenoh/src/api/builders/matching_listener.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index 4671a1e18e..4aa2ec4553 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -136,20 +136,12 @@ impl<'a> MatchingListenerBuilder<'a, DefaultHandler> { where Handler: IntoHandler, { - let MatchingListenerBuilder { - session, - key_expr, - destination, - matching_listeners, - matching_status_type, - handler: _, - } = self; MatchingListenerBuilder { - session, - key_expr, - destination, - matching_listeners, - matching_status_type, + session: self.session, + key_expr: self.key_expr, + destination: self.destination, + matching_listeners: self.matching_listeners, + matching_status_type: self.matching_status_type, handler, } } From 2699f9fdeb76d8880d93378719fdb073adff5524 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 28 Nov 2024 11:04:31 +0100 Subject: [PATCH 26/30] remove aggregated queriers --- commons/zenoh-config/src/lib.rs | 2 -- zenoh/src/api/builders/session.rs | 9 ------ zenoh/src/api/session.rs | 49 ++++++++----------------------- 3 files changed, 12 insertions(+), 48 deletions(-) diff --git a/commons/zenoh-config/src/lib.rs b/commons/zenoh-config/src/lib.rs index b8316cc0f1..d25ccc63c3 100644 --- a/commons/zenoh-config/src/lib.rs +++ b/commons/zenoh-config/src/lib.rs @@ -359,8 +359,6 @@ validated_struct::validator! { subscribers: Vec, /// A list of key-expressions for which all included publishers will be aggregated into. publishers: Vec, - /// A list of key-expressions for which all included queriers will be aggregated into. - queriers: Vec, }, pub transport: #[derive(Default)] TransportConf { diff --git a/zenoh/src/api/builders/session.rs b/zenoh/src/api/builders/session.rs index a694a860f9..0ada1f4a67 100644 --- a/zenoh/src/api/builders/session.rs +++ b/zenoh/src/api/builders/session.rs @@ -122,7 +122,6 @@ pub fn init(runtime: Runtime) -> InitBuilder { runtime, aggregated_subscribers: vec![], aggregated_publishers: vec![], - aggregated_queriers: vec![], } } @@ -134,7 +133,6 @@ pub struct InitBuilder { runtime: Runtime, aggregated_subscribers: Vec, aggregated_publishers: Vec, - aggregated_queriers: Vec, } #[zenoh_macros::internal] @@ -150,12 +148,6 @@ impl InitBuilder { self.aggregated_publishers = exprs; self } - - #[inline] - pub fn aggregated_queriers(mut self, exprs: Vec) -> Self { - self.aggregated_queriers = exprs; - self - } } #[zenoh_macros::internal] @@ -170,7 +162,6 @@ impl Wait for InitBuilder { self.runtime, self.aggregated_subscribers, self.aggregated_publishers, - self.aggregated_queriers, false, ) .wait()) diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 5a417e1f57..67521e5f01 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -149,14 +149,12 @@ pub(crate) struct SessionState { pub(crate) liveliness_queries: HashMap, pub(crate) aggregated_subscribers: Vec, pub(crate) aggregated_publishers: Vec, - pub(crate) aggregated_queriers: Vec, } impl SessionState { pub(crate) fn new( aggregated_subscribers: Vec, aggregated_publishers: Vec, - aggregated_queriers: Vec, ) -> SessionState { SessionState { primitives: None, @@ -187,7 +185,6 @@ impl SessionState { liveliness_queries: HashMap::new(), aggregated_subscribers, aggregated_publishers, - aggregated_queriers, } } } @@ -324,37 +321,19 @@ impl SessionState { destination, }; - let declared_querier = (destination != Locality::SessionLocal) - .then(|| { - match self - .aggregated_queriers - .iter() - .find(|s| s.includes(key_expr)) - { - Some(join_querier) => { - if let Some(joined_querier) = self.queriers.values().find(|q| { - q.destination != Locality::SessionLocal - && join_querier.includes(&q.key_expr) - }) { - querier_state.remote_id = joined_querier.remote_id; - None - } else { - Some(join_querier.clone().into()) - } - } - None => { - if let Some(twin_querier) = self.queriers.values().find(|p| { - p.destination != Locality::SessionLocal && &p.key_expr == key_expr - }) { - querier_state.remote_id = twin_querier.remote_id; - None - } else { - Some(key_expr.clone()) - } + let declared_querier = + (destination != Locality::SessionLocal) + .then(|| { + if let Some(twin_querier) = self.queriers.values().find(|p| { + p.destination != Locality::SessionLocal && &p.key_expr == key_expr + }) { + querier_state.remote_id = twin_querier.remote_id; + None + } else { + Some(key_expr.clone()) } - } - }) - .flatten(); + }) + .flatten(); self.queriers.insert(id, querier_state); declared_querier } @@ -680,7 +659,6 @@ impl Session { runtime: Runtime, aggregated_subscribers: Vec, aggregated_publishers: Vec, - aggregated_queriers: Vec, owns_runtime: bool, ) -> impl Resolve { ResolveClosure::new(move || { @@ -688,7 +666,6 @@ impl Session { let state = RwLock::new(SessionState::new( aggregated_subscribers, aggregated_publishers, - aggregated_queriers, )); let session = Session(Arc::new(SessionInner { weak_counter: Mutex::new(0), @@ -1239,7 +1216,6 @@ impl Session { tracing::debug!("Config: {:?}", &config); let aggregated_subscribers = config.0.aggregation().subscribers().clone(); let aggregated_publishers = config.0.aggregation().publishers().clone(); - let aggregated_queriers = config.0.aggregation().queriers().clone(); #[allow(unused_mut)] // Required for shared-memory let mut runtime = RuntimeBuilder::new(config); #[cfg(feature = "shared-memory")] @@ -1252,7 +1228,6 @@ impl Session { runtime.clone(), aggregated_subscribers, aggregated_publishers, - aggregated_queriers, true, ) .await; From 85f27ad62065cf4339586e36b2c30393cda25705 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 28 Nov 2024 11:29:08 +0100 Subject: [PATCH 27/30] moved all MatchingStatus/Listener functionality under separate module --- zenoh/src/api/builders/matching_listener.rs | 2 +- zenoh/src/api/matching.rs | 258 ++++++++++++++++++++ zenoh/src/api/mod.rs | 2 + zenoh/src/api/publisher.rs | 233 +----------------- zenoh/src/api/querier.rs | 8 +- zenoh/src/api/session.rs | 2 +- zenoh/src/lib.rs | 13 +- zenoh/tests/matching.rs | 2 +- 8 files changed, 279 insertions(+), 241 deletions(-) create mode 100644 zenoh/src/api/matching.rs diff --git a/zenoh/src/api/builders/matching_listener.rs b/zenoh/src/api/builders/matching_listener.rs index 4aa2ec4553..f052756dfd 100644 --- a/zenoh/src/api/builders/matching_listener.rs +++ b/zenoh/src/api/builders/matching_listener.rs @@ -22,7 +22,7 @@ use zenoh_result::ZResult; use { crate::api::{ handlers::{Callback, DefaultHandler, IntoHandler}, - publisher::{MatchingListener, MatchingListenerInner, MatchingStatus, MatchingStatusType}, + matching::{MatchingListener, MatchingListenerInner, MatchingStatus, MatchingStatusType}, Id, }, crate::sample::Locality, diff --git a/zenoh/src/api/matching.rs b/zenoh/src/api/matching.rs new file mode 100644 index 0000000000..46aa4a9b71 --- /dev/null +++ b/zenoh/src/api/matching.rs @@ -0,0 +1,258 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use std::{ + collections::HashSet, + fmt, + future::{IntoFuture, Ready}, + sync::{Arc, Mutex}, +}; + +use tracing::error; +use zenoh_core::{Resolvable, Wait}; +use zenoh_result::ZResult; + +use super::{ + handlers::Callback, + key_expr::KeyExpr, + sample::Locality, + session::{UndeclarableSealed, WeakSession}, + Id, +}; + +/// A struct that indicates if there exist entities matching the key expression. +/// +/// # Examples +/// ``` +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let publisher = session.declare_publisher("key/expression").await.unwrap(); +/// let matching_status = publisher.matching_status().await.unwrap(); +/// # } +/// ``` +#[zenoh_macros::unstable] +#[derive(Copy, Clone, Debug)] +pub struct MatchingStatus { + pub(crate) matching: bool, +} + +#[cfg(feature = "unstable")] +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) enum MatchingStatusType { + Subscribers, + Queryables(bool), +} + +#[zenoh_macros::unstable] +impl MatchingStatus { + /// Return true if there exist entities matching the target (i.e either Subscribers matching Publisher's key expression or Queryables matching Querier's key expression and target). + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").await.unwrap(); + /// let matching_subscribers: bool = publisher + /// .matching_status() + /// .await + /// .unwrap() + /// .matching(); + /// # } + /// ``` + pub fn matching(&self) -> bool { + self.matching + } +} +#[zenoh_macros::unstable] +pub(crate) struct MatchingListenerState { + pub(crate) id: Id, + pub(crate) current: Mutex, + pub(crate) key_expr: KeyExpr<'static>, + pub(crate) destination: Locality, + pub(crate) match_type: MatchingStatusType, + pub(crate) callback: Callback, +} + +#[cfg(feature = "unstable")] +impl MatchingListenerState { + pub(crate) fn is_matching(&self, key_expr: &KeyExpr, match_type: MatchingStatusType) -> bool { + match match_type { + MatchingStatusType::Subscribers => { + self.match_type == MatchingStatusType::Subscribers + && self.key_expr.intersects(key_expr) + } + MatchingStatusType::Queryables(false) => { + self.match_type == MatchingStatusType::Queryables(false) + && self.key_expr.intersects(key_expr) + } + MatchingStatusType::Queryables(true) => { + (self.match_type == MatchingStatusType::Queryables(false) + && self.key_expr.intersects(key_expr)) + || (self.match_type == MatchingStatusType::Queryables(true) + && key_expr.includes(&self.key_expr)) + } + } + } +} + +#[zenoh_macros::unstable] +impl fmt::Debug for MatchingListenerState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("MatchingListener") + .field("id", &self.id) + .field("key_expr", &self.key_expr) + .field("match_type", &self.match_type) + .finish() + } +} + +#[zenoh_macros::unstable] +pub(crate) struct MatchingListenerInner { + pub(crate) session: WeakSession, + pub(crate) matching_listeners: Arc>>, + pub(crate) id: Id, + pub(crate) undeclare_on_drop: bool, +} + +/// A listener that sends notifications when the [`MatchingStatus`] of a +/// publisher changes. +/// +/// Callback matching listeners will run in background until the publisher is undeclared, +/// or until it is undeclared. +/// On the other hand, matching listener with a handler are automatically undeclared when dropped. +/// +/// # Examples +/// ```no_run +/// # #[tokio::main] +/// # async fn main() { +/// +/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); +/// let publisher = session.declare_publisher("key/expression").await.unwrap(); +/// let matching_listener = publisher.matching_listener().await.unwrap(); +/// while let Ok(matching_status) = matching_listener.recv_async().await { +/// if matching_status.matching() { +/// println!("Publisher has matching subscribers."); +/// } else { +/// println!("Publisher has NO MORE matching subscribers."); +/// } +/// } +/// # } +/// ``` +#[zenoh_macros::unstable] +pub struct MatchingListener { + pub(crate) inner: MatchingListenerInner, + pub(crate) handler: Handler, +} + +#[zenoh_macros::unstable] +impl MatchingListener { + /// Undeclare the [`MatchingListener`]. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").await.unwrap(); + /// let matching_listener = publisher.matching_listener().await.unwrap(); + /// matching_listener.undeclare().await.unwrap(); + /// # } + /// ``` + #[inline] + pub fn undeclare(self) -> MatchingListenerUndeclaration + where + Handler: Send, + { + self.undeclare_inner(()) + } + + fn undeclare_impl(&mut self) -> ZResult<()> { + // set the flag first to avoid double panic if this function panic + self.inner.undeclare_on_drop = false; + zlock!(self.inner.matching_listeners).remove(&self.inner.id); + self.inner + .session + .undeclare_matches_listener_inner(self.inner.id) + } + + #[zenoh_macros::internal] + pub fn set_background(&mut self, background: bool) { + self.inner.undeclare_on_drop = !background; + } +} + +#[cfg(feature = "unstable")] +impl Drop for MatchingListener { + fn drop(&mut self) { + if self.inner.undeclare_on_drop { + if let Err(error) = self.undeclare_impl() { + error!(error); + } + } + } +} + +#[zenoh_macros::unstable] +impl UndeclarableSealed<()> for MatchingListener { + type Undeclaration = MatchingListenerUndeclaration; + + fn undeclare_inner(self, _: ()) -> Self::Undeclaration { + MatchingListenerUndeclaration(self) + } +} + +#[zenoh_macros::unstable] +impl std::ops::Deref for MatchingListener { + type Target = Handler; + + fn deref(&self) -> &Self::Target { + &self.handler + } +} +#[zenoh_macros::unstable] +impl std::ops::DerefMut for MatchingListener { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.handler + } +} + +#[zenoh_macros::unstable] +pub struct MatchingListenerUndeclaration(MatchingListener); + +#[zenoh_macros::unstable] +impl Resolvable for MatchingListenerUndeclaration { + type To = ZResult<()>; +} + +#[zenoh_macros::unstable] +impl Wait for MatchingListenerUndeclaration { + fn wait(mut self) -> ::To { + self.0.undeclare_impl() + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for MatchingListenerUndeclaration { + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} diff --git a/zenoh/src/api/mod.rs b/zenoh/src/api/mod.rs index 95c1ea7e52..41278e4ac3 100644 --- a/zenoh/src/api/mod.rs +++ b/zenoh/src/api/mod.rs @@ -26,6 +26,8 @@ pub(crate) mod key_expr; pub(crate) mod liveliness; #[cfg(feature = "plugins")] pub(crate) mod loader; +#[cfg(feature = "unstable")] +pub(crate) mod matching; #[cfg(feature = "plugins")] pub(crate) mod plugins; pub(crate) mod publisher; diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index 1c0376f486..a54374324b 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -29,7 +29,8 @@ use zenoh_result::{Error, ZResult}; use { crate::api::{ builders::matching_listener::MatchingListenerBuilder, - handlers::{Callback, DefaultHandler}, + handlers::DefaultHandler, + matching::{MatchingStatus, MatchingStatusType}, sample::SourceInfo, }, std::{collections::HashSet, sync::Arc, sync::Mutex}, @@ -250,9 +251,9 @@ impl<'a> Publisher<'a> { }) } - /// Return a [`MatchingListener`] for this Publisher. + /// Return a [`MatchingListener`](crate::api::matching::MatchingListener) for this Publisher. /// - /// The [`MatchingListener`] that will send a notification each time the [`MatchingStatus`] of + /// The [`MatchingListener`](crate::api::matching::MatchingListener) that will send a notification each time the [`MatchingStatus`](crate::api::matching::MatchingStatus) of /// the Publisher changes. /// /// # Examples @@ -507,232 +508,6 @@ impl TryFrom for Priority { } } -/// A struct that indicates if there exist entities matching the key expression. -/// -/// # Examples -/// ``` -/// # #[tokio::main] -/// # async fn main() { -/// -/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); -/// let publisher = session.declare_publisher("key/expression").await.unwrap(); -/// let matching_status = publisher.matching_status().await.unwrap(); -/// # } -/// ``` -#[zenoh_macros::unstable] -#[derive(Copy, Clone, Debug)] -pub struct MatchingStatus { - pub(crate) matching: bool, -} - -#[cfg(feature = "unstable")] -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum MatchingStatusType { - Subscribers, - Queryables(bool), -} - -#[zenoh_macros::unstable] -impl MatchingStatus { - /// Return true if there exist entities matching the target (i.e either Subscribers matching Publisher's key expression or Queryables matching Querier's key expression and target). - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let publisher = session.declare_publisher("key/expression").await.unwrap(); - /// let matching_subscribers: bool = publisher - /// .matching_status() - /// .await - /// .unwrap() - /// .matching(); - /// # } - /// ``` - pub fn matching(&self) -> bool { - self.matching - } -} -#[zenoh_macros::unstable] -pub(crate) struct MatchingListenerState { - pub(crate) id: Id, - pub(crate) current: Mutex, - pub(crate) key_expr: KeyExpr<'static>, - pub(crate) destination: Locality, - pub(crate) match_type: MatchingStatusType, - pub(crate) callback: Callback, -} - -#[cfg(feature = "unstable")] -impl MatchingListenerState { - pub(crate) fn is_matching(&self, key_expr: &KeyExpr, match_type: MatchingStatusType) -> bool { - match match_type { - MatchingStatusType::Subscribers => { - self.match_type == MatchingStatusType::Subscribers - && self.key_expr.intersects(key_expr) - } - MatchingStatusType::Queryables(false) => { - self.match_type == MatchingStatusType::Queryables(false) - && self.key_expr.intersects(key_expr) - } - MatchingStatusType::Queryables(true) => { - (self.match_type == MatchingStatusType::Queryables(false) - && self.key_expr.intersects(key_expr)) - || (self.match_type == MatchingStatusType::Queryables(true) - && key_expr.includes(&self.key_expr)) - } - } - } -} - -#[zenoh_macros::unstable] -impl fmt::Debug for MatchingListenerState { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("MatchingListener") - .field("id", &self.id) - .field("key_expr", &self.key_expr) - .field("match_type", &self.match_type) - .finish() - } -} - -#[zenoh_macros::unstable] -pub(crate) struct MatchingListenerInner { - pub(crate) session: WeakSession, - pub(crate) matching_listeners: Arc>>, - pub(crate) id: Id, - pub(crate) undeclare_on_drop: bool, -} - -/// A listener that sends notifications when the [`MatchingStatus`] of a -/// publisher changes. -/// -/// Callback matching listeners will run in background until the publisher is undeclared, -/// or until it is undeclared. -/// On the other hand, matching listener with a handler are automatically undeclared when dropped. -/// -/// # Examples -/// ```no_run -/// # #[tokio::main] -/// # async fn main() { -/// -/// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); -/// let publisher = session.declare_publisher("key/expression").await.unwrap(); -/// let matching_listener = publisher.matching_listener().await.unwrap(); -/// while let Ok(matching_status) = matching_listener.recv_async().await { -/// if matching_status.matching() { -/// println!("Publisher has matching subscribers."); -/// } else { -/// println!("Publisher has NO MORE matching subscribers."); -/// } -/// } -/// # } -/// ``` -#[zenoh_macros::unstable] -pub struct MatchingListener { - pub(crate) inner: MatchingListenerInner, - pub(crate) handler: Handler, -} - -#[zenoh_macros::unstable] -impl MatchingListener { - /// Undeclare the [`MatchingListener`]. - /// - /// # Examples - /// ``` - /// # #[tokio::main] - /// # async fn main() { - /// - /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); - /// let publisher = session.declare_publisher("key/expression").await.unwrap(); - /// let matching_listener = publisher.matching_listener().await.unwrap(); - /// matching_listener.undeclare().await.unwrap(); - /// # } - /// ``` - #[inline] - pub fn undeclare(self) -> MatchingListenerUndeclaration - where - Handler: Send, - { - self.undeclare_inner(()) - } - - fn undeclare_impl(&mut self) -> ZResult<()> { - // set the flag first to avoid double panic if this function panic - self.inner.undeclare_on_drop = false; - zlock!(self.inner.matching_listeners).remove(&self.inner.id); - self.inner - .session - .undeclare_matches_listener_inner(self.inner.id) - } - - #[zenoh_macros::internal] - pub fn set_background(&mut self, background: bool) { - self.inner.undeclare_on_drop = !background; - } -} - -#[cfg(feature = "unstable")] -impl Drop for MatchingListener { - fn drop(&mut self) { - if self.inner.undeclare_on_drop { - if let Err(error) = self.undeclare_impl() { - error!(error); - } - } - } -} - -#[zenoh_macros::unstable] -impl UndeclarableSealed<()> for MatchingListener { - type Undeclaration = MatchingListenerUndeclaration; - - fn undeclare_inner(self, _: ()) -> Self::Undeclaration { - MatchingListenerUndeclaration(self) - } -} - -#[zenoh_macros::unstable] -impl std::ops::Deref for MatchingListener { - type Target = Handler; - - fn deref(&self) -> &Self::Target { - &self.handler - } -} -#[zenoh_macros::unstable] -impl std::ops::DerefMut for MatchingListener { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.handler - } -} - -#[zenoh_macros::unstable] -pub struct MatchingListenerUndeclaration(MatchingListener); - -#[zenoh_macros::unstable] -impl Resolvable for MatchingListenerUndeclaration { - type To = ZResult<()>; -} - -#[zenoh_macros::unstable] -impl Wait for MatchingListenerUndeclaration { - fn wait(mut self) -> ::To { - self.0.undeclare_impl() - } -} - -#[zenoh_macros::unstable] -impl IntoFuture for MatchingListenerUndeclaration { - type Output = ::To; - type IntoFuture = Ready<::To>; - - fn into_future(self) -> Self::IntoFuture { - std::future::ready(self.wait()) - } -} - #[cfg(test)] mod tests { use crate::{sample::SampleKind, Config, Wait}; diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index b20dde5212..d84c2f189a 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -27,9 +27,9 @@ use zenoh_protocol::{ use zenoh_result::ZResult; #[cfg(feature = "unstable")] use { - crate::api::publisher::{MatchingStatus, MatchingStatusType}, + crate::api::builders::matching_listener::MatchingListenerBuilder, + crate::api::matching::{MatchingStatus, MatchingStatusType}, crate::api::sample::SourceInfo, - crate::pubsub::MatchingListenerBuilder, crate::query::ReplyKeyExpr, std::collections::HashSet, std::sync::{Arc, Mutex}, @@ -226,9 +226,9 @@ impl<'a> Querier<'a> { }) } - /// Return a [`MatchingListener`](crate::api::publisher::MatchingListener) for this Querier. + /// Return a [`MatchingListener`](crate::api::matching::MatchingListener) for this Querier. /// - /// The [`MatchingListener`](crate::api::publisher::MatchingListener) that will send a notification each time the [`MatchingStatus`](crate::api::publisher::MatchingStatus) of + /// The [`MatchingListener`](crate::api::matching::MatchingListener) that will send a notification each time the [`MatchingStatus`](crate::api::matching::MatchingStatus) of /// the Querier changes. /// /// # Examples diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 67521e5f01..0edb050321 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -72,7 +72,7 @@ use crate::api::selector::ZenohParameters; #[cfg(feature = "unstable")] use crate::api::{ liveliness::{Liveliness, LivelinessTokenState}, - publisher::{MatchingListenerState, MatchingStatus, MatchingStatusType}, + matching::{MatchingListenerState, MatchingStatus, MatchingStatusType}, query::{LivelinessQueryState, ReplyKeyExpr}, sample::SourceInfo, }; diff --git a/zenoh/src/lib.rs b/zenoh/src/lib.rs index e43f46770e..54ccdebb9a 100644 --- a/zenoh/src/lib.rs +++ b/zenoh/src/lib.rs @@ -249,11 +249,6 @@ pub mod bytes { /// declared by a [`Session::declare_subscriber`](crate::Session::declare_subscriber) /// pub mod pubsub { - #[zenoh_macros::unstable] - pub use crate::api::{ - builders::matching_listener::MatchingListenerBuilder, - publisher::{MatchingListener, MatchingListenerUndeclaration, MatchingStatus}, - }; pub use crate::api::{ builders::{ publisher::{ @@ -296,6 +291,14 @@ pub mod query { pub use crate::api::{query::ReplyKeyExpr, selector::ZenohParameters}; } +#[zenoh_macros::unstable] +pub mod matching { + pub use crate::api::{ + builders::matching_listener::MatchingListenerBuilder, + matching::{MatchingListener, MatchingListenerUndeclaration, MatchingStatus}, + }; +} + /// Callback handler trait. /// /// Zenoh primitives that receive data (e.g., [`Subscriber`](crate::pubsub::Subscriber), diff --git a/zenoh/tests/matching.rs b/zenoh/tests/matching.rs index 077e9f4f48..503c1f5e4b 100644 --- a/zenoh/tests/matching.rs +++ b/zenoh/tests/matching.rs @@ -17,7 +17,7 @@ use std::time::Duration; use zenoh::{ handlers::FifoChannelHandler, - pubsub::{MatchingListener, MatchingStatus}, + matching::{MatchingListener, MatchingStatus}, sample::Locality, Result as ZResult, Session, }; From 1d680d910fbb3bdc427e76613251813b30d737ab Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Thu, 28 Nov 2024 18:56:52 +0100 Subject: [PATCH 28/30] fixed z_querier example to accept selector instead of keyexpr --- examples/examples/z_querier.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/examples/z_querier.rs b/examples/examples/z_querier.rs index 069b72c3a2..d19f2e81e8 100644 --- a/examples/examples/z_querier.rs +++ b/examples/examples/z_querier.rs @@ -14,7 +14,10 @@ use std::time::Duration; use clap::Parser; -use zenoh::{key_expr::KeyExpr, query::QueryTarget, Config}; +use zenoh::{ + query::{QueryTarget, Selector}, + Config, +}; use zenoh_examples::CommonArgs; #[tokio::main] @@ -22,16 +25,16 @@ async fn main() { // initiate logging zenoh::init_log_from_env_or("error"); #[cfg(feature = "unstable")] - let (config, keyexpr, payload, target, timeout, add_matching_listener) = parse_args(); + let (config, selector, payload, target, timeout, add_matching_listener) = parse_args(); #[cfg(not(feature = "unstable"))] - let (config, keyexpr, payload, target, timeout, _) = parse_args(); + let (config, selector, payload, target, timeout, _) = parse_args(); println!("Opening session..."); let session = zenoh::open(config).await.unwrap(); - println!("Declaring Querier on '{keyexpr}'..."); + println!("Declaring Querier on '{}'...", selector.key_expr()); let querier = session - .declare_querier(keyexpr) + .declare_querier(selector.key_expr()) .target(target) .timeout(timeout) .await @@ -53,15 +56,13 @@ async fn main() { .unwrap(); } + let params = selector.parameters().as_str(); + println!("Press CTRL-C to quit..."); for idx in 0..u32::MAX { tokio::time::sleep(Duration::from_secs(1)).await; let buf = format!("[{idx:4}] {}", payload.clone().unwrap_or_default()); - println!( - "Querying '{}' with payload: '{}')...", - &querier.key_expr(), - buf - ); + println!("Querying '{}' with payload: '{}'...", &selector, buf); let replies = querier .get() // // By default get receives replies from a FIFO. @@ -70,6 +71,7 @@ async fn main() { // .with(zenoh::handlers::RingChannel::default()) // Refer to z_bytes.rs to see how to serialize different types of message .payload(buf) + .parameters(params) .await .unwrap(); while let Ok(reply) = replies.recv_async().await { @@ -110,7 +112,7 @@ enum Qt { struct Args { #[arg(short, long, default_value = "demo/example/**")] /// The selection of resources to query - key_expr: KeyExpr<'static>, + selector: Selector<'static>, #[arg(short, long)] /// An optional payload to put in the query. payload: Option, @@ -130,7 +132,7 @@ struct Args { fn parse_args() -> ( Config, - KeyExpr<'static>, + Selector<'static>, Option, QueryTarget, Duration, @@ -139,7 +141,7 @@ fn parse_args() -> ( let args = Args::parse(); ( args.common.into(), - args.key_expr, + args.selector, args.payload, match args.target { Qt::BestMatching => QueryTarget::BestMatching, From b14c4792f210eb46d107b3adf6c437da95684fb5 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Fri, 29 Nov 2024 15:29:18 +0100 Subject: [PATCH 29/30] new clippy fixes --- zenoh/src/api/builders/querier.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zenoh/src/api/builders/querier.rs b/zenoh/src/api/builders/querier.rs index a5b38f9f07..a3134d3d60 100644 --- a/zenoh/src/api/builders/querier.rs +++ b/zenoh/src/api/builders/querier.rs @@ -103,7 +103,7 @@ impl QoSBuilderTrait for QuerierBuilder<'_, '_> { } } -impl<'a, 'b> QuerierBuilder<'a, 'b> { +impl QuerierBuilder<'_, '_> { /// Change the target of the querier queries. #[inline] pub fn target(self, target: QueryTarget) -> Self { @@ -150,11 +150,11 @@ impl<'a, 'b> QuerierBuilder<'a, 'b> { } } -impl<'a, 'b> Resolvable for QuerierBuilder<'a, 'b> { +impl<'b> Resolvable for QuerierBuilder<'_, 'b> { type To = ZResult>; } -impl<'a, 'b> Wait for QuerierBuilder<'a, 'b> { +impl Wait for QuerierBuilder<'_, '_> { fn wait(self) -> ::To { let mut key_expr = self.key_expr?; if !key_expr.is_fully_optimized(&self.session.0) { @@ -182,7 +182,7 @@ impl<'a, 'b> Wait for QuerierBuilder<'a, 'b> { } } -impl<'a, 'b> IntoFuture for QuerierBuilder<'a, 'b> { +impl IntoFuture for QuerierBuilder<'_, '_> { type Output = ::To; type IntoFuture = Ready<::To>; @@ -370,7 +370,7 @@ impl<'a, 'b> QuerierGetBuilder<'a, 'b, DefaultHandler> { } } } -impl<'a, 'b, Handler> QuerierGetBuilder<'a, 'b, Handler> { +impl<'b, Handler> QuerierGetBuilder<'_, 'b, Handler> { /// Set the query payload. #[inline] pub fn payload(mut self, payload: IntoZBytes) -> Self From d2dd91208733d951763abad3a5dd130e93a07af9 Mon Sep 17 00:00:00 2001 From: Denis Biryukov Date: Mon, 2 Dec 2024 17:41:39 +0100 Subject: [PATCH 30/30] mark querier related features as unstable --- examples/Cargo.toml | 5 +++++ examples/README.md | 18 ++++++++++++++++++ zenoh/src/api/builders/mod.rs | 1 + zenoh/src/api/builders/querier.rs | 8 +++++++- zenoh/src/api/matching.rs | 4 ++-- zenoh/src/api/mod.rs | 1 + zenoh/src/api/querier.rs | 8 +++++++- zenoh/src/api/session.rs | 10 ++++++++-- zenoh/src/lib.rs | 12 +++++++----- zenoh/tests/session.rs | 10 +++++++--- 10 files changed, 63 insertions(+), 14 deletions(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 687f4790c1..af948d5b27 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -93,6 +93,11 @@ required-features = ["unstable", "shared-memory"] name = "z_pull" path = "examples/z_pull.rs" +[[example]] +name = "z_querier" +path = "examples/z_querier.rs" +required-features = ["unstable"] + [[example]] name = "z_queryable" path = "examples/z_queryable.rs" diff --git a/examples/README.md b/examples/README.md index 21d1a0ab34..c0c7b4eed3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -123,6 +123,24 @@ z_get -s 'demo/**' ``` +### z_querier + + Continuously sends query messages for a selector. + The queryables with a matching path or selector (for instance [z_queryable](#z_queryable) and [z_storage](#z_storage)) + will receive these queries and reply with paths/values that will be received by the querier. + + Typical usage: + + ```bash + z_querier + ``` + + or + + ```bash + z_querier -s 'demo/**' + ``` + ### z_queryable Declares a queryable function with a path. diff --git a/zenoh/src/api/builders/mod.rs b/zenoh/src/api/builders/mod.rs index a5ac61e1f3..780e25366e 100644 --- a/zenoh/src/api/builders/mod.rs +++ b/zenoh/src/api/builders/mod.rs @@ -15,6 +15,7 @@ pub(crate) mod info; pub(crate) mod matching_listener; pub(crate) mod publisher; +#[cfg(feature = "unstable")] pub(crate) mod querier; pub(crate) mod query; pub(crate) mod queryable; diff --git a/zenoh/src/api/builders/querier.rs b/zenoh/src/api/builders/querier.rs index a3134d3d60..ef1fd010da 100644 --- a/zenoh/src/api/builders/querier.rs +++ b/zenoh/src/api/builders/querier.rs @@ -11,7 +11,6 @@ // Contributors: // ZettaScale Zenoh Team, // - use std::{ future::{IntoFuture, Ready}, sync::Arc, @@ -71,6 +70,7 @@ use crate::{ /// } /// # } /// ``` +#[zenoh_macros::unstable] #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] #[derive(Debug)] pub struct QuerierBuilder<'a, 'b> { @@ -215,6 +215,7 @@ impl IntoFuture for QuerierBuilder<'_, '_> { /// } /// # } /// ``` +#[zenoh_macros::unstable] #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] #[derive(Debug)] pub struct QuerierGetBuilder<'a, 'b, Handler> { @@ -280,6 +281,7 @@ impl<'a, 'b> QuerierGetBuilder<'a, 'b, DefaultHandler> { /// .unwrap(); /// # } /// ``` + #[zenoh_macros::unstable] #[inline] pub fn callback(self, callback: F) -> QuerierGetBuilder<'a, 'b, Callback> where @@ -313,6 +315,7 @@ impl<'a, 'b> QuerierGetBuilder<'a, 'b, DefaultHandler> { /// .unwrap(); /// # } /// ``` + #[zenoh_macros::unstable] #[inline] pub fn callback_mut(self, callback: F) -> QuerierGetBuilder<'a, 'b, Callback> where @@ -345,6 +348,7 @@ impl<'a, 'b> QuerierGetBuilder<'a, 'b, DefaultHandler> { /// } /// # } /// ``` + #[zenoh_macros::unstable] #[inline] pub fn with(self, handler: Handler) -> QuerierGetBuilder<'a, 'b, Handler> where @@ -373,6 +377,7 @@ impl<'a, 'b> QuerierGetBuilder<'a, 'b, DefaultHandler> { impl<'b, Handler> QuerierGetBuilder<'_, 'b, Handler> { /// Set the query payload. #[inline] + #[zenoh_macros::unstable] pub fn payload(mut self, payload: IntoZBytes) -> Self where IntoZBytes: Into, @@ -385,6 +390,7 @@ impl<'b, Handler> QuerierGetBuilder<'_, 'b, Handler> { /// Set the query selector parameters. #[inline] + #[zenoh_macros::unstable] pub fn parameters

(mut self, parameters: P) -> Self where P: Into>, diff --git a/zenoh/src/api/matching.rs b/zenoh/src/api/matching.rs index 46aa4a9b71..c1dd7556ec 100644 --- a/zenoh/src/api/matching.rs +++ b/zenoh/src/api/matching.rs @@ -130,9 +130,9 @@ pub(crate) struct MatchingListenerInner { } /// A listener that sends notifications when the [`MatchingStatus`] of a -/// publisher changes. +/// corresponding Zenoh entity changes. /// -/// Callback matching listeners will run in background until the publisher is undeclared, +/// Callback matching listeners will run in background until the corresponding Zenoh entity is undeclared, /// or until it is undeclared. /// On the other hand, matching listener with a handler are automatically undeclared when dropped. /// diff --git a/zenoh/src/api/mod.rs b/zenoh/src/api/mod.rs index 41278e4ac3..6981e02f73 100644 --- a/zenoh/src/api/mod.rs +++ b/zenoh/src/api/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod matching; #[cfg(feature = "plugins")] pub(crate) mod plugins; pub(crate) mod publisher; +#[cfg(feature = "unstable")] pub(crate) mod querier; pub(crate) mod query; pub(crate) mod queryable; diff --git a/zenoh/src/api/querier.rs b/zenoh/src/api/querier.rs index d84c2f189a..312904617c 100644 --- a/zenoh/src/api/querier.rs +++ b/zenoh/src/api/querier.rs @@ -68,6 +68,7 @@ pub(crate) struct QuerierState { /// let replies = querier.get().await.unwrap(); /// # } /// ``` +#[zenoh_macros::unstable] #[derive(Debug)] pub struct Querier<'a> { pub(crate) session: WeakSession, @@ -119,25 +120,28 @@ impl<'a> Querier<'a> { } #[inline] + #[zenoh_macros::unstable] pub fn key_expr(&self) -> &KeyExpr<'a> { &self.key_expr } /// Get the `congestion_control` applied when routing the data. #[inline] + #[zenoh_macros::unstable] pub fn congestion_control(&self) -> CongestionControl { self.qos.congestion_control() } /// Get the priority of the written data. #[inline] + #[zenoh_macros::unstable] pub fn priority(&self) -> Priority { self.qos.priority() } /// Get type of queryables that can reply to this querier - #[zenoh_macros::unstable] #[inline] + #[zenoh_macros::unstable] pub fn accept_replies(&self) -> ReplyKeyExpr { self.accept_replies } @@ -155,6 +159,7 @@ impl<'a> Querier<'a> { /// # } /// ``` #[inline] + #[zenoh_macros::unstable] pub fn get(&self) -> QuerierGetBuilder<'_, '_, DefaultHandler> { QuerierGetBuilder { querier: self, @@ -179,6 +184,7 @@ impl<'a> Querier<'a> { /// querier.undeclare().await.unwrap(); /// # } /// ``` + #[zenoh_macros::unstable] pub fn undeclare(self) -> impl Resolve> + 'a { UndeclarableSealed::undeclare_inner(self, ()) } diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index 2ae2add3a8..b679a13dbb 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -71,8 +71,10 @@ use zenoh_task::TaskController; use crate::api::selector::ZenohParameters; #[cfg(feature = "unstable")] use crate::api::{ + builders::querier::QuerierBuilder, liveliness::Liveliness, matching::{MatchingListenerState, MatchingStatus, MatchingStatusType}, + querier::QuerierState, query::{LivelinessQueryState, ReplyKeyExpr}, sample::SourceInfo, }; @@ -84,7 +86,6 @@ use crate::{ PublicationBuilderDelete, PublicationBuilderPut, PublisherBuilder, SessionDeleteBuilder, SessionPutBuilder, }, - querier::QuerierBuilder, query::SessionGetBuilder, queryable::QueryableBuilder, session::OpenBuilder, @@ -96,7 +97,6 @@ use crate::{ info::SessionInfo, key_expr::{KeyExpr, KeyExprInner}, publisher::{Priority, PublisherState}, - querier::QuerierState, query::{ConsolidationMode, QueryConsolidation, QueryState, QueryTarget, Reply}, queryable::{Query, QueryInner, QueryableState}, sample::{DataInfo, DataInfoIntoSample, Locality, QoS, Sample, SampleKind}, @@ -131,6 +131,7 @@ pub(crate) struct SessionState { #[cfg(feature = "unstable")] pub(crate) remote_subscribers: HashMap>, pub(crate) publishers: HashMap, + #[cfg(feature = "unstable")] pub(crate) queriers: HashMap, #[cfg(feature = "unstable")] pub(crate) remote_tokens: HashMap>, @@ -165,6 +166,7 @@ impl SessionState { #[cfg(feature = "unstable")] remote_subscribers: HashMap::new(), publishers: HashMap::new(), + #[cfg(feature = "unstable")] queriers: HashMap::new(), #[cfg(feature = "unstable")] remote_tokens: HashMap::new(), @@ -304,6 +306,7 @@ impl SessionState { } } + #[cfg(feature = "unstable")] fn register_querier<'a>( &mut self, id: EntityId, @@ -972,6 +975,7 @@ impl Session { /// let replies = querier.get().await.unwrap(); /// # } /// ``` + #[zenoh_macros::unstable] pub fn declare_querier<'b, TryIntoKeyExpr>( &self, key_expr: TryIntoKeyExpr, @@ -1426,6 +1430,7 @@ impl SessionInner { } } + #[cfg(feature = "unstable")] pub(crate) fn declare_querier_inner( &self, key_expr: KeyExpr, @@ -1451,6 +1456,7 @@ impl SessionInner { Ok(id) } + #[cfg(feature = "unstable")] pub(crate) fn undeclare_querier_inner(&self, pid: Id) -> ZResult<()> { let mut state = zwrite!(self.state); let Ok(primitives) = state.primitives() else { diff --git a/zenoh/src/lib.rs b/zenoh/src/lib.rs index 93354f88d4..9ae97d9e7b 100644 --- a/zenoh/src/lib.rs +++ b/zenoh/src/lib.rs @@ -195,7 +195,6 @@ pub mod session { builders::{ info::{PeersZenohIdBuilder, RoutersZenohIdBuilder, ZenohIdBuilder}, publisher::{SessionDeleteBuilder, SessionPutBuilder}, - querier::QuerierBuilder, query::SessionGetBuilder, session::OpenBuilder, }, @@ -275,19 +274,22 @@ pub mod query { #[zenoh_macros::internal] pub use crate::api::queryable::ReplySample; + #[zenoh_macros::unstable] + pub use crate::api::{ + builders::querier::{QuerierBuilder, QuerierGetBuilder}, + querier::Querier, + query::ReplyKeyExpr, + selector::ZenohParameters, + }; pub use crate::api::{ builders::{ - querier::QuerierGetBuilder, queryable::QueryableBuilder, reply::{ReplyBuilder, ReplyBuilderDelete, ReplyBuilderPut, ReplyErrBuilder}, }, - querier::Querier, query::{ConsolidationMode, QueryConsolidation, QueryTarget, Reply, ReplyError}, queryable::{Query, Queryable, QueryableUndeclaration}, selector::Selector, }; - #[zenoh_macros::unstable] - pub use crate::api::{query::ReplyKeyExpr, selector::ZenohParameters}; } #[zenoh_macros::unstable] diff --git a/zenoh/tests/session.rs b/zenoh/tests/session.rs index 0aff08b7f7..a0eb6be130 100644 --- a/zenoh/tests/session.rs +++ b/zenoh/tests/session.rs @@ -26,9 +26,9 @@ use std::{ use zenoh::internal::runtime::{Runtime, RuntimeBuilder}; #[cfg(feature = "unstable")] use zenoh::qos::Reliability; -use zenoh::{ - key_expr::KeyExpr, qos::CongestionControl, query::Querier, sample::SampleKind, Session, -}; +#[cfg(feature = "unstable")] +use zenoh::query::Querier; +use zenoh::{key_expr::KeyExpr, qos::CongestionControl, sample::SampleKind, Session}; use zenoh_core::ztimeout; #[cfg(not(feature = "unstable"))] use zenoh_protocol::core::Reliability; @@ -177,10 +177,12 @@ impl HasGet for SessionGetter<'_, '_> { } } +#[cfg(feature = "unstable")] struct QuerierGetter<'a> { querier: Querier<'a>, } +#[cfg(feature = "unstable")] impl HasGet for QuerierGetter<'_> { async fn get(&self, params: &str) -> zenoh::handlers::FifoChannelHandler { ztimeout!(self.querier.get().parameters(params)).unwrap() @@ -312,6 +314,7 @@ async fn test_session_getrep(peer01: &Session, peer02: &Session, reliability: Re )) } +#[cfg(feature = "unstable")] async fn test_session_qrrep(peer01: &Session, peer02: &Session, reliability: Reliability) { let key_expr = "test/session"; println!("[QQ][00c] Declaring Querier on peer02 session"); @@ -333,6 +336,7 @@ async fn zenoh_session_unicast() { let (peer01, peer02) = open_session_unicast(&["tcp/127.0.0.1:17447"]).await; test_session_pubsub(&peer01, &peer02, Reliability::Reliable).await; test_session_getrep(&peer01, &peer02, Reliability::Reliable).await; + #[cfg(feature = "unstable")] test_session_qrrep(&peer01, &peer02, Reliability::Reliable).await; close_session(peer01, peer02).await; }