mod convert;
mod consumer;
mod supplier;
use std::future::Future;
use std::time::Duration;
use std::sync::Arc;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use tracing::debug;
use crate::dev::{AttrInfo, AttrRead, AttrValueEvent, DevSource};
use crate::error::{Error, ErrorKind, Result, reason};
use crate::value::{Scalar, Value};
use crate::backend::BackendDev;
use crate::buf::Buf;
pub use consumer::Consumer;
pub use supplier::Supplier;
use consumer::{decode_conf, decode_ready, decode_value, Decode, SubscribeRequest};
use super::orb::cdr::CdrReader;
use super::orb::endian::Endian;
use super::DevVersion;
const IDL_VERSION: DevVersion = DevVersion::V6;
const HEARTBEAT_SUFFIX: &str = ".heartbeat";
const IDL_COMPAT_PREFIX: &str = "idl5_";
const NO_DATABASE_MARKER: &str = "#dbase=no";
const MULTICAST_SCHEME: &str = "epgm://";
const EVENT_NAMES: [&str; 9] = [
"change",
"periodic",
"archive",
"user_event",
"attr_conf",
"data_ready",
"intr_change",
"pipe",
"alarm",
];
const UNPREFIXED_EVENT_NAMES: [&str; 4] = ["alarm", "data_ready", "intr_change", "pipe"];
const CALL_INFO_VERSION: i32 = 1;
const EVENT_METHOD_NAME: &str = "push_zmq_event";
const PAYLOAD_FILLER: u32 = 0xDEC0DEC0;
const MIN_LIB_VERSION: u32 = 930;
const HEARTBEAT_PERIOD: Duration = Duration::from_secs(10);
const RESUBSCRIBE_PERIOD: Duration = Duration::from_secs(600);
const CONFIRM_PERIOD: Duration = Duration::from_secs(RESUBSCRIBE_PERIOD.as_secs() / 3);
const SUBSCRIBE_PROPAGATION: Duration = Duration::from_millis(20);
const DEFAULT_LINGER: i32 = 0;
const SUB_SEND_HWM: i32 = 10_000;
const SUB_HWM: i32 = 1000;
const PUB_HWM: i32 = 1000;
pub fn subscribe_attr_value<'a>(
consumer: &'a Consumer,
dev_name: BoxFuture<'a, Result<Arc<str>>>,
admin: Arc<dyn BackendDev>,
name: &'a str,
event: AttrValueEvent,
) -> BoxFuture<'a, Result<BoxStream<'static, Result<AttrRead<Value>>>>> {
Box::pin(subscribe(consumer, dev_name, admin, name, EventKind::Value(event), decode_value))
}
pub fn subscribe_attr_ready<'a>(
consumer: &'a Consumer,
dev_name: BoxFuture<'a, Result<Arc<str>>>,
admin: Arc<dyn BackendDev>,
name: &'a str,
) -> BoxFuture<'a, Result<BoxStream<'static, Result<()>>>> {
Box::pin(subscribe(consumer, dev_name, admin, name, EventKind::Ready, decode_ready))
}
pub fn subscribe_attr_conf<'a>(
consumer: &'a Consumer,
dev_name: BoxFuture<'a, Result<Arc<str>>>,
admin: Arc<dyn BackendDev>,
name: &'a str,
) -> BoxFuture<'a, Result<BoxStream<'static, Result<AttrInfo>>>> {
Box::pin(subscribe(consumer, dev_name, admin, name, EventKind::Conf, decode_conf))
}
async fn subscribe<T: Send + 'static>(
consumer: &Consumer,
dev_name: impl Future<Output = Result<Arc<str>>>,
admin: Arc<dyn BackendDev>,
name: &str,
kind: EventKind,
decode: Decode<T>,
) -> Result<BoxStream<'static, Result<T>>> {
let dev_name = dev_name.await?;
let obj_name = Arc::<str>::from(name.to_lowercase());
let idl_version = IDL_VERSION.number().to_string();
let input = Value::from([&*dev_name, &*obj_name, "subscribe", kind.name(), &*idl_version]);
debug!(dev = %dev_name, attr = %obj_name, kind = kind.name(), "Negotiating the event subscription");
let reply = admin.command(Scalar::<str>::from("ZmqEventSubscriptionChange"), input, DevSource::Dev).await;
let info = match reply {
Ok(reply) => SubscriptionInfo::from_value(reply)?,
Err(err) if matches!(err.kind(), ErrorKind::CommandNotFound | ErrorKind::Reason(reason::API_CommandNotFound)) =>
return Err(Error::new(ErrorKind::NotHandled)
.with_append_description("The device server does not publish this event over ZMQ")
.with_parent(err)),
Err(err) => return Err(err),
};
debug!(?info, "Negotiated the event subscription");
let request = SubscribeRequest { admin, dev_name, obj_name, kind, info };
let sub = consumer.subscribe(request, decode)?.await?;
Ok(Box::pin(sub))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventKind {
Value(AttrValueEvent),
Ready,
Conf,
}
impl EventKind {
fn name(self) -> &'static str {
match self {
Self::Value(AttrValueEvent::Change) => "change",
Self::Value(AttrValueEvent::Periodic) => "periodic",
Self::Value(AttrValueEvent::Archive) => "archive",
Self::Value(AttrValueEvent::User) => "user_event",
Self::Value(AttrValueEvent::Alarm) => "alarm",
Self::Ready => "data_ready",
Self::Conf => "attr_conf",
}
}
fn topic_name(self, version: DevVersion) -> Arc<str> {
let compat = !matches!(self, Self::Value(AttrValueEvent::Alarm) | Self::Ready);
if compat && version >= DevVersion::V5 {
Arc::from(format!("{IDL_COMPAT_PREFIX}{}", self.name()))
} else {
Arc::from(self.name())
}
}
}
#[derive(Debug)]
pub struct SubscriptionInfo {
version: DevVersion,
sub_hwm: Option<i32>,
endpoints: Vec<Endpoints>,
event_topic: Arc<str>,
channel_name: Arc<str>,
}
#[derive(Debug, Clone)]
struct Endpoints {
heartbeat: Arc<str>,
event: Arc<str>,
}
impl SubscriptionInfo {
fn from_value(value: Value) -> Result<Self> {
let Value::LongString(numbers, strings) = value else {
return Err(Error::new(ErrorKind::InvalidData)
.with_description("The device server did not answer the event negotiation with a DevVarLongStringArray"));
};
let [lib_version, dev_version, rest @ ..] = numbers.as_slice() else {
return Err(Error::new(ErrorKind::InvalidData)
.with_description("The device server sent neither its library release nor the IDL release of the device"));
};
if *lib_version < MIN_LIB_VERSION as i32 {
return Err(Error::not_implemented()
.with_append_description(format_args!("Events of a device server older than Tango 9.3, got library release {lib_version}")));
}
let version = u32::try_from(*dev_version).ok()
.map(|number| number.min(IDL_VERSION.number()))
.and_then(DevVersion::from_number)
.ok_or_else(|| Error::new(ErrorKind::InvalidData)
.with_description("The IDL release the device server sent is not a known one")
.with_append_description(format_args!("Got {dev_version}")))?;
let sub_hwm = rest.first().copied();
let Some(endpoints_len) = strings.len().checked_sub(2) else {
return Err(Error::new(ErrorKind::InvalidData)
.with_description("The device server sent neither the event topic nor the channel name"));
};
if endpoints_len == 0 || endpoints_len % 2 != 0 {
return Err(Error::new(ErrorKind::InvalidData)
.with_description("The device server sent an odd or empty count of endpoints")
.with_append_description(format_args!("Got {endpoints_len}, expected pairs of a heartbeat and an event one")));
}
let string = |index: usize| strings.get([index]).unwrap();
let endpoints = (0..endpoints_len).step_by(2)
.map(|index| Endpoints {
heartbeat: Arc::from(string(index)),
event: Arc::from(string(index + 1)),
})
.collect();
let event_topic = string(endpoints_len);
let channel_name = string(endpoints_len + 1);
let channel_name = if event_topic.contains(NO_DATABASE_MARKER) && !channel_name.ends_with(NO_DATABASE_MARKER) {
Arc::from(format!("{channel_name}{NO_DATABASE_MARKER}"))
} else {
Arc::from(channel_name)
};
Ok(Self {
version,
sub_hwm,
endpoints,
event_topic: Arc::from(event_topic),
channel_name,
})
}
fn heartbeat_topic(&self) -> Arc<str> {
Arc::from(format!("{}{HEARTBEAT_SUFFIX}", self.channel_name))
}
}
impl Endpoints {
fn is_multicast(&self) -> bool {
self.heartbeat.starts_with(MULTICAST_SCHEME) || self.event.starts_with(MULTICAST_SCHEME)
}
}
#[derive(Debug)]
struct CallInfo {
counter: u32,
is_except: bool,
}
impl CallInfo {
fn read(octets: &[u8], endian: Endian) -> Result<Self> {
let buf = Buf::from(octets);
let mut reader = CdrReader::new(&buf).with_endian(endian);
let _version = reader.read_long()?;
let counter = reader.read_ulong()?;
let _method_name = reader.read_string()?;
let oid_len = reader.read_sequence_len()?;
reader.skip(oid_len)?;
let is_except = reader.read_bool()?;
Ok(Self { counter, is_except })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Spectrum;
fn reply(dev_version: i32, strings: &[&str]) -> Value {
Value::LongString(
Spectrum::from(vec![1000, dev_version, 1000, 0, 0, 40305]),
Spectrum::<str>::from(strings),
)
}
const TOPIC: &str = "tango://db.example.org:10000/sys/tg_test/1/double_scalar.idl5_change";
const CHANNEL: &str = "tango://db.example.org:10000/dserver/tangotest/test";
#[test]
fn subscription_info() {
let info = SubscriptionInfo::from_value(reply(6, &[
"tcp://10.0.0.1:5001", "tcp://10.0.0.1:5002",
"tcp://192.168.0.1:5001", "tcp://192.168.0.1:5002",
TOPIC, CHANNEL,
])).unwrap();
assert_eq!(info.version, DevVersion::V6);
assert_eq!(info.sub_hwm, Some(1000));
assert_eq!(info.endpoints.len(), 2);
assert_eq!(&*info.endpoints[1].heartbeat, "tcp://192.168.0.1:5001");
assert_eq!(&*info.endpoints[1].event, "tcp://192.168.0.1:5002");
assert_eq!(&*info.event_topic, TOPIC);
assert_eq!(&*info.channel_name, CHANNEL);
assert_eq!(&*info.heartbeat_topic(), "tango://db.example.org:10000/dserver/tangotest/test.heartbeat");
}
#[test]
fn subscription_info_no_database() {
let info = SubscriptionInfo::from_value(reply(5, &[
"tcp://10.0.0.1:5001", "tcp://10.0.0.1:5002",
"tango://host:1234/sys/tg_test/1/double_scalar#dbase=no.idl5_change",
"tango://host:1234/dserver/tangotest/test",
])).unwrap();
assert_eq!(&*info.channel_name, "tango://host:1234/dserver/tangotest/test#dbase=no");
}
#[test]
fn subscription_info_negotiated_version() {
let info = SubscriptionInfo::from_value(reply(7, &["tcp://a:1", "tcp://a:2", TOPIC, CHANNEL])).unwrap();
assert_eq!(info.version, IDL_VERSION);
}
#[test]
fn subscription_info_invalid() {
let mut old = reply(4, &["tcp://a:1", "tcp://a:2"]);
if let Value::LongString(numbers, _) = &mut old {
*numbers = Spectrum::from(vec![920, 4]);
}
assert_eq!(SubscriptionInfo::from_value(old).unwrap_err().kind(), ErrorKind::NotImplemented);
let odd = reply(6, &["tcp://a:1", TOPIC, CHANNEL]);
assert_eq!(SubscriptionInfo::from_value(odd).unwrap_err().kind(), ErrorKind::InvalidData);
let empty = reply(6, &[]);
assert_eq!(SubscriptionInfo::from_value(empty).unwrap_err().kind(), ErrorKind::InvalidData);
assert_eq!(SubscriptionInfo::from_value(Value::Void).unwrap_err().kind(), ErrorKind::InvalidData);
}
#[test]
fn topic_name() {
let change = EventKind::Value(AttrValueEvent::Change);
assert_eq!(&*change.topic_name(DevVersion::V4), "change");
assert_eq!(&*change.topic_name(DevVersion::V5), "idl5_change");
assert_eq!(&*EventKind::Conf.topic_name(DevVersion::V6), "idl5_attr_conf");
assert_eq!(&*EventKind::Ready.topic_name(DevVersion::V6), "data_ready");
assert_eq!(&*EventKind::Value(AttrValueEvent::Alarm).topic_name(DevVersion::V6), "alarm");
}
#[test]
fn call_info() {
let mut octets = Vec::new();
octets.extend(1i32.to_be_bytes());
octets.extend(42u32.to_be_bytes());
octets.extend(15u32.to_be_bytes());
octets.extend(b"push_zmq_event\0");
octets.push(0); octets.extend(0u32.to_be_bytes());
octets.push(0);
let call = CallInfo::read(&octets, Endian::Big).unwrap();
assert_eq!(call.counter, 42);
assert!(!call.is_except);
}
}