diff --git a/Cargo.lock b/Cargo.lock index 0129941564..8363b46684 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,6 +3065,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "regex" version = "1.10.6" @@ -5071,6 +5091,7 @@ dependencies = [ "petgraph", "phf", "rand 0.8.5", + "ref-cast", "rustc_version 0.4.1", "serde", "serde_json", @@ -5200,6 +5221,7 @@ dependencies = [ name = "zenoh-ext" version = "1.0.0-dev" dependencies = [ + "async-trait", "bincode", "flume", "futures", @@ -5208,6 +5230,7 @@ dependencies = [ "serde", "tokio", "tracing", + "uhlc", "zenoh", "zenoh-config", "zenoh-macros", diff --git a/Cargo.toml b/Cargo.toml index de23d2eea4..b69d42f66e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ quote = "1.0.37" rand = { version = "0.8.5", default-features = false } # Default features are disabled due to usage in no_std crates rand_chacha = "0.3.1" rcgen = "0.13.1" +ref-cast = "1.0.23" regex = "1.10.6" ron = "0.8.1" ringbuffer-spsc = "0.1.9" diff --git a/commons/zenoh-config/src/wrappers.rs b/commons/zenoh-config/src/wrappers.rs index 31afe358e7..759507c770 100644 --- a/commons/zenoh-config/src/wrappers.rs +++ b/commons/zenoh-config/src/wrappers.rs @@ -152,6 +152,16 @@ pub struct EntityGlobalId(EntityGlobalIdProto); pub type EntityId = u32; impl EntityGlobalId { + /// Creates a new EntityGlobalId. + #[zenoh_macros::internal] + pub fn new(zid: ZenohId, eid: EntityId) -> Self { + EntityGlobalIdProto { + zid: zid.into(), + eid, + } + .into() + } + /// Returns the [`ZenohId`], i.e. the Zenoh session, this ID is associated to. pub fn zid(&self) -> ZenohId { self.0.zid.into() diff --git a/commons/zenoh-macros/src/lib.rs b/commons/zenoh-macros/src/lib.rs index 2047c424cf..cc6f4a2295 100644 --- a/commons/zenoh-macros/src/lib.rs +++ b/commons/zenoh-macros/src/lib.rs @@ -515,7 +515,7 @@ pub fn ke(tokens: TokenStream) -> TokenStream { let value: LitStr = syn::parse(tokens).unwrap(); let ke = value.value(); match zenoh_keyexpr::keyexpr::new(&ke) { - Ok(_) => quote!(unsafe {::zenoh::key_expr::keyexpr::from_str_unchecked(#ke)}).into(), + Ok(_) => quote!(unsafe { zenoh::key_expr::keyexpr::from_str_unchecked(#ke)}).into(), Err(e) => panic!("{}", e), } } diff --git a/zenoh-ext/Cargo.toml b/zenoh-ext/Cargo.toml index 4de255d0d6..4fdf98b8df 100644 --- a/zenoh-ext/Cargo.toml +++ b/zenoh-ext/Cargo.toml @@ -38,6 +38,7 @@ tokio = { workspace = true, features = [ "macros", "io-std", ] } +async-trait = { workspace = true } bincode = { workspace = true } zenoh-util = { workspace = true } flume = { workspace = true } @@ -45,7 +46,8 @@ futures = { workspace = true } tracing = { workspace = true } serde = { workspace = true, features = ["default"] } leb128 = { workspace = true } -zenoh = { workspace = true, default-features = false } +uhlc = { workspace = true } +zenoh = { workspace = true, features = ["default"] } zenoh-macros = { workspace = true } [dev-dependencies] diff --git a/zenoh-ext/examples/Cargo.toml b/zenoh-ext/examples/Cargo.toml index 658eba1065..688c044d67 100644 --- a/zenoh-ext/examples/Cargo.toml +++ b/zenoh-ext/examples/Cargo.toml @@ -42,12 +42,12 @@ zenoh-ext = { workspace = true, features = ["unstable"] } zenoh-config = { workspace = true } [[example]] -name = "z_query_sub" -path = "examples/z_query_sub.rs" +name = "z_advanced_pub" +path = "examples/z_advanced_pub.rs" [[example]] -name = "z_pub_cache" -path = "examples/z_pub_cache.rs" +name = "z_advanced_sub" +path = "examples/z_advanced_sub.rs" [[example]] name = "z_member" diff --git a/zenoh-ext/examples/examples/README.md b/zenoh-ext/examples/examples/README.md index 498a1ca6fe..b72e50f816 100644 --- a/zenoh-ext/examples/examples/README.md +++ b/zenoh-ext/examples/examples/README.md @@ -15,31 +15,33 @@ ## Examples description -### z_pub_cache +### z_advanced_pub - Declares a publisher and an associated publication cache with a given key expression. - All the publications are locally cached (with a configurable history size - i.e. max number of cached data per resource). The cache can be queried by a QueryingSubscriber at startup (see next example). + Declares an AdvancedPublisher with a given key expression. + All the publications are locally cached (with a configurable history size - i.e. max number of cached data per resource, default 1). The cache can be queried by an AdvancedSubscriber for hsitory + or retransmission. Typical usage: ```bash - z_pub_cache + z_advanced_pub ``` or ```bash - z_pub_cache --history 10 + z_advanced_pub --history 10 ``` -### z_query_sub +### z_advanced_sub - Declares a querying subscriber with a selector. - At startup, the subscriber issuez a query (by default on the same selector than the subscription) and merge/sort/de-duplicate the query results with the publications received in parallel. + Declares an AdvancedSubscriber with a given key expression. + The AdvancedSubscriber can query for AdvancedPublisher history at startup + and on late joiner publisher detection. The AdvancedSubscriber can detect + sample loss and ask for retransmission. Typical usage: ```bash - z_query_sub + z_advanced_sub ``` - ### z_member Group Management example: join a group and display the received group events (Join, Leave, LeaseExpired), as well as an updated group view. diff --git a/zenoh-ext/examples/examples/z_pub_cache.rs b/zenoh-ext/examples/examples/z_advanced_pub.rs similarity index 61% rename from zenoh-ext/examples/examples/z_pub_cache.rs rename to zenoh-ext/examples/examples/z_advanced_pub.rs index d8e13faec4..6437515c8f 100644 --- a/zenoh-ext/examples/examples/z_pub_cache.rs +++ b/zenoh-ext/examples/examples/z_advanced_pub.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + // // Copyright (c) 2023 ZettaScale Technology // @@ -11,12 +13,10 @@ // Contributors: // ZettaScale Zenoh Team, // -use std::time::Duration; - use clap::{arg, Parser}; use zenoh::{config::Config, key_expr::KeyExpr}; use zenoh_config::ModeDependentValue; -use zenoh_ext::*; +use zenoh_ext::{AdvancedPublisherBuilderExt, CacheConfig}; use zenoh_ext_examples::CommonArgs; #[tokio::main] @@ -24,27 +24,26 @@ async fn main() { // Initiate logging zenoh::init_log_from_env_or("error"); - let (config, key_expr, value, history, prefix, complete) = parse_args(); + let (config, key_expr, value, history) = parse_args(); println!("Opening session..."); let session = zenoh::open(config).await.unwrap(); - println!("Declaring PublicationCache on {}", &key_expr); - let mut publication_cache_builder = session - .declare_publication_cache(&key_expr) - .history(history) - .queryable_complete(complete); - if let Some(prefix) = prefix { - publication_cache_builder = publication_cache_builder.queryable_prefix(prefix); - } - let _publication_cache = publication_cache_builder.await.unwrap(); + println!("Declaring AdvancedPublisher on {}", &key_expr); + let publisher = session + .declare_publisher(&key_expr) + .cache(CacheConfig::default().max_samples(history)) + .sample_miss_detection() + .publisher_detection() + .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}] {value}"); println!("Put Data ('{}': '{}')", &key_expr, buf); - session.put(&key_expr, buf).await.unwrap(); + publisher.put(buf).await.unwrap(); } } @@ -59,36 +58,16 @@ struct Args { #[arg(short = 'i', long, default_value = "1")] /// The number of publications to keep in cache. history: usize, - #[arg(short = 'o', long)] - /// Set `complete` option to true. This means that this queryable is ultimate data source, no need to scan other queryables. - complete: bool, - #[arg(short = 'x', long)] - /// An optional queryable prefix. - prefix: Option, #[command(flatten)] common: CommonArgs, } -fn parse_args() -> ( - Config, - KeyExpr<'static>, - String, - usize, - Option, - bool, -) { +fn parse_args() -> (Config, KeyExpr<'static>, String, usize) { let args = Args::parse(); let mut config: Config = args.common.into(); config .timestamping .set_enabled(Some(ModeDependentValue::Unique(true))) .unwrap(); - ( - config, - args.key, - args.value, - args.history, - args.prefix, - args.complete, - ) + (config, args.key, args.value, args.history) } diff --git a/zenoh-ext/examples/examples/z_advanced_sub.rs b/zenoh-ext/examples/examples/z_advanced_sub.rs new file mode 100644 index 0000000000..5bea70f7d5 --- /dev/null +++ b/zenoh-ext/examples/examples/z_advanced_sub.rs @@ -0,0 +1,84 @@ +// +// 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::{arg, Parser}; +use zenoh::config::Config; +use zenoh_ext::{AdvancedSubscriberBuilderExt, HistoryConfig, RecoveryConfig}; +use zenoh_ext_examples::CommonArgs; + +#[tokio::main] +async fn main() { + // Initiate logging + zenoh::init_log_from_env_or("error"); + + let (config, key_expr) = parse_args(); + + println!("Opening session..."); + let session = zenoh::open(config).await.unwrap(); + + println!("Declaring AdvancedSubscriber on {}", key_expr); + let subscriber = session + .declare_subscriber(key_expr) + .history(HistoryConfig::default().detect_late_publishers()) + .recovery(RecoveryConfig::default().periodic_queries(Some(Duration::from_secs(1)))) + .subscriber_detection() + .await + .unwrap(); + + let miss_listener = subscriber.sample_miss_listener().await.unwrap(); + + println!("Press CTRL-C to quit..."); + loop { + tokio::select! { + sample = subscriber.recv_async() => { + if let Ok(sample) = sample { + let payload = sample + .payload() + .try_to_string() + .unwrap_or_else(|e| e.to_string().into()); + println!( + ">> [Subscriber] Received {} ('{}': '{}')", + sample.kind(), + sample.key_expr().as_str(), + payload + ); + } + }, + miss = miss_listener.recv_async() => { + if let Ok(miss) = miss { + println!( + ">> [Subscriber] Missed {} samples from {:?} !!!", + miss.nb(), + miss.source() + ); + } + }, + } + } +} + +#[derive(clap::Parser, Clone, PartialEq, Eq, Hash, Debug)] +struct Args { + #[arg(short, long, default_value = "demo/example/**")] + /// The key expression to subscribe onto. + key: String, + #[command(flatten)] + common: CommonArgs, +} + +fn parse_args() -> (Config, String) { + let args = Args::parse(); + (args.common.into(), args.key) +} diff --git a/zenoh-ext/examples/examples/z_query_sub.rs b/zenoh-ext/examples/examples/z_query_sub.rs deleted file mode 100644 index 2e8c832670..0000000000 --- a/zenoh-ext/examples/examples/z_query_sub.rs +++ /dev/null @@ -1,80 +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 clap::{arg, Parser}; -use zenoh::{config::Config, query::ReplyKeyExpr}; -use zenoh_ext::*; -use zenoh_ext_examples::CommonArgs; - -#[tokio::main] -async fn main() { - // Initiate logging - zenoh::init_log_from_env_or("error"); - - let (config, key_expr, query) = parse_args(); - - println!("Opening session..."); - let session = zenoh::open(config).await.unwrap(); - - println!( - "Declaring QueryingSubscriber on {} with an initial query on {}", - key_expr, - query.as_ref().unwrap_or(&key_expr) - ); - let subscriber = if let Some(selector) = query { - session - .declare_subscriber(key_expr) - .querying() - .query_selector(&selector) - .query_accept_replies(ReplyKeyExpr::Any) - .await - .unwrap() - } else { - session - .declare_subscriber(key_expr) - .querying() - .await - .unwrap() - }; - - println!("Press CTRL-C to quit..."); - while let Ok(sample) = subscriber.recv_async().await { - let payload = sample - .payload() - .try_to_string() - .unwrap_or_else(|e| e.to_string().into()); - println!( - ">> [Subscriber] Received {} ('{}': '{}')", - sample.kind(), - sample.key_expr().as_str(), - payload - ); - } -} - -#[derive(clap::Parser, Clone, PartialEq, Eq, Hash, Debug)] -struct Args { - #[arg(short, long, default_value = "demo/example/**")] - /// The key expression to subscribe onto. - key: String, - #[arg(short, long)] - /// The selector to use for queries (by default it's same as 'key' option) - query: Option, - #[command(flatten)] - common: CommonArgs, -} - -fn parse_args() -> (Config, String, Option) { - let args = Args::parse(); - (args.common.into(), args.key, args.query) -} diff --git a/zenoh-ext/src/advanced_cache.rs b/zenoh-ext/src/advanced_cache.rs new file mode 100644 index 0000000000..c18add5a59 --- /dev/null +++ b/zenoh-ext/src/advanced_cache.rs @@ -0,0 +1,363 @@ +// +// Copyright (c) 2022 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::VecDeque, + future::{IntoFuture, Ready}, + ops::{Bound, RangeBounds}, + sync::{Arc, RwLock}, +}; + +use zenoh::{ + internal::{bail, traits::QoSBuilderTrait}, + key_expr::{ + format::{ke, kedefine}, + keyexpr, KeyExpr, + }, + liveliness::LivelinessToken, + qos::{CongestionControl, Priority}, + query::{Queryable, ZenohParameters}, + sample::{Locality, Sample, SampleBuilder}, + Resolvable, Result as ZResult, Session, Wait, KE_ADV_PREFIX, KE_AT, KE_STARSTAR, +}; + +pub(crate) static KE_UHLC: &keyexpr = ke!("uhlc"); +#[zenoh_macros::unstable] +kedefine!( + pub(crate) ke_liveliness: "@adv/${entity:*}/${zid:*}/${eid:*}/${meta:**}/@/${remaining:**}", +); + +#[zenoh_macros::unstable] +/// Configure replies. +#[derive(Clone, Debug)] +pub struct RepliesConfig { + priority: Priority, + congestion_control: CongestionControl, + is_express: bool, +} + +#[zenoh_macros::unstable] +impl Default for RepliesConfig { + fn default() -> Self { + Self { + priority: Priority::Data, + congestion_control: CongestionControl::Block, + is_express: false, + } + } +} + +#[zenoh_macros::internal_trait] +#[zenoh_macros::unstable] +impl QoSBuilderTrait for RepliesConfig { + #[allow(unused_mut)] + #[zenoh_macros::unstable] + fn congestion_control(mut self, congestion_control: CongestionControl) -> Self { + self.congestion_control = congestion_control; + self + } + + #[allow(unused_mut)] + #[zenoh_macros::unstable] + fn priority(mut self, priority: Priority) -> Self { + self.priority = priority; + self + } + + #[allow(unused_mut)] + #[zenoh_macros::unstable] + fn express(mut self, is_express: bool) -> Self { + self.is_express = is_express; + self + } +} + +#[derive(Debug, Clone)] +/// Configure an [`AdvancedPublisher`](crate::AdvancedPublisher) cache. +#[zenoh_macros::unstable] +pub struct CacheConfig { + max_samples: usize, + replies_config: RepliesConfig, +} + +#[zenoh_macros::unstable] +impl Default for CacheConfig { + fn default() -> Self { + Self { + max_samples: 1, + replies_config: RepliesConfig::default(), + } + } +} + +#[zenoh_macros::unstable] +impl CacheConfig { + /// Specify how many samples to keep for each resource. + #[zenoh_macros::unstable] + pub fn max_samples(mut self, depth: usize) -> Self { + self.max_samples = depth; + self + } + + /// The QoS to apply to replies. + #[zenoh_macros::unstable] + pub fn replies_config(mut self, qos: RepliesConfig) -> Self { + self.replies_config = qos; + self + } +} + +/// The builder of an [`AdvancedCache`], allowing to configure it. +#[zenoh_macros::unstable] +pub struct AdvancedCacheBuilder<'a, 'b, 'c> { + session: &'a Session, + pub_key_expr: ZResult>, + queryable_prefix: Option>>, + queryable_origin: Locality, + history: CacheConfig, + liveliness: bool, +} + +#[zenoh_macros::unstable] +impl<'a, 'b, 'c> AdvancedCacheBuilder<'a, 'b, 'c> { + #[zenoh_macros::unstable] + pub(crate) fn new( + session: &'a Session, + pub_key_expr: ZResult>, + ) -> AdvancedCacheBuilder<'a, 'b, 'c> { + AdvancedCacheBuilder { + session, + pub_key_expr, + queryable_prefix: Some(Ok((KE_ADV_PREFIX / KE_STARSTAR / KE_AT).into())), + queryable_origin: Locality::default(), + history: CacheConfig::default(), + liveliness: false, + } + } + + /// Change the prefix used for queryable. + #[zenoh_macros::unstable] + pub fn queryable_prefix(mut self, queryable_prefix: TryIntoKeyExpr) -> Self + where + TryIntoKeyExpr: TryInto>, + >>::Error: Into, + { + self.queryable_prefix = Some(queryable_prefix.try_into().map_err(Into::into)); + self + } + + /// Change the history size for each resource. + #[zenoh_macros::unstable] + pub fn history(mut self, history: CacheConfig) -> Self { + self.history = history; + self + } +} + +#[zenoh_macros::unstable] +impl Resolvable for AdvancedCacheBuilder<'_, '_, '_> { + type To = ZResult; +} + +#[zenoh_macros::unstable] +impl Wait for AdvancedCacheBuilder<'_, '_, '_> { + fn wait(self) -> ::To { + AdvancedCache::new(self) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for AdvancedCacheBuilder<'_, '_, '_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +#[zenoh_macros::unstable] +fn decode_range(range: &str) -> (Bound, Bound) { + let mut split = range.split(".."); + let start = split + .next() + .and_then(|s| s.parse::().ok().map(Bound::Included)) + .unwrap_or(Bound::Unbounded); + let end = split + .next() + .map(|s| { + s.parse::() + .ok() + .map(Bound::Included) + .unwrap_or(Bound::Unbounded) + }) + .unwrap_or(start); + (start, end) +} + +/// [`AdvancedCache`]. +#[zenoh_macros::unstable] +pub struct AdvancedCache { + cache: Arc>>, + max_samples: usize, + _queryable: Queryable<()>, + _token: Option, +} + +#[zenoh_macros::unstable] +impl AdvancedCache { + #[zenoh_macros::unstable] + fn new(conf: AdvancedCacheBuilder<'_, '_, '_>) -> ZResult { + let key_expr = conf.pub_key_expr?.into_owned(); + // the queryable_prefix (optional), and the key_expr for AdvancedCache's queryable ("[]/") + let queryable_key_expr = match conf.queryable_prefix { + None => key_expr.clone(), + Some(Ok(ke)) => (&ke) / &key_expr, + Some(Err(e)) => bail!("Invalid key expression for queryable_prefix: {}", e), + }; + tracing::debug!( + "Create AdvancedCache on {} with max_samples={:?}", + &key_expr, + conf.history, + ); + let cache = Arc::new(RwLock::new(VecDeque::::new())); + + // declare the queryable that will answer to queries on cache + let queryable = conf + .session + .declare_queryable(&queryable_key_expr) + .allowed_origin(conf.queryable_origin) + .callback({ + let cache = cache.clone(); + move |query| { + let range = query + .parameters() + .get("_sn") + .map(decode_range) + .unwrap_or((Bound::Unbounded, Bound::Unbounded)); + let max = query + .parameters() + .get("_max") + .and_then(|s| s.parse::().ok()); + if let Ok(queue) = cache.read() { + if let Some(max) = max { + let mut samples = VecDeque::new(); + for sample in queue.iter() { + if range == (Bound::Unbounded, Bound::Unbounded) + || sample + .source_info() + .source_sn() + .is_some_and(|sn| range.contains(&sn)) + { + if let (Some(Ok(time_range)), Some(timestamp)) = + (query.parameters().time_range(), sample.timestamp()) + { + if !time_range + .contains(timestamp.get_time().to_system_time()) + { + continue; + } + } + samples.push_front(sample); + samples.truncate(max as usize); + } + } + for sample in samples.drain(..).rev() { + if let Err(e) = query + .reply_sample( + SampleBuilder::from(sample.clone()) + .congestion_control( + conf.history.replies_config.congestion_control, + ) + .priority(conf.history.replies_config.priority) + .express(conf.history.replies_config.is_express) + .into(), + ) + .wait() + { + tracing::warn!("Error replying to query: {}", e); + } + } + } else { + for sample in queue.iter() { + if range == (Bound::Unbounded, Bound::Unbounded) + || sample + .source_info() + .source_sn() + .is_some_and(|sn| range.contains(&sn)) + { + if let (Some(Ok(time_range)), Some(timestamp)) = + (query.parameters().time_range(), sample.timestamp()) + { + if !time_range + .contains(timestamp.get_time().to_system_time()) + { + continue; + } + } + if let Err(e) = query + .reply_sample( + SampleBuilder::from(sample.clone()) + .congestion_control( + conf.history.replies_config.congestion_control, + ) + .priority(conf.history.replies_config.priority) + .express(conf.history.replies_config.is_express) + .into(), + ) + .wait() + { + tracing::warn!("Error replying to query: {}", e); + } + } + } + } + } else { + tracing::error!("Unable to take AdvancedPublisher cache read lock"); + } + } + }) + .wait()?; + + let token = if conf.liveliness { + Some( + conf.session + .liveliness() + .declare_token(queryable_key_expr) + .wait()?, + ) + } else { + None + }; + + Ok(AdvancedCache { + cache, + max_samples: conf.history.max_samples, + _queryable: queryable, + _token: token, + }) + } + + #[zenoh_macros::unstable] + pub(crate) fn cache_sample(&self, sample: Sample) { + if let Ok(mut queue) = self.cache.write() { + if queue.len() >= self.max_samples { + queue.pop_front(); + } + queue.push_back(sample); + } else { + tracing::error!("Unable to take AdvancedPublisher cache write lock"); + } + } +} diff --git a/zenoh-ext/src/advanced_publisher.rs b/zenoh-ext/src/advanced_publisher.rs new file mode 100644 index 0000000000..023e509d57 --- /dev/null +++ b/zenoh-ext/src/advanced_publisher.rs @@ -0,0 +1,598 @@ +// +// 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::{ + future::{IntoFuture, Ready}, + sync::atomic::{AtomicU32, Ordering}, +}; + +use zenoh::{ + bytes::{Encoding, OptionZBytes, ZBytes}, + internal::{ + bail, + traits::{ + EncodingBuilderTrait, QoSBuilderTrait, SampleBuilderTrait, TimestampBuilderTrait, + }, + }, + key_expr::{keyexpr, KeyExpr}, + liveliness::LivelinessToken, + pubsub::{ + PublicationBuilder, PublicationBuilderDelete, PublicationBuilderPut, Publisher, + PublisherBuilder, + }, + qos::{CongestionControl, Priority, Reliability}, + sample::{Locality, SourceInfo}, + session::EntityGlobalId, + Resolvable, Resolve, Result as ZResult, Session, Wait, KE_ADV_PREFIX, KE_AT, KE_EMPTY, +}; +use zenoh_macros::ke; + +use crate::advanced_cache::{AdvancedCache, AdvancedCacheBuilder, CacheConfig, KE_UHLC}; + +pub(crate) static KE_PUB: &keyexpr = ke!("pub"); + +#[derive(PartialEq)] +#[zenoh_macros::unstable] +pub(crate) enum Sequencing { + None, + Timestamp, + SequenceNumber, +} + +/// The builder of PublicationCache, allowing to configure it. +#[must_use = "Resolvables do nothing unless you resolve them using the `res` method from either `SyncResolve` or `AsyncResolve`"] +#[zenoh_macros::unstable] +pub struct AdvancedPublisherBuilder<'a, 'b, 'c> { + session: &'a Session, + pub_key_expr: ZResult>, + encoding: Encoding, + destination: Locality, + reliability: Reliability, + congestion_control: CongestionControl, + priority: Priority, + is_express: bool, + meta_key_expr: Option>>, + sequencing: Sequencing, + liveliness: bool, + cache: bool, + history: CacheConfig, +} + +#[zenoh_macros::unstable] +impl<'a, 'b, 'c> AdvancedPublisherBuilder<'a, 'b, 'c> { + #[zenoh_macros::unstable] + pub(crate) fn new(builder: PublisherBuilder<'a, 'b>) -> AdvancedPublisherBuilder<'a, 'b, 'c> { + AdvancedPublisherBuilder { + session: builder.session, + pub_key_expr: builder.key_expr, + encoding: builder.encoding, + destination: builder.destination, + reliability: builder.reliability, + congestion_control: builder.congestion_control, + priority: builder.priority, + is_express: builder.is_express, + meta_key_expr: None, + sequencing: Sequencing::None, + liveliness: false, + cache: false, + history: CacheConfig::default(), + } + } + + /// Changes the [`zenoh::sample::Locality`] applied when routing the data. + /// + /// This restricts the matching subscribers that will receive the published data to the ones + /// that have the given [`zenoh::sample::Locality`]. + #[zenoh_macros::unstable] + #[inline] + pub fn allowed_destination(mut self, destination: Locality) -> Self { + self.destination = destination; + self + } + + /// Changes the [`zenoh::qos::Reliability`] to apply when routing the data. + /// + /// **NOTE**: Currently `reliability` does not trigger any data retransmission on the wire. It + /// is rather used as a marker on the wire and it may be used to select the best link + /// available (e.g. TCP for reliable data and UDP for best effort data). + #[zenoh_macros::unstable] + #[inline] + pub fn reliability(self, reliability: Reliability) -> Self { + Self { + reliability, + ..self + } + } + + /// Allow matching [`AdvancedSubscribers`](crate::AdvancedSubscriber) to detect lost samples and optionally ask for retransimission. + /// + /// Retransmission can only be achieved if [`cache`](crate::AdvancedPublisherBuilder::cache) is enabled. + #[zenoh_macros::unstable] + pub fn sample_miss_detection(mut self) -> Self { + self.sequencing = Sequencing::SequenceNumber; + self + } + + /// Attach a cache to this [`AdvancedPublisher`]. + /// + /// The cache can be used for history and/or recovery. + #[zenoh_macros::unstable] + pub fn cache(mut self, config: CacheConfig) -> Self { + self.cache = true; + if self.sequencing == Sequencing::None { + self.sequencing = Sequencing::Timestamp; + } + self.history = config; + self + } + + /// Allow this [`AdvancedPublisher`] to be detected by [`AdvancedSubscribers`](crate::AdvancedSubscriber). + /// + /// This allows [`AdvancedSubscribers`](crate::AdvancedSubscriber) to retrieve the local history. + #[zenoh_macros::unstable] + pub fn publisher_detection(mut self) -> Self { + self.liveliness = true; + self + } + + /// A key expression added to the liveliness token key expression + /// and to the cache queryable key expression. + /// It can be used to convey meta data. + #[zenoh_macros::unstable] + pub fn publisher_detection_metadata(mut self, meta: TryIntoKeyExpr) -> Self + where + TryIntoKeyExpr: TryInto>, + >>::Error: Into, + { + self.meta_key_expr = Some(meta.try_into().map_err(Into::into)); + self + } +} + +#[zenoh_macros::internal_trait] +#[zenoh_macros::unstable] +impl EncodingBuilderTrait for AdvancedPublisherBuilder<'_, '_, '_> { + #[zenoh_macros::unstable] + fn encoding>(self, encoding: T) -> Self { + Self { + encoding: encoding.into(), + ..self + } + } +} + +#[zenoh_macros::internal_trait] +#[zenoh_macros::unstable] +impl QoSBuilderTrait for AdvancedPublisherBuilder<'_, '_, '_> { + /// Changes the [`zenoh::qos::CongestionControl`] to apply when routing the data. + #[inline] + #[zenoh_macros::unstable] + fn congestion_control(self, congestion_control: CongestionControl) -> Self { + Self { + congestion_control, + ..self + } + } + + /// Changes the [`zenoh::qos::Priority`] of the written data. + #[inline] + #[zenoh_macros::unstable] + fn priority(self, priority: Priority) -> Self { + Self { priority, ..self } + } + + /// Changes the Express policy to apply when routing the data. + /// + /// When express is set to `true`, then the message will not be batched. + /// This usually has a positive impact on latency but negative impact on throughput. + #[inline] + #[zenoh_macros::unstable] + fn express(self, is_express: bool) -> Self { + Self { is_express, ..self } + } +} + +#[zenoh_macros::unstable] +impl<'a> Resolvable for AdvancedPublisherBuilder<'a, '_, '_> { + type To = ZResult>; +} + +#[zenoh_macros::unstable] +impl Wait for AdvancedPublisherBuilder<'_, '_, '_> { + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + AdvancedPublisher::new(self) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for AdvancedPublisherBuilder<'_, '_, '_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +/// [`AdvancedPublisher`]. +#[zenoh_macros::unstable] +pub struct AdvancedPublisher<'a> { + publisher: Publisher<'a>, + seqnum: Option, + cache: Option, + _token: Option, +} + +#[zenoh_macros::unstable] +impl<'a> AdvancedPublisher<'a> { + #[zenoh_macros::unstable] + fn new(conf: AdvancedPublisherBuilder<'a, '_, '_>) -> ZResult { + let key_expr = conf.pub_key_expr?; + let meta = match conf.meta_key_expr { + Some(meta) => Some(meta?), + None => None, + }; + + let publisher = conf + .session + .declare_publisher(key_expr.clone().into_owned()) + .encoding(conf.encoding) + .allowed_destination(conf.destination) + .reliability(conf.reliability) + .congestion_control(conf.congestion_control) + .priority(conf.priority) + .express(conf.is_express) + .wait()?; + let id = publisher.id(); + let prefix = KE_ADV_PREFIX / KE_PUB / &id.zid().into_keyexpr(); + let prefix = match conf.sequencing { + Sequencing::SequenceNumber => { + prefix / &KeyExpr::try_from(id.eid().to_string()).unwrap() + } + _ => prefix / KE_UHLC, + }; + let prefix = match meta { + Some(meta) => prefix / &meta / KE_AT, + // We need this empty chunk because af a routing matching bug + _ => prefix / KE_EMPTY / KE_AT, + }; + + let seqnum = match conf.sequencing { + Sequencing::SequenceNumber => Some(AtomicU32::new(0)), + Sequencing::Timestamp => { + if conf.session.hlc().is_none() { + bail!( + "Cannot create AdvancedPublisher {} with Sequencing::Timestamp: \ + the 'timestamping' setting must be enabled in the Zenoh configuration.", + key_expr, + ) + } + None + } + _ => None, + }; + + let cache = if conf.cache { + Some( + AdvancedCacheBuilder::new(conf.session, Ok(key_expr.clone().into_owned())) + .history(conf.history) + .queryable_prefix(&prefix) + .wait()?, + ) + } else { + None + }; + + let token = if conf.liveliness { + Some( + conf.session + .liveliness() + .declare_token(prefix / &key_expr) + .wait()?, + ) + } else { + None + }; + + Ok(AdvancedPublisher { + publisher, + seqnum, + cache, + _token: token, + }) + } + + /// Returns the [`EntityGlobalId`] of this Publisher. + #[zenoh_macros::unstable] + pub fn id(&self) -> EntityGlobalId { + self.publisher.id() + } + + /// Returns the [`KeyExpr`] of this Publisher. + #[inline] + #[zenoh_macros::unstable] + pub fn key_expr(&self) -> &KeyExpr<'a> { + self.publisher.key_expr() + } + + /// Get the [`Encoding`] used when publishing data. + #[inline] + #[zenoh_macros::unstable] + pub fn encoding(&self) -> &Encoding { + self.publisher.encoding() + } + + /// Get the `congestion_control` applied when routing the data. + #[inline] + #[zenoh_macros::unstable] + pub fn congestion_control(&self) -> CongestionControl { + self.publisher.congestion_control() + } + + /// Get the priority of the written data. + #[inline] + #[zenoh_macros::unstable] + pub fn priority(&self) -> Priority { + self.publisher.priority() + } + + /// Put data. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh_ext::AdvancedPublisherBuilderExt; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").advanced().await.unwrap(); + /// publisher.put("value").await.unwrap(); + /// # } + /// ``` + #[inline] + #[zenoh_macros::unstable] + pub fn put(&self, payload: IntoZBytes) -> AdvancedPublisherPutBuilder<'_> + where + IntoZBytes: Into, + { + let mut builder = self.publisher.put(payload); + if let Some(seqnum) = &self.seqnum { + builder = builder.source_info(SourceInfo::new( + Some(self.publisher.id()), + Some(seqnum.fetch_add(1, Ordering::Relaxed)), + )); + } + if let Some(hlc) = self.publisher.session().hlc() { + builder = builder.timestamp(hlc.new_timestamp()); + } + AdvancedPublisherPutBuilder { + builder, + cache: self.cache.as_ref(), + } + } + + /// Delete data. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh_ext::AdvancedPublisherBuilderExt; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").advanced().await.unwrap(); + /// publisher.delete().await.unwrap(); + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn delete(&self) -> AdvancedPublisherDeleteBuilder<'_> { + let mut builder = self.publisher.delete(); + if let Some(seqnum) = &self.seqnum { + builder = builder.source_info(SourceInfo::new( + Some(self.publisher.id()), + Some(seqnum.fetch_add(1, Ordering::Relaxed)), + )); + } + if let Some(hlc) = self.publisher.session().hlc() { + builder = builder.timestamp(hlc.new_timestamp()); + } + AdvancedPublisherDeleteBuilder { + builder, + cache: self.cache.as_ref(), + } + } + + /// Return the [`MatchingStatus`](zenoh::matching::MatchingStatus) of the publisher. + /// + /// [`MatchingStatus::matching`](zenoh::matching::MatchingStatus::matching) + /// will return true if there exist Subscribers matching the Publisher's key expression and false otherwise. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh_ext::AdvancedPublisherBuilderExt; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").advanced().await.unwrap(); + /// let matching_subscribers: bool = publisher + /// .matching_status() + /// .await + /// .unwrap() + /// .matching(); + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn matching_status(&self) -> impl Resolve> + '_ { + self.publisher.matching_status() + } + + /// Return a [`MatchingListener`](zenoh::matching::MatchingStatus) for this Publisher. + /// + /// The [`MatchingListener`](zenoh::matching::MatchingStatus) that will send a notification each time + /// the [`MatchingStatus`](zenoh::matching::MatchingStatus) of the Publisher changes. + /// + /// # Examples + /// ```no_run + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh_ext::AdvancedPublisherBuilderExt; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").advanced().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 fn matching_listener( + &self, + ) -> zenoh::matching::MatchingListenerBuilder<'_, zenoh::handlers::DefaultHandler> { + self.publisher.matching_listener() + } + + /// Undeclares the [`Publisher`], informing the network that it needn't optimize publications for its key expression anymore. + /// + /// # Examples + /// ``` + /// # #[tokio::main] + /// # async fn main() { + /// use zenoh_ext::AdvancedPublisherBuilderExt; + /// + /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + /// let publisher = session.declare_publisher("key/expression").advanced().await.unwrap(); + /// publisher.undeclare().await.unwrap(); + /// # } + /// ``` + #[zenoh_macros::unstable] + pub fn undeclare(self) -> impl Resolve> + 'a { + self.publisher.undeclare() + } +} + +#[zenoh_macros::unstable] +pub type AdvancedPublisherPutBuilder<'a> = AdvancedPublicationBuilder<'a, PublicationBuilderPut>; +#[zenoh_macros::unstable] +pub type AdvancedPublisherDeleteBuilder<'a> = + AdvancedPublicationBuilder<'a, PublicationBuilderDelete>; + +#[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[derive(Clone)] +#[zenoh_macros::unstable] +pub struct AdvancedPublicationBuilder<'a, P> { + pub(crate) builder: PublicationBuilder<&'a Publisher<'a>, P>, + pub(crate) cache: Option<&'a AdvancedCache>, +} + +#[zenoh_macros::internal_trait] +#[zenoh_macros::unstable] +impl EncodingBuilderTrait for AdvancedPublicationBuilder<'_, PublicationBuilderPut> { + #[zenoh_macros::unstable] + fn encoding>(self, encoding: T) -> Self { + Self { + builder: self.builder.encoding(encoding), + ..self + } + } +} + +#[zenoh_macros::internal_trait] +#[zenoh_macros::unstable] +impl

SampleBuilderTrait for AdvancedPublicationBuilder<'_, P> { + #[zenoh_macros::unstable] + fn source_info(self, source_info: SourceInfo) -> Self { + Self { + builder: self.builder.source_info(source_info), + ..self + } + } + #[zenoh_macros::unstable] + fn attachment>(self, attachment: TA) -> Self { + let attachment: OptionZBytes = attachment.into(); + Self { + builder: self.builder.attachment(attachment), + ..self + } + } +} + +#[zenoh_macros::internal_trait] +#[zenoh_macros::unstable] +impl

TimestampBuilderTrait for AdvancedPublicationBuilder<'_, P> { + #[zenoh_macros::unstable] + fn timestamp>>(self, timestamp: TS) -> Self { + Self { + builder: self.builder.timestamp(timestamp), + ..self + } + } +} + +#[zenoh_macros::unstable] +impl

Resolvable for AdvancedPublicationBuilder<'_, P> { + type To = ZResult<()>; +} + +#[zenoh_macros::unstable] +impl Wait for AdvancedPublisherPutBuilder<'_> { + #[inline] + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + if let Some(cache) = self.cache { + cache.cache_sample(zenoh::sample::Sample::from(&self.builder)); + } + self.builder.wait() + } +} + +#[zenoh_macros::unstable] +impl Wait for AdvancedPublisherDeleteBuilder<'_> { + #[inline] + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + if let Some(cache) = self.cache { + cache.cache_sample(zenoh::sample::Sample::from(&self.builder)); + } + self.builder.wait() + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for AdvancedPublisherPutBuilder<'_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for AdvancedPublisherDeleteBuilder<'_> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} diff --git a/zenoh-ext/src/advanced_subscriber.rs b/zenoh-ext/src/advanced_subscriber.rs new file mode 100644 index 0000000000..e0d9ed7e3a --- /dev/null +++ b/zenoh-ext/src/advanced_subscriber.rs @@ -0,0 +1,1324 @@ +// +// Copyright (c) 2022 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::BTreeMap, future::IntoFuture, str::FromStr}; + +use zenoh::{ + config::ZenohId, + handlers::{Callback, IntoHandler}, + key_expr::KeyExpr, + liveliness::{LivelinessSubscriberBuilder, LivelinessToken}, + pubsub::SubscriberBuilder, + query::{ + ConsolidationMode, Parameters, Selector, TimeBound, TimeExpr, TimeRange, ZenohParameters, + }, + sample::{Locality, Sample, SampleKind}, + session::{EntityGlobalId, EntityId}, + Resolvable, Resolve, Session, Wait, KE_ADV_PREFIX, KE_AT, KE_EMPTY, KE_PUB, KE_STAR, + KE_STARSTAR, KE_SUB, +}; +use zenoh_util::{Timed, TimedEvent, Timer}; +#[zenoh_macros::unstable] +use { + async_trait::async_trait, + std::collections::hash_map::Entry, + std::collections::HashMap, + std::convert::TryFrom, + std::future::Ready, + std::sync::{Arc, Mutex}, + std::time::Duration, + uhlc::ID, + zenoh::handlers::{locked, DefaultHandler}, + zenoh::internal::{runtime::ZRuntime, zlock}, + zenoh::pubsub::Subscriber, + zenoh::query::{QueryTarget, Reply, ReplyKeyExpr}, + zenoh::time::Timestamp, + zenoh::Result as ZResult, +}; + +use crate::advanced_cache::{ke_liveliness, KE_UHLC}; + +#[derive(Debug, Default, Clone)] +/// Configure query for historical data. +#[zenoh_macros::unstable] +pub struct HistoryConfig { + liveliness: bool, + sample_depth: Option, + age: Option, +} + +#[zenoh_macros::unstable] +impl HistoryConfig { + /// Enable detection of late joiner publishers and query for their historical data. + /// + /// Late joiner detection can only be achieved for [`AdvancedPublishers`](crate::AdvancedPublisher) that enable publisher_detection. + /// History can only be retransmitted by [`AdvancedPublishers`](crate::AdvancedPublisher) that enable [`cache`](crate::AdvancedPublisherBuilder::cache). + #[inline] + #[zenoh_macros::unstable] + pub fn detect_late_publishers(mut self) -> Self { + self.liveliness = true; + self + } + + /// Specify how many samples to query for each resource. + #[zenoh_macros::unstable] + pub fn max_samples(mut self, depth: usize) -> Self { + self.sample_depth = Some(depth); + self + } + + /// Specify the maximum age of samples to query. + #[zenoh_macros::unstable] + pub fn max_age(mut self, seconds: f64) -> Self { + self.age = Some(seconds); + self + } +} + +#[derive(Default)] +/// Configure retransmission. +#[zenoh_macros::unstable] +pub struct RecoveryConfig { + periodic_queries: Option, +} + +impl std::fmt::Debug for RecoveryConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("RetransmissionConf"); + s.field("periodic_queries", &self.periodic_queries); + s.finish() + } +} + +#[zenoh_macros::unstable] +impl RecoveryConfig { + /// Enable periodic queries for not yet received Samples and specify their period. + /// + /// This allows to retrieve the last Sample(s) if the last Sample(s) is/are lost. + /// So it is useful for sporadic publications but useless for periodic publications + /// with a period smaller or equal to this period. + /// Retransmission can only be achieved by [`AdvancedPublishers`](crate::AdvancedPublisher) + /// that enable [`cache`](crate::AdvancedPublisherBuilder::cache) and + /// [`sample_miss_detection`](crate::AdvancedPublisherBuilder::sample_miss_detection). + #[zenoh_macros::unstable] + #[inline] + pub fn periodic_queries(mut self, period: Option) -> Self { + self.periodic_queries = period; + self + } +} + +/// The builder of an [`AdvancedSubscriber`], allowing to configure it. +#[zenoh_macros::unstable] +pub struct AdvancedSubscriberBuilder<'a, 'b, 'c, Handler, const BACKGROUND: bool = false> { + pub(crate) session: &'a Session, + pub(crate) key_expr: ZResult>, + pub(crate) origin: Locality, + pub(crate) retransmission: Option, + pub(crate) query_target: QueryTarget, + pub(crate) query_timeout: Duration, + pub(crate) history: Option, + pub(crate) liveliness: bool, + pub(crate) meta_key_expr: Option>>, + pub(crate) handler: Handler, +} + +#[zenoh_macros::unstable] +impl<'a, 'b, Handler> AdvancedSubscriberBuilder<'a, 'b, '_, Handler> { + #[zenoh_macros::unstable] + pub(crate) fn new(builder: SubscriberBuilder<'a, 'b, Handler>) -> Self { + AdvancedSubscriberBuilder { + session: builder.session, + key_expr: builder.key_expr, + origin: builder.origin, + handler: builder.handler, + retransmission: None, + query_target: QueryTarget::All, + query_timeout: Duration::from_secs(10), + history: None, + liveliness: false, + meta_key_expr: None, + } + } +} + +#[zenoh_macros::unstable] +impl<'a, 'b, 'c> AdvancedSubscriberBuilder<'a, 'b, 'c, DefaultHandler> { + /// Add callback to AdvancedSubscriber. + #[inline] + #[zenoh_macros::unstable] + pub fn callback(self, callback: F) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Callback> + where + F: Fn(Sample) + Send + Sync + 'static, + { + self.with(Callback::new(Arc::new(callback))) + } + + /// Add callback to `AdvancedSubscriber`. + /// + /// Using this guarantees that your callback will never be called concurrently. + /// If your callback is also accepted by the [`callback`](AdvancedSubscriberBuilder::callback) method, we suggest you use it instead of `callback_mut` + #[inline] + #[zenoh_macros::unstable] + pub fn callback_mut( + self, + callback: F, + ) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Callback> + where + F: FnMut(Sample) + Send + Sync + 'static, + { + self.callback(locked(callback)) + } + + /// Make the built AdvancedSubscriber an [`AdvancedSubscriber`](AdvancedSubscriber). + #[inline] + #[zenoh_macros::unstable] + pub fn with(self, handler: Handler) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler> + where + Handler: IntoHandler, + { + AdvancedSubscriberBuilder { + session: self.session, + key_expr: self.key_expr.map(|s| s.into_owned()), + origin: self.origin, + retransmission: self.retransmission, + query_target: self.query_target, + query_timeout: self.query_timeout, + history: self.history, + liveliness: self.liveliness, + meta_key_expr: self.meta_key_expr, + handler, + } + } +} + +#[zenoh_macros::unstable] +impl<'a, 'c, Handler> AdvancedSubscriberBuilder<'a, '_, 'c, Handler> { + /// Restrict the matching publications that will be receive by this [`Subscriber`] + /// to the ones that have the given [`Locality`](crate::prelude::Locality). + #[zenoh_macros::unstable] + #[inline] + pub fn allowed_origin(mut self, origin: Locality) -> Self { + self.origin = origin; + self + } + + /// Ask for retransmission of detected lost Samples. + /// + /// Retransmission can only be achieved by [`AdvancedPublishers`](crate::AdvancedPublisher) + /// that enable [`cache`](crate::AdvancedPublisherBuilder::cache) and + /// [`sample_miss_detection`](crate::AdvancedPublisherBuilder::sample_miss_detection). + #[zenoh_macros::unstable] + #[inline] + pub fn recovery(mut self, conf: RecoveryConfig) -> Self { + self.retransmission = Some(conf); + self + } + + // /// Change the target to be used for queries. + + // #[inline] + // pub fn query_target(mut self, query_target: QueryTarget) -> Self { + // self.query_target = query_target; + // self + // } + + /// Change the timeout to be used for queries (history, retransmission). + #[zenoh_macros::unstable] + #[inline] + pub fn query_timeout(mut self, query_timeout: Duration) -> Self { + self.query_timeout = query_timeout; + self + } + + /// Enable query for historical data. + /// + /// History can only be retransmitted by [`AdvancedPublishers`](crate::AdvancedPublisher) that enable [`cache`](crate::AdvancedPublisherBuilder::cache). + #[zenoh_macros::unstable] + #[inline] + pub fn history(mut self, config: HistoryConfig) -> Self { + self.history = Some(config); + self + } + + /// Allow this subscriber to be detected through liveliness. + #[zenoh_macros::unstable] + pub fn subscriber_detection(mut self) -> Self { + self.liveliness = true; + self + } + + /// A key expression added to the liveliness token key expression. + /// It can be used to convey meta data. + #[zenoh_macros::unstable] + pub fn subscriber_detection_metadata(mut self, meta: TryIntoKeyExpr) -> Self + where + TryIntoKeyExpr: TryInto>, + >>::Error: Into, + { + self.meta_key_expr = Some(meta.try_into().map_err(Into::into)); + self + } + + #[zenoh_macros::unstable] + fn with_static_keys(self) -> AdvancedSubscriberBuilder<'a, 'static, 'static, Handler> { + AdvancedSubscriberBuilder { + session: self.session, + key_expr: self.key_expr.map(|s| s.into_owned()), + origin: self.origin, + retransmission: self.retransmission, + query_target: self.query_target, + query_timeout: self.query_timeout, + history: self.history, + liveliness: self.liveliness, + meta_key_expr: self.meta_key_expr.map(|s| s.map(|s| s.into_owned())), + handler: self.handler, + } + } +} + +#[zenoh_macros::unstable] +impl Resolvable for AdvancedSubscriberBuilder<'_, '_, '_, Handler> +where + Handler: IntoHandler, + Handler::Handler: Send, +{ + type To = ZResult>; +} + +#[zenoh_macros::unstable] +impl Wait for AdvancedSubscriberBuilder<'_, '_, '_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + AdvancedSubscriber::new(self.with_static_keys()) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for AdvancedSubscriberBuilder<'_, '_, '_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +#[zenoh_macros::unstable] +struct Period { + timer: Timer, + period: Duration, +} + +#[zenoh_macros::unstable] +struct State { + next_id: usize, + global_pending_queries: u64, + sequenced_states: HashMap>, + timestamped_states: HashMap>, + session: Session, + key_expr: KeyExpr<'static>, + retransmission: bool, + period: Option, + query_target: QueryTarget, + query_timeout: Duration, + callback: Callback, + miss_handlers: HashMap>, +} + +#[zenoh_macros::unstable] +impl State { + #[zenoh_macros::unstable] + fn register_miss_callback(&mut self, callback: Callback) -> usize { + let id = self.next_id; + self.next_id += 1; + self.miss_handlers.insert(id, callback); + id + } + #[zenoh_macros::unstable] + fn unregister_miss_callback(&mut self, id: &usize) { + self.miss_handlers.remove(id); + } +} + +macro_rules! spawn_periodoic_queries { + ($p:expr,$s:expr,$r:expr) => {{ + if let Some(period) = &$p.period { + period.timer.add(TimedEvent::periodic( + period.period, + PeriodicQuery { + source_id: $s, + statesref: $r, + }, + )) + } + }}; +} + +#[zenoh_macros::unstable] +struct SourceState { + last_delivered: Option, + pending_queries: u64, + pending_samples: BTreeMap, +} + +/// [`AdvancedSubscriber`]. +#[zenoh_macros::unstable] +pub struct AdvancedSubscriber { + statesref: Arc>, + subscriber: Subscriber<()>, + receiver: Receiver, + _liveliness_subscriber: Option>, + _token: Option, +} + +#[zenoh_macros::unstable] +impl std::ops::Deref for AdvancedSubscriber { + type Target = Receiver; + fn deref(&self) -> &Self::Target { + &self.receiver + } +} + +#[zenoh_macros::unstable] +impl std::ops::DerefMut for AdvancedSubscriber { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.receiver + } +} + +#[zenoh_macros::unstable] +fn handle_sample(states: &mut State, sample: Sample) -> bool { + if let (Some(source_id), Some(source_sn)) = ( + sample.source_info().source_id(), + sample.source_info().source_sn(), + ) { + let entry = states.sequenced_states.entry(*source_id); + let new = matches!(&entry, Entry::Vacant(_)); + let state = entry.or_insert(SourceState:: { + last_delivered: None, + pending_queries: 0, + pending_samples: BTreeMap::new(), + }); + if states.global_pending_queries != 0 { + state.pending_samples.insert(source_sn, sample); + } else if state.last_delivered.is_some() && source_sn != state.last_delivered.unwrap() + 1 { + if source_sn > state.last_delivered.unwrap() { + if states.retransmission { + state.pending_samples.insert(source_sn, sample); + } else { + tracing::info!( + "Sample missed: missed {} samples from {:?}.", + source_sn - state.last_delivered.unwrap() - 1, + source_id, + ); + for miss_callback in states.miss_handlers.values() { + miss_callback.call(Miss { + source: *source_id, + nb: source_sn - state.last_delivered.unwrap() - 1, + }); + } + states.callback.call(sample); + state.last_delivered = Some(source_sn); + } + } + } else { + states.callback.call(sample); + let mut last_seq_num = source_sn; + state.last_delivered = Some(last_seq_num); + while let Some(s) = state.pending_samples.remove(&(last_seq_num + 1)) { + states.callback.call(s); + last_seq_num += 1; + state.last_delivered = Some(last_seq_num); + } + } + new + } else if let Some(timestamp) = sample.timestamp() { + let entry = states.timestamped_states.entry(*timestamp.get_id()); + let state = entry.or_insert(SourceState:: { + last_delivered: None, + pending_queries: 0, + pending_samples: BTreeMap::new(), + }); + if state.last_delivered.map(|t| t < *timestamp).unwrap_or(true) { + if states.global_pending_queries == 0 && state.pending_queries == 0 { + state.last_delivered = Some(*timestamp); + states.callback.call(sample); + } else { + state.pending_samples.entry(*timestamp).or_insert(sample); + } + } + false + } else { + states.callback.call(sample); + false + } +} + +#[zenoh_macros::unstable] +fn seq_num_range(start: Option, end: Option) -> String { + match (start, end) { + (Some(start), Some(end)) => format!("_sn={}..{}", start, end), + (Some(start), None) => format!("_sn={}..", start), + (None, Some(end)) => format!("_sn=..{}", end), + (None, None) => "_sn=..".to_string(), + } +} + +#[zenoh_macros::unstable] +#[derive(Clone)] +struct PeriodicQuery { + source_id: EntityGlobalId, + statesref: Arc>, +} + +#[zenoh_macros::unstable] +#[async_trait] +impl Timed for PeriodicQuery { + async fn run(&mut self) { + let mut lock = zlock!(self.statesref); + let states = &mut *lock; + if let Some(state) = states.sequenced_states.get_mut(&self.source_id) { + state.pending_queries += 1; + let query_expr = KE_ADV_PREFIX + / KE_STAR + / &self.source_id.zid().into_keyexpr() + / &KeyExpr::try_from(self.source_id.eid().to_string()).unwrap() + / KE_STARSTAR + / KE_AT + / &states.key_expr; + let seq_num_range = seq_num_range(state.last_delivered.map(|s| s + 1), None); + + let session = states.session.clone(); + let key_expr = states.key_expr.clone().into_owned(); + let query_target = states.query_target; + let query_timeout = states.query_timeout; + drop(lock); + let handler = SequencedRepliesHandler { + source_id: self.source_id, + statesref: self.statesref.clone(), + }; + let _ = session + .get(Selector::from((query_expr, seq_num_range))) + .callback({ + move |r: Reply| { + if let Ok(s) = r.into_result() { + if key_expr.intersects(s.key_expr()) { + let states = &mut *zlock!(handler.statesref); + handle_sample(states, s); + } + } + } + }) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .target(query_target) + .timeout(query_timeout) + .wait(); + } + } +} + +#[zenoh_macros::unstable] +impl AdvancedSubscriber { + fn new(conf: AdvancedSubscriberBuilder<'_, '_, '_, H>) -> ZResult + where + H: IntoHandler + Send, + { + let (callback, receiver) = conf.handler.into_handler(); + let key_expr = conf.key_expr?; + let meta = match conf.meta_key_expr { + Some(meta) => Some(meta?), + None => None, + }; + let retransmission = conf.retransmission; + let query_target = conf.query_target; + let query_timeout = conf.query_timeout; + let session = conf.session.clone(); + let statesref = Arc::new(Mutex::new(State { + next_id: 0, + sequenced_states: HashMap::new(), + timestamped_states: HashMap::new(), + global_pending_queries: if conf.history.is_some() { 1 } else { 0 }, + session, + period: retransmission.as_ref().and_then(|r| { + let _rt = ZRuntime::Application.enter(); + r.periodic_queries.map(|p| Period { + timer: Timer::new(false), + period: p, + }) + }), + key_expr: key_expr.clone().into_owned(), + retransmission: retransmission.is_some(), + query_target: conf.query_target, + query_timeout: conf.query_timeout, + callback: callback.clone(), + miss_handlers: HashMap::new(), + })); + + let sub_callback = { + let statesref = statesref.clone(); + let session = conf.session.clone(); + let key_expr = key_expr.clone().into_owned(); + + move |s: Sample| { + let mut lock = zlock!(statesref); + let states = &mut *lock; + let source_id = s.source_info().source_id().cloned(); + let new = handle_sample(states, s); + + if let Some(source_id) = source_id { + if new { + spawn_periodoic_queries!(states, source_id, statesref.clone()); + } + + if let Some(state) = states.sequenced_states.get_mut(&source_id) { + if retransmission.is_some() + && state.pending_queries == 0 + && !state.pending_samples.is_empty() + { + state.pending_queries += 1; + let query_expr = KE_ADV_PREFIX + / KE_STAR + / &source_id.zid().into_keyexpr() + / &KeyExpr::try_from(source_id.eid().to_string()).unwrap() + / KE_STARSTAR + / KE_AT + / &key_expr; + let seq_num_range = + seq_num_range(state.last_delivered.map(|s| s + 1), None); + drop(lock); + let handler = SequencedRepliesHandler { + source_id, + statesref: statesref.clone(), + }; + let _ = session + .get(Selector::from((query_expr, seq_num_range))) + .callback({ + let key_expr = key_expr.clone().into_owned(); + move |r: Reply| { + if let Ok(s) = r.into_result() { + if key_expr.intersects(s.key_expr()) { + let states = &mut *zlock!(handler.statesref); + handle_sample(states, s); + } + } + } + }) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .target(query_target) + .timeout(query_timeout) + .wait(); + } + } + } + } + }; + + let subscriber = conf + .session + .declare_subscriber(&key_expr) + .callback(sub_callback) + .allowed_origin(conf.origin) + .wait()?; + + if let Some(historyconf) = conf.history.as_ref() { + let handler = InitialRepliesHandler { + statesref: statesref.clone(), + }; + let mut params = Parameters::empty(); + if let Some(max) = historyconf.sample_depth { + params.insert("_max", max.to_string()); + } + if let Some(age) = historyconf.age { + params.set_time_range(TimeRange { + start: TimeBound::Inclusive(TimeExpr::Now { offset_secs: -age }), + end: TimeBound::Unbounded, + }); + } + let _ = conf + .session + .get(Selector::from(( + KE_ADV_PREFIX / KE_STARSTAR / KE_AT / &key_expr, + params, + ))) + .callback({ + let key_expr = key_expr.clone().into_owned(); + move |r: Reply| { + if let Ok(s) = r.into_result() { + if key_expr.intersects(s.key_expr()) { + let states = &mut *zlock!(handler.statesref); + handle_sample(states, s); + } + } + } + }) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .target(query_target) + .timeout(query_timeout) + .wait(); + } + + let liveliness_subscriber = if let Some(historyconf) = conf.history { + if historyconf.liveliness { + let live_callback = { + let session = conf.session.clone(); + let statesref = statesref.clone(); + let key_expr = key_expr.clone().into_owned(); + move |s: Sample| { + if s.kind() == SampleKind::Put { + if let Ok(parsed) = ke_liveliness::parse(s.key_expr().as_keyexpr()) { + if let Ok(zid) = ZenohId::from_str(parsed.zid().as_str()) { + // TODO : If we already have a state associated to this discovered source + // we should query with the appropriate range to avoid unnecessary retransmissions + if parsed.eid() == KE_UHLC { + let mut lock = zlock!(statesref); + let states = &mut *lock; + let entry = states.timestamped_states.entry(ID::from(zid)); + let state = entry.or_insert(SourceState:: { + last_delivered: None, + pending_queries: 0, + pending_samples: BTreeMap::new(), + }); + state.pending_queries += 1; + drop(lock); + + let handler = TimestampedRepliesHandler { + id: ID::from(zid), + statesref: statesref.clone(), + callback: callback.clone(), + }; + let mut params = Parameters::empty(); + if let Some(max) = historyconf.sample_depth { + params.insert("_max", max.to_string()); + } + if let Some(age) = historyconf.age { + params.set_time_range(TimeRange { + start: TimeBound::Inclusive(TimeExpr::Now { + offset_secs: -age, + }), + end: TimeBound::Unbounded, + }); + } + let _ = session + .get(Selector::from((s.key_expr(), params))) + .callback({ + let key_expr = key_expr.clone().into_owned(); + move |r: Reply| { + if let Ok(s) = r.into_result() { + if key_expr.intersects(s.key_expr()) { + let states = + &mut *zlock!(handler.statesref); + handle_sample(states, s); + } + } + } + }) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .target(query_target) + .timeout(query_timeout) + .wait(); + } else if let Ok(eid) = + EntityId::from_str(parsed.eid().as_str()) + { + let source_id = EntityGlobalId::new(zid, eid); + let mut lock = zlock!(statesref); + let states = &mut *lock; + let entry = states.sequenced_states.entry(source_id); + let new = matches!(&entry, Entry::Vacant(_)); + let state = entry.or_insert(SourceState:: { + last_delivered: None, + pending_queries: 0, + pending_samples: BTreeMap::new(), + }); + state.pending_queries += 1; + drop(lock); + + let handler = SequencedRepliesHandler { + source_id, + statesref: statesref.clone(), + }; + let mut params = Parameters::empty(); + if let Some(max) = historyconf.sample_depth { + params.insert("_max", max.to_string()); + } + if let Some(age) = historyconf.age { + params.set_time_range(TimeRange { + start: TimeBound::Inclusive(TimeExpr::Now { + offset_secs: -age, + }), + end: TimeBound::Unbounded, + }); + } + let _ = session + .get(Selector::from((s.key_expr(), params))) + .callback({ + let key_expr = key_expr.clone().into_owned(); + move |r: Reply| { + if let Ok(s) = r.into_result() { + if key_expr.intersects(s.key_expr()) { + let states = + &mut *zlock!(handler.statesref); + handle_sample(states, s); + } + } + } + }) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .target(query_target) + .timeout(query_timeout) + .wait(); + + if new { + spawn_periodoic_queries!( + zlock!(statesref), + source_id, + statesref.clone() + ); + } + } + } else { + let mut lock = zlock!(statesref); + let states = &mut *lock; + states.global_pending_queries += 1; + drop(lock); + + let handler = InitialRepliesHandler { + statesref: statesref.clone(), + }; + let mut params = Parameters::empty(); + if let Some(max) = historyconf.sample_depth { + params.insert("_max", max.to_string()); + } + if let Some(age) = historyconf.age { + params.set_time_range(TimeRange { + start: TimeBound::Inclusive(TimeExpr::Now { + offset_secs: -age, + }), + end: TimeBound::Unbounded, + }); + } + let _ = session + .get(Selector::from((s.key_expr(), params))) + .callback({ + let key_expr = key_expr.clone().into_owned(); + move |r: Reply| { + if let Ok(s) = r.into_result() { + if key_expr.intersects(s.key_expr()) { + let states = + &mut *zlock!(handler.statesref); + handle_sample(states, s); + } + } + } + }) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .target(query_target) + .timeout(query_timeout) + .wait(); + } + } else { + tracing::warn!( + "Received malformed liveliness token key expression: {}", + s.key_expr() + ); + } + } + } + }; + + Some( + conf + .session + .liveliness() + .declare_subscriber(KE_ADV_PREFIX / KE_PUB / KE_STARSTAR / KE_AT / &key_expr) + // .declare_subscriber(keformat!(ke_liveliness_all::formatter(), zid = 0, eid = 0, remaining = key_expr).unwrap()) + .history(true) + .callback(live_callback) + .wait()?, + ) + } else { + None + } + } else { + None + }; + + let token = if conf.liveliness { + let prefix = KE_ADV_PREFIX + / KE_SUB + / &subscriber.id().zid().into_keyexpr() + / &KeyExpr::try_from(subscriber.id().eid().to_string()).unwrap(); + let prefix = match meta { + Some(meta) => prefix / &meta / KE_AT, + // We need this empty chunk because af a routing matching bug + _ => prefix / KE_EMPTY / KE_AT, + }; + Some( + conf.session + .liveliness() + .declare_token(prefix / &key_expr) + .wait()?, + ) + } else { + None + }; + + let reliable_subscriber = AdvancedSubscriber { + statesref, + subscriber, + receiver, + _liveliness_subscriber: liveliness_subscriber, + _token: token, + }; + + Ok(reliable_subscriber) + } + + /// Returns the [`EntityGlobalId`] of this AdvancedSubscriber. + #[zenoh_macros::unstable] + pub fn id(&self) -> EntityGlobalId { + self.subscriber.id() + } + + /// Returns the [`KeyExpr`] this subscriber subscribes to. + #[zenoh_macros::unstable] + pub fn key_expr(&self) -> &KeyExpr<'static> { + self.subscriber.key_expr() + } + + /// Returns a reference to this subscriber's handler. + /// An handler is anything that implements [`zenoh::handlers::IntoHandler`]. + /// The default handler is [`zenoh::handlers::DefaultHandler`]. + #[zenoh_macros::unstable] + pub fn handler(&self) -> &Handler { + &self.receiver + } + + /// Returns a mutable reference to this subscriber's handler. + /// An handler is anything that implements [`zenoh::handlers::IntoHandler`]. + /// The default handler is [`zenoh::handlers::DefaultHandler`]. + #[zenoh_macros::unstable] + pub fn handler_mut(&mut self) -> &mut Handler { + &mut self.receiver + } + + /// Declares a listener to detect missed samples. + /// + /// Missed samples can only be detected from [`AdvancedPublisher`](crate::AdvancedPublisher) that + /// enable [`sample_miss_detection`](crate::AdvancedPublisherBuilder::sample_miss_detection). + #[zenoh_macros::unstable] + pub fn sample_miss_listener(&self) -> SampleMissListenerBuilder<'_, DefaultHandler> { + SampleMissListenerBuilder { + statesref: &self.statesref, + handler: DefaultHandler::default(), + } + } + + /// Declares a listener to detect matching publishers. + /// + /// Only [`AdvancedPublisher`](crate::AdvancedPublisher) that enable + /// [`publisher_detection`](crate::AdvancedPublisherBuilder::publisher_detection) can be detected. + #[zenoh_macros::unstable] + pub fn detect_publishers(&self) -> LivelinessSubscriberBuilder<'_, '_, DefaultHandler> { + self.subscriber.session().liveliness().declare_subscriber( + KE_ADV_PREFIX / KE_PUB / KE_STARSTAR / KE_AT / self.subscriber.key_expr(), + ) + } + + /// Undeclares this AdvancedSubscriber + #[inline] + #[zenoh_macros::unstable] + pub fn undeclare(self) -> impl Resolve> { + self.subscriber.undeclare() + } +} + +#[zenoh_macros::unstable] +#[inline] +fn flush_sequenced_source( + state: &mut SourceState, + callback: &Callback, + source_id: &EntityGlobalId, + miss_handlers: &HashMap>, +) { + if state.pending_queries == 0 && !state.pending_samples.is_empty() { + let mut pending_samples = BTreeMap::new(); + std::mem::swap(&mut state.pending_samples, &mut pending_samples); + for (seq_num, sample) in pending_samples { + match state.last_delivered { + None => { + state.last_delivered = Some(seq_num); + callback.call(sample); + } + Some(last) if seq_num == last + 1 => { + state.last_delivered = Some(seq_num); + callback.call(sample); + } + Some(last) if seq_num > last + 1 => { + tracing::warn!( + "Sample missed: missed {} samples from {:?}.", + seq_num - last - 1, + source_id, + ); + for miss_callback in miss_handlers.values() { + miss_callback.call(Miss { + source: *source_id, + nb: seq_num - last - 1, + }) + } + state.last_delivered = Some(seq_num); + callback.call(sample); + } + _ => { + // duplicate + } + } + } + } +} + +#[zenoh_macros::unstable] +#[inline] +fn flush_timestamped_source(state: &mut SourceState, callback: &Callback) { + if state.pending_queries == 0 && !state.pending_samples.is_empty() { + let mut pending_samples = BTreeMap::new(); + std::mem::swap(&mut state.pending_samples, &mut pending_samples); + for (timestamp, sample) in pending_samples { + if state + .last_delivered + .map(|last| timestamp > last) + .unwrap_or(true) + { + state.last_delivered = Some(timestamp); + callback.call(sample); + } + } + } +} + +#[zenoh_macros::unstable] +#[derive(Clone)] +struct InitialRepliesHandler { + statesref: Arc>, +} + +#[zenoh_macros::unstable] +impl Drop for InitialRepliesHandler { + fn drop(&mut self) { + let states = &mut *zlock!(self.statesref); + states.global_pending_queries = states.global_pending_queries.saturating_sub(1); + + if states.global_pending_queries == 0 { + for (source_id, state) in states.sequenced_states.iter_mut() { + flush_sequenced_source(state, &states.callback, source_id, &states.miss_handlers); + spawn_periodoic_queries!(states, *source_id, self.statesref.clone()); + } + for state in states.timestamped_states.values_mut() { + flush_timestamped_source(state, &states.callback); + } + } + } +} + +#[zenoh_macros::unstable] +#[derive(Clone)] +struct SequencedRepliesHandler { + source_id: EntityGlobalId, + statesref: Arc>, +} + +#[zenoh_macros::unstable] +impl Drop for SequencedRepliesHandler { + fn drop(&mut self) { + let states = &mut *zlock!(self.statesref); + if let Some(state) = states.sequenced_states.get_mut(&self.source_id) { + state.pending_queries = state.pending_queries.saturating_sub(1); + if states.global_pending_queries == 0 { + flush_sequenced_source( + state, + &states.callback, + &self.source_id, + &states.miss_handlers, + ) + } + } + } +} + +#[zenoh_macros::unstable] +#[derive(Clone)] +struct TimestampedRepliesHandler { + id: ID, + statesref: Arc>, + callback: Callback, +} + +#[zenoh_macros::unstable] +impl Drop for TimestampedRepliesHandler { + fn drop(&mut self) { + let states = &mut *zlock!(self.statesref); + if let Some(state) = states.timestamped_states.get_mut(&self.id) { + state.pending_queries = state.pending_queries.saturating_sub(1); + if states.global_pending_queries == 0 { + flush_timestamped_source(state, &self.callback); + } + } + } +} + +/// A struct that represent missed samples. +#[zenoh_macros::unstable] +pub struct Miss { + source: EntityGlobalId, + nb: u32, +} + +impl Miss { + /// The source of missed samples. + pub fn source(&self) -> EntityGlobalId { + self.source + } + + /// The number of missed samples. + pub fn nb(&self) -> u32 { + self.nb + } +} + +/// A listener to detect missed samples. +/// +/// Missed samples can only be detected from [`AdvancedPublisher`](crate::AdvancedPublisher) that +/// enable [`sample_miss_detection`](crate::AdvancedPublisherBuilder::sample_miss_detection). +#[zenoh_macros::unstable] +pub struct SampleMissListener { + id: usize, + statesref: Arc>, + handler: Handler, +} + +#[zenoh_macros::unstable] +impl SampleMissListener { + #[inline] + pub fn undeclare(self) -> SampleMissHandlerUndeclaration + where + Handler: Send, + { + // self.undeclare_inner(()) + SampleMissHandlerUndeclaration(self) + } + + fn undeclare_impl(&mut self) -> ZResult<()> { + // set the flag first to avoid double panic if this function panic + zlock!(self.statesref).unregister_miss_callback(&self.id); + Ok(()) + } +} + +#[cfg(feature = "unstable")] +impl Drop for SampleMissListener { + fn drop(&mut self) { + if let Err(error) = self.undeclare_impl() { + tracing::error!(error); + } + } +} + +// #[zenoh_macros::unstable] +// impl UndeclarableSealed<()> for SampleMissHandler { +// type Undeclaration = SampleMissHandlerUndeclaration; + +// fn undeclare_inner(self, _: ()) -> Self::Undeclaration { +// SampleMissHandlerUndeclaration(self) +// } +// } + +#[zenoh_macros::unstable] +impl std::ops::Deref for SampleMissListener { + type Target = Handler; + + fn deref(&self) -> &Self::Target { + &self.handler + } +} +#[zenoh_macros::unstable] +impl std::ops::DerefMut for SampleMissListener { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.handler + } +} + +/// A [`Resolvable`] returned when undeclaring a [`SampleMissListener`]. +#[zenoh_macros::unstable] +pub struct SampleMissHandlerUndeclaration(SampleMissListener); + +#[zenoh_macros::unstable] +impl Resolvable for SampleMissHandlerUndeclaration { + type To = ZResult<()>; +} + +#[zenoh_macros::unstable] +impl Wait for SampleMissHandlerUndeclaration { + fn wait(mut self) -> ::To { + self.0.undeclare_impl() + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for SampleMissHandlerUndeclaration { + type Output = ::To; + type IntoFuture = Ready<::To>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +/// A builder for initializing a [`SampleMissListener`]. +#[zenoh_macros::unstable] +pub struct SampleMissListenerBuilder<'a, Handler, const BACKGROUND: bool = false> { + statesref: &'a Arc>, + handler: Handler, +} + +#[zenoh_macros::unstable] +impl<'a> SampleMissListenerBuilder<'a, DefaultHandler> { + /// Receive the sample miss notification with a callback. + #[inline] + #[zenoh_macros::unstable] + pub fn callback(self, callback: F) -> SampleMissListenerBuilder<'a, Callback> + where + F: Fn(Miss) + Send + Sync + 'static, + { + self.with(Callback::new(Arc::new(callback))) + } + + /// Receive the sample miss notification with a mutable callback. + #[inline] + #[zenoh_macros::unstable] + pub fn callback_mut(self, callback: F) -> SampleMissListenerBuilder<'a, Callback> + where + F: FnMut(Miss) + Send + Sync + 'static, + { + self.callback(zenoh::handlers::locked(callback)) + } + + /// Receive the sample miss notification with a [`Handler`](IntoHandler). + #[inline] + #[zenoh_macros::unstable] + pub fn with(self, handler: Handler) -> SampleMissListenerBuilder<'a, Handler> + where + Handler: IntoHandler, + { + SampleMissListenerBuilder { + statesref: self.statesref, + handler, + } + } +} + +#[zenoh_macros::unstable] +impl<'a> SampleMissListenerBuilder<'a, Callback> { + /// Register the sample miss notification callback to be run in background until the adanced subscriber is undeclared. + /// + /// Background builder doesn't return a `SampleMissHandler` object anymore. + #[zenoh_macros::unstable] + pub fn background(self) -> SampleMissListenerBuilder<'a, Callback, true> { + SampleMissListenerBuilder { + statesref: self.statesref, + handler: self.handler, + } + } +} + +#[zenoh_macros::unstable] +impl Resolvable for SampleMissListenerBuilder<'_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type To = ZResult>; +} + +#[zenoh_macros::unstable] +impl Wait for SampleMissListenerBuilder<'_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + let (callback, handler) = self.handler.into_handler(); + let id = zlock!(self.statesref).register_miss_callback(callback); + Ok(SampleMissListener { + id, + statesref: self.statesref.clone(), + handler, + }) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for SampleMissListenerBuilder<'_, Handler> +where + Handler: IntoHandler + Send, + Handler::Handler: Send, +{ + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} + +#[zenoh_macros::unstable] +impl Resolvable for SampleMissListenerBuilder<'_, Callback, true> { + type To = ZResult<()>; +} + +#[zenoh_macros::unstable] +impl Wait for SampleMissListenerBuilder<'_, Callback, true> { + #[zenoh_macros::unstable] + fn wait(self) -> ::To { + let (callback, _) = self.handler.into_handler(); + zlock!(self.statesref).register_miss_callback(callback); + Ok(()) + } +} + +#[zenoh_macros::unstable] +impl IntoFuture for SampleMissListenerBuilder<'_, Callback, true> { + type Output = ::To; + type IntoFuture = Ready<::To>; + + #[zenoh_macros::unstable] + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.wait()) + } +} diff --git a/zenoh-ext/src/lib.rs b/zenoh-ext/src/lib.rs index 4bab50804e..2d73bd16c7 100644 --- a/zenoh-ext/src/lib.rs +++ b/zenoh-ext/src/lib.rs @@ -12,10 +12,18 @@ // ZettaScale Zenoh Team, // #[cfg(feature = "unstable")] +mod advanced_cache; +#[cfg(feature = "unstable")] +mod advanced_publisher; +#[cfg(feature = "unstable")] +mod advanced_subscriber; +#[cfg(feature = "unstable")] pub mod group; #[cfg(feature = "unstable")] mod publication_cache; #[cfg(feature = "unstable")] +mod publisher_ext; +#[cfg(feature = "unstable")] mod querying_subscriber; mod serialization; #[cfg(feature = "unstable")] @@ -30,12 +38,20 @@ pub use crate::serialization::{ ZReadIter, ZSerializer, }; #[cfg(feature = "unstable")] +#[allow(deprecated)] pub use crate::{ + advanced_cache::{CacheConfig, RepliesConfig}, + advanced_publisher::{AdvancedPublisher, AdvancedPublisherBuilder}, + advanced_subscriber::{ + AdvancedSubscriber, AdvancedSubscriberBuilder, HistoryConfig, Miss, RecoveryConfig, + SampleMissHandlerUndeclaration, SampleMissListener, SampleMissListenerBuilder, + }, publication_cache::{PublicationCache, PublicationCacheBuilder}, + publisher_ext::AdvancedPublisherBuilderExt, querying_subscriber::{ ExtractSample, FetchingSubscriber, FetchingSubscriberBuilder, KeySpace, LivelinessSpace, QueryingSubscriberBuilder, UserSpace, }, session_ext::SessionExt, - subscriber_ext::{SubscriberBuilderExt, SubscriberForward}, + subscriber_ext::{AdvancedSubscriberBuilderExt, SubscriberBuilderExt, SubscriberForward}, }; diff --git a/zenoh-ext/src/publication_cache.rs b/zenoh-ext/src/publication_cache.rs index 403a887100..bdc1d3d11e 100644 --- a/zenoh-ext/src/publication_cache.rs +++ b/zenoh-ext/src/publication_cache.rs @@ -31,6 +31,7 @@ use zenoh::{ /// The builder of PublicationCache, allowing to configure it. #[zenoh_macros::unstable] #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub struct PublicationCacheBuilder<'a, 'b, 'c, const BACKGROUND: bool = false> { session: &'a Session, pub_key_expr: ZResult>, @@ -41,6 +42,8 @@ pub struct PublicationCacheBuilder<'a, 'b, 'c, const BACKGROUND: bool = false> { resources_limit: Option, } +#[allow(deprecated)] +#[zenoh_macros::unstable] impl<'a, 'b, 'c> PublicationCacheBuilder<'a, 'b, 'c> { pub(crate) fn new( session: &'a Session, @@ -58,6 +61,8 @@ impl<'a, 'b, 'c> PublicationCacheBuilder<'a, 'b, 'c> { } /// Change the prefix used for queryable. + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn queryable_prefix(mut self, queryable_prefix: TryIntoKeyExpr) -> Self where TryIntoKeyExpr: TryInto>, @@ -71,29 +76,38 @@ impl<'a, 'b, 'c> PublicationCacheBuilder<'a, 'b, 'c> { /// to the ones that have the given [`Locality`](zenoh::prelude::Locality). #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn queryable_allowed_origin(mut self, origin: Locality) -> Self { self.queryable_origin = Some(origin); self } /// Set completeness option for the queryable. + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn queryable_complete(mut self, complete: bool) -> Self { self.complete = Some(complete); self } /// Change the history size for each resource. + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn history(mut self, history: usize) -> Self { self.history = history; self } /// Change the limit number of cached resources. + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn resources_limit(mut self, limit: usize) -> Self { self.resources_limit = Some(limit); self } + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn background(self) -> PublicationCacheBuilder<'a, 'b, 'c, true> { PublicationCacheBuilder { session: self.session, @@ -107,52 +121,73 @@ impl<'a, 'b, 'c> PublicationCacheBuilder<'a, 'b, 'c> { } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Resolvable for PublicationCacheBuilder<'_, '_, '_> { type To = ZResult; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Wait for PublicationCacheBuilder<'_, '_, '_> { + #[zenoh_macros::unstable] fn wait(self) -> ::To { PublicationCache::new(self) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl IntoFuture for PublicationCacheBuilder<'_, '_, '_> { type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Resolvable for PublicationCacheBuilder<'_, '_, '_, true> { type To = ZResult<()>; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Wait for PublicationCacheBuilder<'_, '_, '_, true> { + #[zenoh_macros::unstable] fn wait(self) -> ::To { PublicationCache::new(self).map(|_| ()) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl IntoFuture for PublicationCacheBuilder<'_, '_, '_, true> { type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } } +/// [`PublicationCache`]. #[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub struct PublicationCache { local_sub: Subscriber>, _queryable: Queryable>, task: TerminatableTask, } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl PublicationCache { + #[zenoh_macros::unstable] fn new( conf: PublicationCacheBuilder<'_, '_, '_, BACKGROUND>, ) -> ZResult { @@ -297,7 +332,9 @@ impl PublicationCache { } /// Undeclare this [`PublicationCache`]`. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn undeclare(self) -> impl Resolve> { ResolveFuture::new(async move { let PublicationCache { @@ -313,11 +350,15 @@ impl PublicationCache { } #[zenoh_macros::internal] + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn set_background(&mut self, background: bool) { self.local_sub.set_background(background); self._queryable.set_background(background); } + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn key_expr(&self) -> &KeyExpr<'static> { self.local_sub.key_expr() } diff --git a/zenoh-ext/src/publisher_ext.rs b/zenoh-ext/src/publisher_ext.rs new file mode 100644 index 0000000000..de045d0ff0 --- /dev/null +++ b/zenoh-ext/src/publisher_ext.rs @@ -0,0 +1,73 @@ +// +// 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 zenoh::pubsub::PublisherBuilder; + +use crate::{advanced_cache::CacheConfig, AdvancedPublisherBuilder}; + +/// Some extensions to the [`zenoh::publication::PublisherBuilder`](zenoh::publication::PublisherBuilder) +#[zenoh_macros::unstable] +pub trait AdvancedPublisherBuilderExt<'a, 'b, 'c> { + /// Allow matching [`AdvancedSubscribers`](crate::AdvancedSubscriber) to recover history and/or missed samples. + #[zenoh_macros::unstable] + fn cache(self, config: CacheConfig) -> AdvancedPublisherBuilder<'a, 'b, 'c>; + + /// Allow matching [`AdvancedSubscribers`](crate::AdvancedSubscriber) to detect lost samples + /// and optionally ask for retransimission. + /// + /// Retransmission can only be achieved if [`cache`](crate::AdvancedPublisherBuilder::cache) is also enabled. + #[zenoh_macros::unstable] + fn sample_miss_detection(self) -> AdvancedPublisherBuilder<'a, 'b, 'c>; + + /// Allow this publisher to be detected by [`AdvancedSubscribers`](crate::AdvancedSubscriber). + /// + /// This allows [`AdvancedSubscribers`](crate::AdvancedSubscriber) to retrieve the local history. + #[zenoh_macros::unstable] + fn publisher_detection(self) -> AdvancedPublisherBuilder<'a, 'b, 'c>; + + /// Turn this `Publisher` into an `AdvancedPublisher`. + #[zenoh_macros::unstable] + fn advanced(self) -> AdvancedPublisherBuilder<'a, 'b, 'c>; +} + +#[zenoh_macros::unstable] +impl<'a, 'b, 'c> AdvancedPublisherBuilderExt<'a, 'b, 'c> for PublisherBuilder<'a, 'b> { + /// Allow matching [`AdvancedSubscribers`](crate::AdvancedSubscriber) to recover history and/or missed samples. + #[zenoh_macros::unstable] + fn cache(self, config: CacheConfig) -> AdvancedPublisherBuilder<'a, 'b, 'c> { + AdvancedPublisherBuilder::new(self).cache(config) + } + + /// Allow matching [`AdvancedSubscribers`](crate::AdvancedSubscriber) to detect lost samples + /// and optionally ask for retransimission. + /// + /// Retransmission can only be achieved if [`cache`](crate::AdvancedPublisherBuilder::cache) is also enabled. + #[zenoh_macros::unstable] + fn sample_miss_detection(self) -> AdvancedPublisherBuilder<'a, 'b, 'c> { + AdvancedPublisherBuilder::new(self).sample_miss_detection() + } + + /// Allow this publisher to be detected by [`AdvancedSubscribers`](crate::AdvancedSubscriber). + /// + /// This allows [`AdvancedSubscribers`](crate::AdvancedSubscriber) to retrieve the local history. + #[zenoh_macros::unstable] + fn publisher_detection(self) -> AdvancedPublisherBuilder<'a, 'b, 'c> { + AdvancedPublisherBuilder::new(self).publisher_detection() + } + + /// Turn this `Publisher` into an `AdvancedPublisher`. + #[zenoh_macros::unstable] + fn advanced(self) -> AdvancedPublisherBuilder<'a, 'b, 'c> { + AdvancedPublisherBuilder::new(self) + } +} diff --git a/zenoh-ext/src/querying_subscriber.rs b/zenoh-ext/src/querying_subscriber.rs index fa9981e2b5..9a4943756b 100644 --- a/zenoh-ext/src/querying_subscriber.rs +++ b/zenoh-ext/src/querying_subscriber.rs @@ -33,6 +33,7 @@ use zenoh::{ /// The space of keys to use in a [`FetchingSubscriber`]. #[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub enum KeySpace { User, Liveliness, @@ -42,8 +43,10 @@ pub enum KeySpace { #[zenoh_macros::unstable] #[non_exhaustive] #[derive(Debug, Clone, Copy)] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub struct UserSpace; +#[allow(deprecated)] impl From for KeySpace { fn from(_: UserSpace) -> Self { KeySpace::User @@ -54,9 +57,13 @@ impl From for KeySpace { #[zenoh_macros::unstable] #[non_exhaustive] #[derive(Debug, Clone, Copy)] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub struct LivelinessSpace; +#[zenoh_macros::unstable] +#[allow(deprecated)] impl From for KeySpace { + #[zenoh_macros::unstable] fn from(_: LivelinessSpace) -> Self { KeySpace::Liveliness } @@ -65,6 +72,7 @@ impl From for KeySpace { /// The builder of [`FetchingSubscriber`], allowing to configure it. #[zenoh_macros::unstable] #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub struct QueryingSubscriberBuilder<'a, 'b, KeySpace, Handler, const BACKGROUND: bool = false> { pub(crate) session: &'a Session, pub(crate) key_expr: ZResult>, @@ -78,9 +86,13 @@ pub struct QueryingSubscriberBuilder<'a, 'b, KeySpace, Handler, const BACKGROUND pub(crate) handler: Handler, } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl<'a, 'b, KeySpace> QueryingSubscriberBuilder<'a, 'b, KeySpace, DefaultHandler> { /// Add callback to [`FetchingSubscriber`]. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn callback( self, callback: F, @@ -99,7 +111,9 @@ impl<'a, 'b, KeySpace> QueryingSubscriberBuilder<'a, 'b, KeySpace, DefaultHandle /// /// Subscriber will not be undeclared when dropped, with the callback running /// in background until the session is closed. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn callback_mut( self, callback: F, @@ -111,7 +125,9 @@ impl<'a, 'b, KeySpace> QueryingSubscriberBuilder<'a, 'b, KeySpace, DefaultHandle } /// Use the given handler to receive Samples. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn with( self, handler: Handler, @@ -146,10 +162,15 @@ impl<'a, 'b, KeySpace> QueryingSubscriberBuilder<'a, 'b, KeySpace, DefaultHandle } } +#[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] impl<'a, 'b, KeySpace> QueryingSubscriberBuilder<'a, 'b, KeySpace, Callback> { /// Register the subscriber callback to be run in background until the session is closed. /// /// Background builder doesn't return a `FetchingSubscriber` object anymore. + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn background(self) -> QueryingSubscriberBuilder<'a, 'b, KeySpace, Callback, true> { QueryingSubscriberBuilder { session: self.session, @@ -166,6 +187,9 @@ impl<'a, 'b, KeySpace> QueryingSubscriberBuilder<'a, 'b, KeySpace, Callback QueryingSubscriberBuilder<'_, 'b, UserSpace, Handler, BACKGROUND> { @@ -175,13 +199,16 @@ impl<'b, Handler, const BACKGROUND: bool> /// to the ones that have the given [`Locality`](Locality). #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn allowed_origin(mut self, origin: Locality) -> Self { self.origin = origin; self } /// Change the selector to be used for queries. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn query_selector(mut self, query_selector: IntoSelector) -> Self where IntoSelector: TryInto>, @@ -192,14 +219,18 @@ impl<'b, Handler, const BACKGROUND: bool> } /// Change the target to be used for queries. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn query_target(mut self, query_target: QueryTarget) -> Self { self.query_target = query_target; self } /// Change the consolidation mode to be used for queries. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn query_consolidation>( mut self, query_consolidation: QC, @@ -209,24 +240,33 @@ impl<'b, Handler, const BACKGROUND: bool> } /// Change the accepted replies for queries. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn query_accept_replies(mut self, accept_replies: ReplyKeyExpr) -> Self { self.query_accept_replies = accept_replies; self } } +#[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] impl<'a, 'b, KeySpace, Handler, const BACKGROUND: bool> QueryingSubscriberBuilder<'a, 'b, KeySpace, Handler, BACKGROUND> { /// Change the timeout to be used for queries. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn query_timeout(mut self, query_timeout: Duration) -> Self { self.query_timeout = query_timeout; self } + #[zenoh_macros::unstable] #[allow(clippy::type_complexity)] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] fn into_fetching_subscriber_builder( self, ) -> ZResult< @@ -285,6 +325,8 @@ impl<'a, 'b, KeySpace, Handler, const BACKGROUND: bool> } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Resolvable for QueryingSubscriberBuilder<'_, '_, KeySpace, Handler> where Handler: IntoHandler, @@ -293,17 +335,22 @@ where type To = ZResult>; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Wait for QueryingSubscriberBuilder<'_, '_, KeySpace, Handler> where KeySpace: Into + Clone, Handler: IntoHandler + Send, Handler::Handler: Send, { + #[zenoh_macros::unstable] fn wait(self) -> ::To { self.into_fetching_subscriber_builder()?.wait() } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl IntoFuture for QueryingSubscriberBuilder<'_, '_, KeySpace, Handler> where KeySpace: Into + Clone, @@ -313,24 +360,32 @@ where type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Resolvable for QueryingSubscriberBuilder<'_, '_, KeySpace, Callback, true> { type To = ZResult<()>; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl Wait for QueryingSubscriberBuilder<'_, '_, KeySpace, Callback, true> where KeySpace: Into + Clone, { + #[zenoh_macros::unstable] fn wait(self) -> ::To { self.into_fetching_subscriber_builder()?.wait() } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl IntoFuture for QueryingSubscriberBuilder<'_, '_, KeySpace, Callback, true> where KeySpace: Into + Clone, @@ -338,6 +393,7 @@ where type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } @@ -406,6 +462,8 @@ struct InnerState { /// The builder of [`FetchingSubscriber`], allowing to configure it. #[zenoh_macros::unstable] #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] pub struct FetchingSubscriberBuilder< 'a, 'b, @@ -426,6 +484,8 @@ pub struct FetchingSubscriberBuilder< pub(crate) phantom: std::marker::PhantomData, } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< 'a, KeySpace, @@ -437,6 +497,7 @@ impl< where TryIntoSample: ExtractSample, { + #[zenoh_macros::unstable] fn with_static_keys( self, ) -> FetchingSubscriberBuilder<'a, 'static, KeySpace, Handler, Fetch, TryIntoSample> { @@ -452,6 +513,9 @@ where } } +#[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] impl< 'a, 'b, @@ -463,7 +527,9 @@ where TryIntoSample: ExtractSample, { /// Add callback to [`FetchingSubscriber`]. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn callback( self, callback: F, @@ -482,7 +548,9 @@ where /// /// Subscriber will not be undeclared when dropped, with the callback running /// in background until the session is closed. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn callback_mut( self, callback: F, @@ -494,7 +562,9 @@ where } /// Use the given handler to receive Samples. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn with( self, handler: Handler, @@ -523,6 +593,9 @@ where } } +#[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] impl< 'a, 'b, @@ -536,6 +609,8 @@ where /// Register the subscriber callback to be run in background until the session is closed. /// /// Background builder doesn't return a `FetchingSubscriber` object anymore. + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn background( self, ) -> FetchingSubscriberBuilder<'a, 'b, KeySpace, Callback, Fetch, TryIntoSample, true> @@ -552,6 +627,9 @@ where } } +#[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] impl< Handler, Fetch: FnOnce(Box) -> ZResult<()>, @@ -565,12 +643,15 @@ where /// to the ones that have the given [`Locality`](Locality). #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn allowed_origin(mut self, origin: Locality) -> Self { self.origin = origin; self } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< KeySpace, Handler, @@ -585,6 +666,8 @@ where type To = ZResult>; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< KeySpace, Handler, @@ -597,11 +680,14 @@ where Handler::Handler: Send, TryIntoSample: ExtractSample + Send + Sync, { + #[zenoh_macros::unstable] fn wait(self) -> ::To { FetchingSubscriber::new(self.with_static_keys()) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< KeySpace, Handler, @@ -617,11 +703,14 @@ where type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< KeySpace, Fetch: FnOnce(Box) -> ZResult<()>, @@ -634,6 +723,8 @@ where type To = ZResult<()>; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< KeySpace, Fetch: FnOnce(Box) -> ZResult<()> + Send + Sync, @@ -644,6 +735,7 @@ where KeySpace: Into, TryIntoSample: ExtractSample + Send + Sync, { + #[zenoh_macros::unstable] fn wait(self) -> ::To { FetchingSubscriber::new(self.with_static_keys())? .subscriber @@ -652,6 +744,8 @@ where } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl< KeySpace, Fetch: FnOnce(Box) -> ZResult<()> + Send + Sync, @@ -665,6 +759,7 @@ where type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } @@ -702,6 +797,7 @@ where /// # } /// ``` #[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub struct FetchingSubscriber { subscriber: Subscriber<()>, callback: Callback, @@ -709,19 +805,28 @@ pub struct FetchingSubscriber { handler: Handler, } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl std::ops::Deref for FetchingSubscriber { type Target = Handler; + #[zenoh_macros::unstable] fn deref(&self) -> &Self::Target { &self.handler } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl std::ops::DerefMut for FetchingSubscriber { + #[zenoh_macros::unstable] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.handler } } +#[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] impl FetchingSubscriber { fn new< 'a, @@ -805,18 +910,24 @@ impl FetchingSubscriber { } /// Undeclare this [`FetchingSubscriber`]`. + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn undeclare(self) -> impl Resolve> { self.subscriber.undeclare() } + #[zenoh_macros::unstable] #[zenoh_macros::internal] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn set_background(&mut self, background: bool) { self.subscriber.set_background(background) } /// Return the key expression of this FetchingSubscriber + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn key_expr(&self) -> &KeyExpr<'static> { self.subscriber.key_expr() } @@ -857,7 +968,9 @@ impl FetchingSubscriber { /// .unwrap(); /// # } /// ``` + #[zenoh_macros::unstable] #[inline] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] pub fn fetch< Fetch: FnOnce(Box) -> ZResult<()> + Send + Sync, TryIntoSample, @@ -936,6 +1049,8 @@ impl Drop for RepliesHandler { /// ``` #[zenoh_macros::unstable] #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] pub struct FetchBuilder< Fetch: FnOnce(Box) -> ZResult<()>, TryIntoSample, @@ -948,6 +1063,8 @@ pub struct FetchBuilder< callback: Callback, } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl) -> ZResult<()>, TryIntoSample> Resolvable for FetchBuilder where @@ -956,17 +1073,22 @@ where type To = ZResult<()>; } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl) -> ZResult<()>, TryIntoSample> Wait for FetchBuilder where TryIntoSample: ExtractSample, { + #[zenoh_macros::unstable] fn wait(self) -> ::To { let handler = register_handler(self.state, self.callback); run_fetch(self.fetch, handler) } } +#[zenoh_macros::unstable] +#[allow(deprecated)] impl) -> ZResult<()>, TryIntoSample> IntoFuture for FetchBuilder where @@ -975,6 +1097,7 @@ where type Output = ::To; type IntoFuture = Ready<::To>; + #[zenoh_macros::unstable] fn into_future(self) -> Self::IntoFuture { std::future::ready(self.wait()) } @@ -986,6 +1109,8 @@ fn register_handler(state: Arc>, callback: Callback) - RepliesHandler { state, callback } } +#[zenoh_macros::unstable] +#[allow(deprecated)] fn run_fetch< Fetch: FnOnce(Box) -> ZResult<()>, TryIntoSample, @@ -1007,11 +1132,16 @@ where })) } +/// [`ExtractSample`]. #[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] pub trait ExtractSample { + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] fn extract(self) -> ZResult; } +#[allow(deprecated)] impl ExtractSample for Reply { fn extract(self) -> ZResult { self.into_result().map_err(|e| zerror!("{:?}", e).into()) diff --git a/zenoh-ext/src/session_ext.rs b/zenoh-ext/src/session_ext.rs index 9d3c430aaf..8daa25e78d 100644 --- a/zenoh-ext/src/session_ext.rs +++ b/zenoh-ext/src/session_ext.rs @@ -14,12 +14,16 @@ use zenoh::{key_expr::KeyExpr, session::Session, Error}; +#[allow(deprecated)] use super::PublicationCacheBuilder; /// Some extensions to the [`zenoh::Session`](zenoh::Session) #[zenoh_macros::unstable] +#[allow(deprecated)] pub trait SessionExt { // REVIEW(fuzzypixelz): this doc test is the only one to use the programmatic configuration API.. + /// Declare a [`PublicationCache`](crate::PublicationCache). + /// /// Examples: /// ``` /// # #[tokio::main] @@ -37,6 +41,8 @@ pub trait SessionExt { /// }).await; /// # } /// ``` + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] + #[zenoh_macros::unstable] fn declare_publication_cache<'a, 'b, 'c, TryIntoKeyExpr>( &'a self, pub_key_expr: TryIntoKeyExpr, @@ -46,7 +52,9 @@ pub trait SessionExt { >>::Error: Into; } +#[allow(deprecated)] impl SessionExt for Session { + #[zenoh_macros::unstable] fn declare_publication_cache<'a, 'b, 'c, TryIntoKeyExpr>( &'a self, pub_key_expr: TryIntoKeyExpr, diff --git a/zenoh-ext/src/subscriber_ext.rs b/zenoh-ext/src/subscriber_ext.rs index 8d931726c3..4441ebabdc 100644 --- a/zenoh-ext/src/subscriber_ext.rs +++ b/zenoh-ext/src/subscriber_ext.rs @@ -23,8 +23,10 @@ use zenoh::{ Result as ZResult, }; +#[allow(deprecated)] use crate::{ - querying_subscriber::QueryingSubscriberBuilder, ExtractSample, FetchingSubscriberBuilder, + advanced_subscriber::HistoryConfig, querying_subscriber::QueryingSubscriberBuilder, + AdvancedSubscriberBuilder, ExtractSample, FetchingSubscriberBuilder, RecoveryConfig, }; /// Allows writing `subscriber.forward(receiver)` instead of `subscriber.stream().map(Ok).forward(publisher)` @@ -37,6 +39,7 @@ impl<'a, S> SubscriberForward<'a, S> for Subscriber> where S: futures::sink::Sink, { + #[zenoh_macros::unstable] type Output = Forward, fn(Sample) -> Result>, S>; fn forward(&'a mut self, sink: S) -> Self::Output { @@ -46,6 +49,8 @@ where /// Some extensions to the [`zenoh::subscriber::SubscriberBuilder`](zenoh::pubsub::SubscriberBuilder) #[zenoh_macros::unstable] +#[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] +#[allow(deprecated)] pub trait SubscriberBuilderExt<'a, 'b, Handler> { type KeySpace; @@ -82,6 +87,8 @@ pub trait SubscriberBuilderExt<'a, 'b, Handler> { /// } /// # } /// ``` + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] fn fetching< Fetch: FnOnce(Box) -> ZResult<()>, TryIntoSample, @@ -120,9 +127,39 @@ pub trait SubscriberBuilderExt<'a, 'b, Handler> { /// } /// # } /// ``` + #[zenoh_macros::unstable] + #[deprecated = "Use `AdvancedPublisher` and `AdvancedSubscriber` instead."] fn querying(self) -> QueryingSubscriberBuilder<'a, 'b, Self::KeySpace, Handler>; } +/// Some extensions to the [`zenoh::subscriber::SubscriberBuilder`](zenoh::pubsub::SubscriberBuilder) +#[zenoh_macros::unstable] +pub trait AdvancedSubscriberBuilderExt<'a, 'b, 'c, Handler> { + /// Enable query for historical data. + /// + /// History can only be retransmitted by [`AdvancedPublishers`](crate::AdvancedPublisher) that enable [`cache`](crate::AdvancedPublisherBuilder::cache). + #[zenoh_macros::unstable] + fn history(self, config: HistoryConfig) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler>; + + /// Ask for retransmission of detected lost Samples. + /// + /// Retransmission can only be achieved by [`AdvancedPublishers`](crate::AdvancedPublisher) + /// that enable [`cache`](crate::AdvancedPublisherBuilder::cache) and + /// [`sample_miss_detection`](crate::AdvancedPublisherBuilder::sample_miss_detection). + #[zenoh_macros::unstable] + fn recovery(self, conf: RecoveryConfig) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler>; + + /// Allow this subscriber to be detected through liveliness. + #[zenoh_macros::unstable] + fn subscriber_detection(self) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler>; + + /// Turn this `Subscriber`into an `AdvancedSubscriber`. + #[zenoh_macros::unstable] + fn advanced(self) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler>; +} + +#[zenoh_macros::unstable] +#[allow(deprecated)] impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> for SubscriberBuilder<'a, 'b, Handler> { type KeySpace = crate::UserSpace; @@ -159,6 +196,7 @@ impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> for SubscriberBuilde /// } /// # } /// ``` + #[zenoh_macros::unstable] fn fetching< Fetch: FnOnce(Box) -> ZResult<()>, TryIntoSample, @@ -208,6 +246,7 @@ impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> for SubscriberBuilde /// } /// # } /// ``` + #[zenoh_macros::unstable] fn querying(self) -> QueryingSubscriberBuilder<'a, 'b, Self::KeySpace, Handler> { QueryingSubscriberBuilder { session: self.session, @@ -227,6 +266,43 @@ impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> for SubscriberBuilde } } +#[zenoh_macros::unstable] +impl<'a, 'b, 'c, Handler> AdvancedSubscriberBuilderExt<'a, 'b, 'c, Handler> + for SubscriberBuilder<'a, 'b, Handler> +{ + /// Enable query for historical data. + /// + /// History can only be retransmitted by [`AdvancedPublishers`](crate::AdvancedPublisher) that enable [`cache`](crate::AdvancedPublisherBuilder::cache). + #[zenoh_macros::unstable] + fn history(self, config: HistoryConfig) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler> { + AdvancedSubscriberBuilder::new(self).history(config) + } + + /// Ask for retransmission of detected lost Samples. + /// + /// Retransmission can only be achieved by [`AdvancedPublishers`](crate::AdvancedPublisher) + /// that enable [`cache`](crate::AdvancedPublisherBuilder::cache) and + /// [`sample_miss_detection`](crate::AdvancedPublisherBuilder::sample_miss_detection). + #[zenoh_macros::unstable] + fn recovery(self, conf: RecoveryConfig) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler> { + AdvancedSubscriberBuilder::new(self).recovery(conf) + } + + /// Allow this subscriber to be detected through liveliness. + #[zenoh_macros::unstable] + fn subscriber_detection(self) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler> { + AdvancedSubscriberBuilder::new(self).subscriber_detection() + } + + /// Turn this `Subscriber`into an `AdvancedSubscriber`. + #[zenoh_macros::unstable] + fn advanced(self) -> AdvancedSubscriberBuilder<'a, 'b, 'c, Handler> { + AdvancedSubscriberBuilder::new(self) + } +} + +#[zenoh_macros::unstable] +#[allow(deprecated)] impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> for LivelinessSubscriberBuilder<'a, 'b, Handler> { @@ -268,6 +344,7 @@ impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> /// } /// # } /// ``` + #[zenoh_macros::unstable] fn fetching< Fetch: FnOnce(Box) -> ZResult<()>, TryIntoSample, @@ -319,6 +396,7 @@ impl<'a, 'b, Handler> SubscriberBuilderExt<'a, 'b, Handler> /// } /// # } /// ``` + #[zenoh_macros::unstable] fn querying(self) -> QueryingSubscriberBuilder<'a, 'b, Self::KeySpace, Handler> { QueryingSubscriberBuilder { session: self.session, diff --git a/zenoh-ext/tests/advanced.rs b/zenoh-ext/tests/advanced.rs new file mode 100644 index 0000000000..f7d3593c63 --- /dev/null +++ b/zenoh-ext/tests/advanced.rs @@ -0,0 +1,715 @@ +// +// 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 zenoh::sample::SampleKind; +use zenoh_config::{EndPoint, ModeDependentValue, WhatAmI}; +use zenoh_ext::{ + AdvancedPublisherBuilderExt, AdvancedSubscriberBuilderExt, CacheConfig, HistoryConfig, + RecoveryConfig, +}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_advanced_history() { + use std::time::Duration; + + use zenoh::internal::ztimeout; + + const TIMEOUT: Duration = Duration::from_secs(60); + const SLEEP: Duration = Duration::from_secs(1); + const PEER1_ENDPOINT: &str = "tcp/localhost:47450"; + + const ADVANCED_HISTORY_KEYEXPR: &str = "test/advanced/history"; + + zenoh_util::init_log_from_env_or("error"); + + let peer1 = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![PEER1_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + c.timestamping + .set_enabled(Some(ModeDependentValue::Unique(true))) + .unwrap(); + let _ = c.set_mode(Some(WhatAmI::Peer)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Peer (1) ZID: {}", s.zid()); + s + }; + + let publ = ztimeout!(peer1 + .declare_publisher(ADVANCED_HISTORY_KEYEXPR) + .cache(CacheConfig::default().max_samples(3))) + .unwrap(); + ztimeout!(publ.put("1")).unwrap(); + ztimeout!(publ.put("2")).unwrap(); + ztimeout!(publ.put("3")).unwrap(); + ztimeout!(publ.put("4")).unwrap(); + + tokio::time::sleep(SLEEP).await; + + let peer2 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![PEER1_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Peer)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Peer (2) ZID: {}", s.zid()); + s + }; + + let sub = ztimeout!(peer2 + .declare_subscriber(ADVANCED_HISTORY_KEYEXPR) + .history(HistoryConfig::default())) + .unwrap(); + tokio::time::sleep(SLEEP).await; + + ztimeout!(publ.put("5")).unwrap(); + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "2"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "3"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "4"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "5"); + + assert!(sub.try_recv().unwrap().is_none()); + + publ.undeclare().await.unwrap(); + // sub.undeclare().await.unwrap(); + + peer1.close().await.unwrap(); + peer2.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_advanced_retransmission() { + use std::time::Duration; + + use zenoh::internal::ztimeout; + + const TIMEOUT: Duration = Duration::from_secs(60); + const SLEEP: Duration = Duration::from_secs(1); + const RECONNECT_SLEEP: Duration = Duration::from_secs(5); + const ROUTER_ENDPOINT: &str = "tcp/localhost:47451"; + + const ADVANCED_RETRANSMISSION_KEYEXPR: &str = "test/advanced/retransmission"; + + zenoh_util::init_log_from_env_or("error"); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + + let client1 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (1) ZID: {}", s.zid()); + s + }; + + let client2 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (2) ZID: {}", s.zid()); + s + }; + + let sub = ztimeout!(client2 + .declare_subscriber(ADVANCED_RETRANSMISSION_KEYEXPR) + .recovery(RecoveryConfig::default())) + .unwrap(); + tokio::time::sleep(SLEEP).await; + + let publ = ztimeout!(client1 + .declare_publisher(ADVANCED_RETRANSMISSION_KEYEXPR) + .cache(CacheConfig::default().max_samples(10)) + .sample_miss_detection()) + .unwrap(); + ztimeout!(publ.put("1")).unwrap(); + + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "1"); + + assert!(sub.try_recv().unwrap().is_none()); + + router.close().await.unwrap(); + tokio::time::sleep(SLEEP).await; + + ztimeout!(publ.put("2")).unwrap(); + ztimeout!(publ.put("3")).unwrap(); + ztimeout!(publ.put("4")).unwrap(); + tokio::time::sleep(SLEEP).await; + + assert!(sub.try_recv().unwrap().is_none()); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + tokio::time::sleep(RECONNECT_SLEEP).await; + + ztimeout!(publ.put("5")).unwrap(); + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "2"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "3"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "4"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "5"); + + assert!(sub.try_recv().unwrap().is_none()); + + publ.undeclare().await.unwrap(); + // sub.undeclare().await.unwrap(); + + client1.close().await.unwrap(); + client2.close().await.unwrap(); + + router.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_advanced_retransmission_periodic() { + use std::time::Duration; + + use zenoh::internal::ztimeout; + + const TIMEOUT: Duration = Duration::from_secs(60); + const SLEEP: Duration = Duration::from_secs(1); + const RECONNECT_SLEEP: Duration = Duration::from_secs(8); + const ROUTER_ENDPOINT: &str = "tcp/localhost:47452"; + + const ADVANCED_RETRANSMISSION_PERIODIC_KEYEXPR: &str = "test/advanced/retransmission/periodic"; + + zenoh_util::init_log_from_env_or("error"); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + + let client1 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (1) ZID: {}", s.zid()); + s + }; + + let client2 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (2) ZID: {}", s.zid()); + s + }; + + let sub = ztimeout!(client2 + .declare_subscriber(ADVANCED_RETRANSMISSION_PERIODIC_KEYEXPR) + .recovery(RecoveryConfig::default().periodic_queries(Some(Duration::from_secs(1))))) + .unwrap(); + tokio::time::sleep(SLEEP).await; + + let publ = ztimeout!(client1 + .declare_publisher(ADVANCED_RETRANSMISSION_PERIODIC_KEYEXPR) + .cache(CacheConfig::default().max_samples(10)) + .sample_miss_detection()) + .unwrap(); + ztimeout!(publ.put("1")).unwrap(); + + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "1"); + + assert!(sub.try_recv().unwrap().is_none()); + + router.close().await.unwrap(); + tokio::time::sleep(SLEEP).await; + + ztimeout!(publ.put("2")).unwrap(); + ztimeout!(publ.put("3")).unwrap(); + ztimeout!(publ.put("4")).unwrap(); + tokio::time::sleep(SLEEP).await; + + assert!(sub.try_recv().unwrap().is_none()); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + tokio::time::sleep(RECONNECT_SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "2"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "3"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "4"); + + assert!(sub.try_recv().unwrap().is_none()); + + publ.undeclare().await.unwrap(); + // sub.undeclare().await.unwrap(); + + client1.close().await.unwrap(); + client2.close().await.unwrap(); + + router.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_advanced_sample_miss() { + use std::time::Duration; + + use zenoh::internal::ztimeout; + + const TIMEOUT: Duration = Duration::from_secs(60); + const SLEEP: Duration = Duration::from_secs(1); + const RECONNECT_SLEEP: Duration = Duration::from_secs(5); + const ROUTER_ENDPOINT: &str = "tcp/localhost:47453"; + + const ADVANCED_SAMPLE_MISS_KEYEXPR: &str = "test/advanced/sample_miss"; + + zenoh_util::init_log_from_env_or("error"); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + + let client1 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (1) ZID: {}", s.zid()); + s + }; + + let client2 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (2) ZID: {}", s.zid()); + s + }; + + let sub = ztimeout!(client2 + .declare_subscriber(ADVANCED_SAMPLE_MISS_KEYEXPR) + .advanced()) + .unwrap(); + let miss_listener = ztimeout!(sub.sample_miss_listener()).unwrap(); + tokio::time::sleep(SLEEP).await; + + let publ = ztimeout!(client1 + .declare_publisher(ADVANCED_SAMPLE_MISS_KEYEXPR) + .sample_miss_detection()) + .unwrap(); + ztimeout!(publ.put("1")).unwrap(); + + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "1"); + + assert!(sub.try_recv().unwrap().is_none()); + + router.close().await.unwrap(); + tokio::time::sleep(SLEEP).await; + + ztimeout!(publ.put("2")).unwrap(); + tokio::time::sleep(SLEEP).await; + + assert!(sub.try_recv().unwrap().is_none()); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + tokio::time::sleep(RECONNECT_SLEEP).await; + + ztimeout!(publ.put("3")).unwrap(); + tokio::time::sleep(SLEEP).await; + + let miss = ztimeout!(miss_listener.recv_async()).unwrap(); + assert_eq!(miss.source(), publ.id()); + assert_eq!(miss.nb(), 1); + + assert!(miss_listener.try_recv().unwrap().is_none()); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "3"); + + assert!(sub.try_recv().unwrap().is_none()); + + publ.undeclare().await.unwrap(); + // sub.undeclare().await.unwrap(); + + client1.close().await.unwrap(); + client2.close().await.unwrap(); + + router.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_advanced_retransmission_sample_miss() { + use std::time::Duration; + + use zenoh::internal::ztimeout; + + const TIMEOUT: Duration = Duration::from_secs(60); + const SLEEP: Duration = Duration::from_secs(1); + const RECONNECT_SLEEP: Duration = Duration::from_secs(5); + const ROUTER_ENDPOINT: &str = "tcp/localhost:47454"; + + const ADVANCED_RETRANSMISSION_SAMPLE_MISS_KEYEXPR: &str = + "test/advanced/retransmission/sample_miss"; + + zenoh_util::init_log_from_env_or("error"); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + + let client1 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (1) ZID: {}", s.zid()); + s + }; + + let client2 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Client)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Client (2) ZID: {}", s.zid()); + s + }; + + let sub = ztimeout!(client2 + .declare_subscriber(ADVANCED_RETRANSMISSION_SAMPLE_MISS_KEYEXPR) + .recovery(RecoveryConfig::default().periodic_queries(Some(Duration::from_secs(1))))) + .unwrap(); + let miss_listener = ztimeout!(sub.sample_miss_listener()).unwrap(); + tokio::time::sleep(SLEEP).await; + + let publ = ztimeout!(client1 + .declare_publisher(ADVANCED_RETRANSMISSION_SAMPLE_MISS_KEYEXPR) + .cache(CacheConfig::default().max_samples(1)) + .sample_miss_detection()) + .unwrap(); + ztimeout!(publ.put("1")).unwrap(); + + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "1"); + + assert!(sub.try_recv().unwrap().is_none()); + + router.close().await.unwrap(); + tokio::time::sleep(SLEEP).await; + + ztimeout!(publ.put("2")).unwrap(); + ztimeout!(publ.put("3")).unwrap(); + ztimeout!(publ.put("4")).unwrap(); + tokio::time::sleep(SLEEP).await; + + assert!(sub.try_recv().unwrap().is_none()); + + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + tokio::time::sleep(RECONNECT_SLEEP).await; + + ztimeout!(publ.put("5")).unwrap(); + tokio::time::sleep(SLEEP).await; + + let miss = ztimeout!(miss_listener.recv_async()).unwrap(); + assert_eq!(miss.source(), publ.id()); + assert_eq!(miss.nb(), 2); + + assert!(miss_listener.try_recv().unwrap().is_none()); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "4"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "5"); + + assert!(sub.try_recv().unwrap().is_none()); + + publ.undeclare().await.unwrap(); + // sub.undeclare().await.unwrap(); + + client1.close().await.unwrap(); + client2.close().await.unwrap(); + + router.close().await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_advanced_late_joiner() { + use std::time::Duration; + + use zenoh::internal::ztimeout; + + const TIMEOUT: Duration = Duration::from_secs(60); + const SLEEP: Duration = Duration::from_secs(1); + const RECONNECT_SLEEP: Duration = Duration::from_secs(8); + const ROUTER_ENDPOINT: &str = "tcp/localhost:47455"; + + const ADVANCED_LATE_JOINER_KEYEXPR: &str = "test/advanced/late_joiner"; + + zenoh_util::init_log_from_env_or("error"); + + let peer1 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + c.timestamping + .set_enabled(Some(ModeDependentValue::Unique(true))) + .unwrap(); + let _ = c.set_mode(Some(WhatAmI::Peer)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Peer (1) ZID: {}", s.zid()); + s + }; + + let peer2 = { + let mut c = zenoh::Config::default(); + c.connect + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Peer)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Peer (2) ZID: {}", s.zid()); + s + }; + + let sub = ztimeout!(peer2 + .declare_subscriber(ADVANCED_LATE_JOINER_KEYEXPR) + .history(HistoryConfig::default().detect_late_publishers())) + .unwrap(); + tokio::time::sleep(SLEEP).await; + + let publ = ztimeout!(peer1 + .declare_publisher(ADVANCED_LATE_JOINER_KEYEXPR) + .cache(CacheConfig::default().max_samples(10)) + .publisher_detection()) + .unwrap(); + ztimeout!(publ.put("1")).unwrap(); + ztimeout!(publ.put("2")).unwrap(); + ztimeout!(publ.put("3")).unwrap(); + + tokio::time::sleep(SLEEP).await; + + assert!(sub.try_recv().unwrap().is_none()); + let router = { + let mut c = zenoh::Config::default(); + c.listen + .endpoints + .set(vec![ROUTER_ENDPOINT.parse::().unwrap()]) + .unwrap(); + c.scouting.multicast.set_enabled(Some(false)).unwrap(); + let _ = c.set_mode(Some(WhatAmI::Router)); + let s = ztimeout!(zenoh::open(c)).unwrap(); + tracing::info!("Router ZID: {}", s.zid()); + s + }; + tokio::time::sleep(RECONNECT_SLEEP).await; + + ztimeout!(publ.put("4")).unwrap(); + tokio::time::sleep(SLEEP).await; + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "1"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "2"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "3"); + + let sample = ztimeout!(sub.recv_async()).unwrap(); + assert_eq!(sample.kind(), SampleKind::Put); + assert_eq!(sample.payload().try_to_string().unwrap().as_ref(), "4"); + + assert!(sub.try_recv().unwrap().is_none()); + + publ.undeclare().await.unwrap(); + // sub.undeclare().await.unwrap(); + + peer1.close().await.unwrap(); + peer2.close().await.unwrap(); + + router.close().await.unwrap(); +} diff --git a/zenoh-ext/tests/liveliness.rs b/zenoh-ext/tests/liveliness.rs index aebbc52b5d..cc47cdca74 100644 --- a/zenoh-ext/tests/liveliness.rs +++ b/zenoh-ext/tests/liveliness.rs @@ -19,6 +19,7 @@ use zenoh::{ }; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[allow(deprecated)] async fn test_liveliness_querying_subscriber_clique() { use std::time::Duration; @@ -97,6 +98,7 @@ async fn test_liveliness_querying_subscriber_clique() { } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[allow(deprecated)] async fn test_liveliness_querying_subscriber_brokered() { use std::time::Duration; @@ -203,6 +205,7 @@ async fn test_liveliness_querying_subscriber_brokered() { } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[allow(deprecated)] async fn test_liveliness_fetching_subscriber_clique() { use std::time::Duration; @@ -285,6 +288,7 @@ async fn test_liveliness_fetching_subscriber_clique() { } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[allow(deprecated)] async fn test_liveliness_fetching_subscriber_brokered() { use std::time::Duration; diff --git a/zenoh/Cargo.toml b/zenoh/Cargo.toml index ece72ab5c4..4f50deefd2 100644 --- a/zenoh/Cargo.toml +++ b/zenoh/Cargo.toml @@ -84,6 +84,7 @@ paste = { workspace = true } petgraph = { workspace = true } phf = { workspace = true } rand = { workspace = true, features = ["default"] } +ref-cast = { workspace = true } serde = { workspace = true, features = ["default"] } serde_json = { workspace = true } socket2 = { workspace = true } diff --git a/zenoh/src/api/admin.rs b/zenoh/src/api/admin.rs index d043ec856c..28808ee664 100644 --- a/zenoh/src/api/admin.rs +++ b/zenoh/src/api/admin.rs @@ -19,6 +19,7 @@ use std::{ use zenoh_core::{Result as ZResult, Wait}; use zenoh_keyexpr::keyexpr; +use zenoh_macros::ke; #[cfg(feature = "unstable")] use zenoh_protocol::core::Reliability; use zenoh_protocol::{core::WireExpr, network::NetworkMessage}; @@ -26,6 +27,7 @@ use zenoh_transport::{ TransportEventHandler, TransportMulticastEventHandler, TransportPeer, TransportPeerEventHandler, }; +use crate as zenoh; use crate::{ api::{ encoding::Encoding, @@ -38,39 +40,71 @@ use crate::{ handlers::Callback, }; -lazy_static::lazy_static!( - static ref KE_STARSTAR: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("**") }; - static ref KE_PREFIX: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("@") }; - static ref KE_SESSION: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("session") }; - static ref KE_TRANSPORT_UNICAST: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("transport/unicast") }; - static ref KE_LINK: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("link") }; -); +#[cfg(feature = "internal")] +pub static KE_AT: &keyexpr = ke!("@"); +#[cfg(not(feature = "internal"))] +static KE_AT: &keyexpr = ke!("@"); +#[cfg(feature = "internal")] +pub static KE_ADV_PREFIX: &keyexpr = ke!("@adv"); +#[cfg(not(feature = "internal"))] +static KE_ADV_PREFIX: &keyexpr = ke!("@adv"); +#[cfg(feature = "internal")] +pub static KE_PUB: &keyexpr = ke!("pub"); +#[cfg(not(feature = "internal"))] +static KE_PUB: &keyexpr = ke!("pub"); +#[cfg(feature = "internal")] +pub static KE_SUB: &keyexpr = ke!("sub"); +#[cfg(feature = "internal")] +pub static KE_EMPTY: &keyexpr = ke!("_"); +#[cfg(not(feature = "internal"))] +static KE_EMPTY: &keyexpr = ke!("_"); +#[cfg(feature = "internal")] +pub static KE_STAR: &keyexpr = ke!("*"); +#[cfg(feature = "internal")] +pub static KE_STARSTAR: &keyexpr = ke!("**"); +#[cfg(not(feature = "internal"))] +static KE_STARSTAR: &keyexpr = ke!("**"); + +static KE_SESSION: &keyexpr = ke!("session"); +static KE_TRANSPORT_UNICAST: &keyexpr = ke!("transport/unicast"); +static KE_LINK: &keyexpr = ke!("link"); 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); - let _admin_qabl = session.declare_queryable_inner( - &admin_key, + &KeyExpr::from(KE_AT / own_zid / KE_SESSION / KE_STARSTAR), + true, + Locality::SessionLocal, + Callback::new(Arc::new({ + let session = session.clone(); + move |q| on_admin_query(&session, KE_AT, q) + })), + ); + + let adv_prefix = KE_ADV_PREFIX / KE_PUB / own_zid / KE_EMPTY / KE_EMPTY / KE_AT / KE_AT; + + let _admin_adv_qabl = session.declare_queryable_inner( + &KeyExpr::from(&adv_prefix / own_zid / KE_SESSION / KE_STARSTAR), true, Locality::SessionLocal, Callback::new(Arc::new({ let session = session.clone(); - move |q| on_admin_query(&session, q) + move |q| on_admin_query(&session, &adv_prefix, q) })), ); } } -pub(crate) fn on_admin_query(session: &WeakSession, query: Query) { - fn reply_peer(own_zid: &keyexpr, query: &Query, peer: TransportPeer) { +pub(crate) fn on_admin_query(session: &WeakSession, prefix: &keyexpr, query: Query) { + fn reply_peer(prefix: &keyexpr, own_zid: &keyexpr, query: &Query, peer: TransportPeer) { let zid = peer.zid.to_string(); if let Ok(zid) = keyexpr::new(&zid) { - let key_expr = *KE_PREFIX / own_zid / *KE_SESSION / *KE_TRANSPORT_UNICAST / zid; + let key_expr = prefix / own_zid / KE_SESSION / KE_TRANSPORT_UNICAST / zid; if query.key_expr().intersects(&key_expr) { match serde_json::to_vec(&peer) { Ok(bytes) => { - let _ = query.reply(key_expr, bytes).wait(); + let reply_expr = KE_AT / own_zid / KE_SESSION / KE_TRANSPORT_UNICAST / zid; + let _ = query.reply(reply_expr, bytes).wait(); } Err(e) => tracing::debug!("Admin query error: {}", e), } @@ -80,17 +114,19 @@ pub(crate) fn on_admin_query(session: &WeakSession, query: Query) { let mut s = DefaultHasher::new(); link.hash(&mut s); if let Ok(lid) = keyexpr::new(&s.finish().to_string()) { - let key_expr = *KE_PREFIX - / own_zid - / *KE_SESSION - / *KE_TRANSPORT_UNICAST - / zid - / *KE_LINK - / lid; + let key_expr = + prefix / own_zid / KE_SESSION / KE_TRANSPORT_UNICAST / zid / KE_LINK / lid; if query.key_expr().intersects(&key_expr) { match serde_json::to_vec(&link) { Ok(bytes) => { - let _ = query.reply(key_expr, bytes).wait(); + let reply_expr = KE_AT + / own_zid + / KE_SESSION + / KE_TRANSPORT_UNICAST + / zid + / KE_LINK + / lid; + let _ = query.reply(reply_expr, bytes).wait(); } Err(e) => tracing::debug!("Admin query error: {}", e), } @@ -105,14 +141,14 @@ pub(crate) fn on_admin_query(session: &WeakSession, query: Query) { .block_in_place(session.runtime.manager().get_transports_unicast()) { if let Ok(peer) = transport.get_peer() { - reply_peer(own_zid, &query, peer); + reply_peer(prefix, own_zid, &query, peer); } } for transport in zenoh_runtime::ZRuntime::Net .block_in_place(session.runtime.manager().get_transports_multicast()) { for peer in transport.get_peers().unwrap_or_default() { - reply_peer(own_zid, &query, peer); + reply_peer(prefix, own_zid, &query, peer); } } } @@ -153,10 +189,9 @@ impl TransportMulticastEventHandler for Handler { ) -> ZResult> { if let Ok(own_zid) = keyexpr::new(&self.session.zid().to_string()) { if let Ok(zid) = keyexpr::new(&peer.zid.to_string()) { - let expr = WireExpr::from( - &(*KE_PREFIX / own_zid / *KE_SESSION / *KE_TRANSPORT_UNICAST / zid), - ) - .to_owned(); + let expr = + WireExpr::from(&(KE_AT / own_zid / KE_SESSION / KE_TRANSPORT_UNICAST / zid)) + .to_owned(); let info = DataInfo { encoding: Some(Encoding::APPLICATION_JSON), ..Default::default() diff --git a/zenoh/src/api/builders/publisher.rs b/zenoh/src/api/builders/publisher.rs index a2515eb284..689c12a96d 100644 --- a/zenoh/src/api/builders/publisher.rs +++ b/zenoh/src/api/builders/publisher.rs @@ -283,14 +283,41 @@ impl IntoFuture for PublicationBuilder, PublicationBuil #[must_use = "Resolvables do nothing unless you resolve them using `.await` or `zenoh::Wait::wait`"] #[derive(Debug)] pub struct PublisherBuilder<'a, 'b> { + #[cfg(feature = "internal")] + pub session: &'a Session, + #[cfg(not(feature = "internal"))] pub(crate) session: &'a Session, + + #[cfg(feature = "internal")] + pub key_expr: ZResult>, + #[cfg(not(feature = "internal"))] pub(crate) key_expr: ZResult>, + + #[cfg(feature = "internal")] + pub encoding: Encoding, + #[cfg(not(feature = "internal"))] pub(crate) encoding: Encoding, + #[cfg(feature = "internal")] + pub congestion_control: CongestionControl, + #[cfg(not(feature = "internal"))] pub(crate) congestion_control: CongestionControl, + #[cfg(feature = "internal")] + pub priority: Priority, + #[cfg(not(feature = "internal"))] pub(crate) priority: Priority, + #[cfg(feature = "internal")] + pub is_express: bool, + #[cfg(not(feature = "internal"))] pub(crate) is_express: bool, + #[cfg(feature = "internal")] + #[cfg(feature = "unstable")] + pub reliability: Reliability, + #[cfg(not(feature = "internal"))] #[cfg(feature = "unstable")] pub(crate) reliability: Reliability, + #[cfg(feature = "internal")] + pub destination: Locality, + #[cfg(not(feature = "internal"))] pub(crate) destination: Locality, } diff --git a/zenoh/src/api/builders/sample.rs b/zenoh/src/api/builders/sample.rs index ebec624626..f00875d185 100644 --- a/zenoh/src/api/builders/sample.rs +++ b/zenoh/src/api/builders/sample.rs @@ -26,9 +26,12 @@ use crate::api::{ publisher::Priority, sample::{QoS, QoSBuilder, Sample, SampleKind}, }; +#[zenoh_macros::internal] +use crate::pubsub::{ + PublicationBuilder, PublicationBuilderDelete, PublicationBuilderPut, Publisher, +}; #[cfg(feature = "unstable")] use crate::sample::SourceInfo; - pub trait QoSBuilderTrait { /// Change the `congestion_control` to apply when routing the data. fn congestion_control(self, congestion_control: CongestionControl) -> Self; @@ -285,3 +288,49 @@ impl From> for Sample { sample_builder.sample } } + +#[zenoh_macros::internal] +impl From<&PublicationBuilder<&Publisher<'_>, PublicationBuilderPut>> for Sample { + fn from(builder: &PublicationBuilder<&Publisher<'_>, PublicationBuilderPut>) -> Self { + Sample { + key_expr: builder.publisher.key_expr.clone().into_owned(), + payload: builder.kind.payload.clone(), + kind: SampleKind::Put, + encoding: builder.kind.encoding.clone(), + timestamp: builder.timestamp, + qos: QoSBuilder::from(QoS::default()) + .congestion_control(builder.publisher.congestion_control) + .priority(builder.publisher.priority) + .express(builder.publisher.is_express) + .into(), + #[cfg(feature = "unstable")] + reliability: builder.publisher.reliability, + #[cfg(feature = "unstable")] + source_info: builder.source_info.clone(), + attachment: builder.attachment.clone(), + } + } +} + +#[zenoh_macros::internal] +impl From<&PublicationBuilder<&Publisher<'_>, PublicationBuilderDelete>> for Sample { + fn from(builder: &PublicationBuilder<&Publisher<'_>, PublicationBuilderDelete>) -> Self { + Sample { + key_expr: builder.publisher.key_expr.clone().into_owned(), + payload: ZBytes::new(), + kind: SampleKind::Put, + encoding: Encoding::ZENOH_BYTES, + timestamp: builder.timestamp, + qos: QoSBuilder::from(QoS::default()) + .congestion_control(builder.publisher.congestion_control) + .priority(builder.publisher.priority) + .express(builder.publisher.is_express) + .into(), + #[cfg(feature = "unstable")] + reliability: builder.publisher.reliability, + #[cfg(feature = "unstable")] + source_info: builder.source_info.clone(), + attachment: builder.attachment.clone(), + } + } +} diff --git a/zenoh/src/api/publisher.rs b/zenoh/src/api/publisher.rs index 78d44fc794..2b2f21d8b0 100644 --- a/zenoh/src/api/publisher.rs +++ b/zenoh/src/api/publisher.rs @@ -313,6 +313,11 @@ impl<'a> Publisher<'a> { } self.session.undeclare_publisher_inner(self.id) } + + #[zenoh_macros::internal] + pub fn session(&self) -> &crate::Session { + self.session.session() + } } impl<'a> UndeclarableSealed<()> for Publisher<'a> { diff --git a/zenoh/src/api/session.rs b/zenoh/src/api/session.rs index ca18c288db..294427426f 100644 --- a/zenoh/src/api/session.rs +++ b/zenoh/src/api/session.rs @@ -26,6 +26,9 @@ use std::{ }; use async_trait::async_trait; +#[zenoh_macros::internal] +use ref_cast::ref_cast_custom; +use ref_cast::RefCastCustom; use tracing::{error, info, trace, warn}; use uhlc::Timestamp; #[cfg(feature = "internal")] @@ -553,12 +556,18 @@ impl fmt::Debug for SessionInner { /// let session = zenoh::open(zenoh::Config::default()).await.unwrap(); /// session.put("key/expression", "value").await.unwrap(); /// # } +#[derive(RefCastCustom)] +#[repr(transparent)] pub struct Session(pub(crate) Arc); impl Session { pub(crate) fn downgrade(&self) -> WeakSession { WeakSession::new(&self.0) } + + #[cfg(feature = "internal")] + #[ref_cast_custom] + pub(crate) const fn ref_cast(from: &Arc) -> &Self; } impl fmt::Debug for Session { @@ -608,6 +617,11 @@ impl WeakSession { *weak += 1; Self(session.clone()) } + + #[zenoh_macros::internal] + pub(crate) fn session(&self) -> &Session { + Session::ref_cast(&self.0) + } } impl Clone for WeakSession { @@ -2146,7 +2160,7 @@ impl SessionInner { timestamp, encoding: encoding.clone().into(), #[cfg(feature = "unstable")] - ext_sinfo: source_info.into(), + ext_sinfo: source_info.clone().into(), #[cfg(not(feature = "unstable"))] ext_sinfo: None, #[cfg(feature = "shared-memory")] @@ -2158,7 +2172,7 @@ impl SessionInner { SampleKind::Delete => PushBody::Del(Del { timestamp, #[cfg(feature = "unstable")] - ext_sinfo: source_info.into(), + ext_sinfo: source_info.clone().into(), #[cfg(not(feature = "unstable"))] ext_sinfo: None, ext_attachment: attachment.clone().map(|a| a.into()), @@ -2177,7 +2191,13 @@ impl SessionInner { kind, encoding: Some(encoding), timestamp, + #[cfg(feature = "unstable")] + source_id: source_info.source_id, + #[cfg(not(feature = "unstable"))] source_id: None, + #[cfg(feature = "unstable")] + source_sn: source_info.source_sn, + #[cfg(not(feature = "unstable"))] source_sn: None, qos: QoS::from(push::ext::QoSType::new( priority.into(), diff --git a/zenoh/src/api/subscriber.rs b/zenoh/src/api/subscriber.rs index 099a1d174a..094757f8d2 100644 --- a/zenoh/src/api/subscriber.rs +++ b/zenoh/src/api/subscriber.rs @@ -221,6 +221,11 @@ impl Subscriber { pub fn set_background(&mut self, background: bool) { self.inner.undeclare_on_drop = !background; } + + #[zenoh_macros::internal] + pub fn session(&self) -> &crate::Session { + self.inner.session.session() + } } impl Drop for Subscriber { diff --git a/zenoh/src/lib.rs b/zenoh/src/lib.rs index ec39dd29c3..119ee34533 100644 --- a/zenoh/src/lib.rs +++ b/zenoh/src/lib.rs @@ -81,6 +81,21 @@ extern crate zenoh_result; mod api; mod net; +#[cfg(feature = "internal")] +pub use api::admin::KE_ADV_PREFIX; +#[cfg(feature = "internal")] +pub use api::admin::KE_AT; +#[cfg(feature = "internal")] +pub use api::admin::KE_EMPTY; +#[cfg(feature = "internal")] +pub use api::admin::KE_PUB; +#[cfg(feature = "internal")] +pub use api::admin::KE_STAR; +#[cfg(feature = "internal")] +pub use api::admin::KE_STARSTAR; +#[cfg(feature = "internal")] +pub use api::admin::KE_SUB; + lazy_static::lazy_static!( static ref LONG_VERSION: String = format!("{} built with {}", GIT_VERSION, env!("RUSTC_VERSION")); ); @@ -172,7 +187,7 @@ pub mod key_expr { #[zenoh_macros::unstable] pub mod format { pub use zenoh_keyexpr::format::*; - pub use zenoh_macros::{kedefine, keformat, kewrite}; + pub use zenoh_macros::{ke, kedefine, keformat, kewrite}; pub mod macro_support { pub use zenoh_keyexpr::format::macro_support::*; }