use crate::base::SocketBase;
use bytes::Bytes;
use compio_io::{AsyncRead, AsyncWrite};
use monocoque_core::endpoint::Endpoint;
use monocoque_core::options::SocketOptions;
use monocoque_core::rt::TcpStream;
use monocoque_core::subscription::{SubscriptionEvent, SubscriptionTrie};
use smallvec::SmallVec;
use std::io;
use tracing::{debug, trace};
use crate::handshake::perform_handshake_with_options;
use crate::session::SocketType;
pub struct XSubSocket<S = TcpStream>
where
S: AsyncRead + AsyncWrite + Unpin,
{
base: SocketBase<S>,
subscriptions: SubscriptionTrie,
}
impl<S> XSubSocket<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
pub async fn new(stream: S) -> io::Result<Self> {
Self::with_options(stream, SocketOptions::default()).await
}
pub async fn with_options(mut stream: S, options: SocketOptions) -> io::Result<Self> {
debug!("[XSUB] Creating new XSUB socket");
debug!("[XSUB] Performing ZMTP handshake...");
let handshake_result = perform_handshake_with_options(
&mut stream,
SocketType::Xsub,
options.routing_id.as_deref(),
Some(options.handshake_timeout),
&options,
)
.await
.map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
debug!(
peer_socket_type = ?handshake_result.peer_socket_type,
"[XSUB] Handshake complete"
);
let mut base = SocketBase::new(stream, SocketType::Xsub, options);
base.curve_cipher = handshake_result.curve_cipher;
Ok(Self {
base,
subscriptions: SubscriptionTrie::new(),
})
}
pub async fn subscribe(&mut self, prefix: impl Into<Bytes>) -> io::Result<()> {
let prefix = prefix.into();
trace!("[XSUB] Subscribing to: {:?}", prefix);
self.send_subscription_event_prefix(0x01, &prefix).await?;
self.subscriptions.subscribe(prefix);
Ok(())
}
pub async fn unsubscribe(&mut self, prefix: impl Into<Bytes>) -> io::Result<()> {
let prefix = prefix.into();
trace!("[XSUB] Unsubscribing from: {:?}", prefix);
self.subscriptions.unsubscribe(&prefix);
if self.base.options.xsub_verbose_unsubs {
self.send_subscription_event_prefix(0x00, &prefix).await?;
}
Ok(())
}
pub async fn send_subscription_event(&mut self, event: SubscriptionEvent) -> io::Result<()> {
let (cmd, prefix) = match &event {
SubscriptionEvent::Subscribe(prefix) => (0x01, prefix.as_ref()),
SubscriptionEvent::Unsubscribe(prefix) => (0x00, prefix.as_ref()),
};
self.send_subscription_event_prefix(cmd, prefix).await
}
async fn send_subscription_event_prefix(&mut self, cmd: u8, prefix: &[u8]) -> io::Result<()> {
use bytes::BytesMut;
use compio_buf::BufResult;
use compio_io::AsyncWriteExt;
trace!(
"[XSUB] Sending subscription event ({} bytes)",
1 + prefix.len()
);
let mut raw = BytesMut::with_capacity(1 + prefix.len());
raw.extend_from_slice(&[cmd]);
raw.extend_from_slice(prefix);
let raw = raw.freeze();
let mut wire = BytesMut::with_capacity(raw.len() + 9);
if let Some(ref mut cipher) = self.base.curve_cipher {
let body = cipher
.encrypt_frame(&raw, false)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
crate::base::append_zmtp_cmd_frame(&mut wire, &body);
} else {
crate::codec::encode_multipart(&[raw], &mut wire);
}
let wire = wire.freeze();
let stream =
self.base.stream.as_mut().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotConnected, "Socket not connected")
})?;
let BufResult(result, _) = stream.write_all(wire).await;
result?;
trace!("[XSUB] Subscription event sent successfully");
Ok(())
}
pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
let mut frames: SmallVec<[Bytes; 4]> = SmallVec::new();
loop {
loop {
match self.base.process_frame()? {
crate::base::FrameResult::NeedMore => break,
crate::base::FrameResult::CommandHandled => {
if !self.base.send_buffer.is_empty() {
self.base.flush_send_buffer().await?;
}
}
crate::base::FrameResult::Data(more, payload) => {
frames.push(payload);
if !more {
trace!("[XSUB] Received {} frames", frames.len());
return Ok(Some(frames.into_vec()));
}
}
}
}
let n = self.base.read_raw().await?;
if n == 0 {
trace!("[XSUB] Connection closed");
return Ok(None);
}
if self.base.check_heartbeat()? {
self.base.flush_send_buffer().await?;
}
}
}
pub fn subscription_count(&self) -> usize {
self.subscriptions.len()
}
pub fn is_subscribed(&self, topic: &[u8]) -> bool {
self.subscriptions.matches(topic)
}
pub fn subscriptions(&self) -> Vec<monocoque_core::subscription::Subscription> {
self.subscriptions.subscriptions()
}
pub const fn socket_type(&self) -> SocketType {
SocketType::Xsub
}
#[inline]
pub fn last_endpoint(&self) -> Option<&Endpoint> {
self.base.last_endpoint()
}
#[inline]
pub fn has_more(&self) -> bool {
self.base.has_more()
}
#[inline]
pub fn events(&self) -> u32 {
self.base.events()
}
}
impl XSubSocket<TcpStream> {
pub async fn connect(addr: &str) -> io::Result<Self> {
Self::connect_with_options(addr, SocketOptions::default()).await
}
pub async fn connect_with_options(addr: &str, options: SocketOptions) -> io::Result<Self> {
let stream = TcpStream::connect(addr).await?;
let peer_addr = stream.peer_addr()?;
crate::utils::configure_tcp_stream(&stream, &options, "XSUB")?;
let mut stream = stream;
let handshake_result = crate::handshake::perform_handshake_with_options(
&mut stream,
crate::session::SocketType::Xsub,
options.routing_id.as_deref(),
Some(options.handshake_timeout),
&options,
)
.await
.map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
debug!(
peer_identity = ?handshake_result.peer_identity,
peer_socket_type = ?handshake_result.peer_socket_type,
"[XSUB] Connected to {} (endpoint stored for reconnection)",
peer_addr
);
let endpoint = monocoque_core::endpoint::Endpoint::Tcp(peer_addr);
let mut base = crate::base::SocketBase::with_endpoint(
stream,
crate::session::SocketType::Xsub,
endpoint,
options,
);
base.curve_cipher = handshake_result.curve_cipher;
Ok(Self {
base,
subscriptions: SubscriptionTrie::new(),
})
}
#[inline]
pub fn is_connected(&self) -> bool {
self.base.is_connected()
}
pub async fn try_reconnect(&mut self) -> io::Result<()> {
self.base
.try_reconnect(crate::session::SocketType::Xsub)
.await?;
let prefixes: Vec<bytes::Bytes> = self
.subscriptions
.subscriptions()
.iter()
.map(|s| s.prefix.clone())
.collect();
for prefix in prefixes {
self.send_subscription_event(
monocoque_core::subscription::SubscriptionEvent::Subscribe(prefix),
)
.await?;
}
Ok(())
}
pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<bytes::Bytes>>> {
let max = self.base.options.max_reconnect_attempts;
let mut attempts = 0u32;
loop {
if self.base.stream.is_none() {
if let Some(limit) = max
&& attempts >= limit
{
return Err(io::Error::new(
io::ErrorKind::NotConnected,
format!("Max {} reconnection attempts exceeded", limit),
));
}
attempts += 1;
trace!(
"[XSUB] Stream disconnected, reconnecting (attempt {})",
attempts
);
self.try_reconnect().await?;
}
match self.recv().await {
Ok(Some(msg)) => return Ok(Some(msg)),
Ok(None) => {
debug!("[XSUB] EOF on recv, will reconnect");
}
Err(e) => {
if self.base.stream.is_none()
|| matches!(
e.kind(),
io::ErrorKind::ConnectionReset
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::BrokenPipe
| io::ErrorKind::UnexpectedEof
)
{
debug!("[XSUB] Connection error on recv ({}), will reconnect", e);
self.base.stream = None;
} else {
return Err(e);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn xsub_connect_sets_tcp_nodelay() {
use monocoque_core::rt::LocalRuntime;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::sync::mpsc;
use std::thread;
fn fd_nodelay(fd: RawFd) -> bool {
let sock = unsafe { socket2::Socket::from_raw_fd(fd) };
let nd = sock.nodelay().expect("query TCP_NODELAY");
std::mem::forget(sock); nd
}
let (port_tx, port_rx) = mpsc::channel::<u16>();
let (nd_tx, nd_rx) = mpsc::channel::<bool>();
let (done_tx, done_rx) = mpsc::channel::<()>();
let server = thread::spawn(move || {
let rt = LocalRuntime::new().unwrap();
rt.block_on(async move {
let mut xpub = crate::xpub::XPubSocket::bind("127.0.0.1:0").await.unwrap();
port_tx.send(xpub.local_addr().unwrap().port()).unwrap();
xpub.accept().await.unwrap();
done_rx.recv().unwrap();
});
});
let port = port_rx.recv().unwrap();
let client = thread::spawn(move || {
let rt = LocalRuntime::new().unwrap();
rt.block_on(async move {
let xsub = XSubSocket::connect(&format!("127.0.0.1:{port}"))
.await
.unwrap();
let fd = xsub.base.stream.as_ref().unwrap().as_raw_fd();
nd_tx.send(fd_nodelay(fd)).unwrap();
done_tx.send(()).unwrap();
});
});
let nodelay = nd_rx.recv().unwrap();
client.join().unwrap();
server.join().unwrap();
assert!(
nodelay,
"XSUB connect must set TCP_NODELAY on the outbound socket",
);
}
#[test]
fn test_subscription_tracking() {
use monocoque_core::rt::LocalRuntime as Runtime;
Runtime::new().unwrap().block_on(async {
});
}
#[test]
fn test_subscription_event_creation() {
let event = SubscriptionEvent::Subscribe(Bytes::from_static(b"topic"));
let msg = event.to_message();
assert_eq!(msg[0], 0x01);
assert_eq!(&msg[1..], b"topic");
}
}
crate::impl_socket_trait!(XSubSocket<S>, SocketType::Xsub);