use std::sync::{Mutex, MutexGuard};
use crate::validation::Validate;
use crate::zmq_context::shared_context;
use crate::{
CanonicalEncodeError, IpcMessage, ValidationKind, decode_ipc_message_json,
encode_canonical_ipc_message,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum FramingFault {
#[error("message was empty")]
Empty,
#[error("message had multiple ZeroMQ frames; exactly one JSON envelope frame is required")]
Multipart,
}
#[derive(Debug, thiserror::Error)]
pub enum ZmqMessageError {
#[error("invalid transport config: {0}")]
InvalidConfig(&'static str),
#[error("ZeroMQ socket setup failed: {0}")]
Setup(String),
#[error("payload failed validation: {0}")]
Validation(#[from] crate::ValidationError),
#[error("canonical encoding failed: {0}")]
Encode(CanonicalEncodeError),
#[error("structured frame is too large: {actual} bytes (maximum {max} bytes)")]
FrameTooLarge {
actual: usize,
max: usize,
},
#[error("envelope decode failed: {0}")]
Envelope(#[from] crate::EnvelopeError),
#[error("unexpected ZeroMQ framing: {0}")]
UnexpectedFraming(FramingFault),
#[error("no message available (EAGAIN)")]
WouldBlock,
#[error("outbound high-water mark is full (EAGAIN on send)")]
HighWaterMark,
#[error("ZeroMQ socket is closed")]
Closed,
#[error("ZeroMQ transport error: {0}")]
Transport(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZmqTransportConfig {
pub endpoint: String,
pub send_hwm: i32,
pub receive_hwm: i32,
pub linger_ms: i32,
pub reconnect_ivl_ms: i32,
pub reconnect_ivl_max_ms: i32,
}
impl ZmqTransportConfig {
#[must_use]
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
send_hwm: 1000,
receive_hwm: 1000,
linger_ms: 0,
reconnect_ivl_ms: 100,
reconnect_ivl_max_ms: 5_000,
}
}
fn check(&self) -> Result<(), ZmqMessageError> {
self.endpoint_failure()?;
self.limit_failure()?;
self.reconnect_failure()
}
fn endpoint_failure(&self) -> Result<(), ZmqMessageError> {
if self.endpoint.is_empty() {
return Err(invalid_config("endpoint must be non-empty"));
}
if self.endpoint.as_bytes().contains(&0) {
return Err(invalid_config("endpoint must not contain an interior null"));
}
Ok(())
}
fn limit_failure(&self) -> Result<(), ZmqMessageError> {
if self.send_hwm < 0 {
return Err(invalid_config("send_hwm must be >= 0"));
}
if self.receive_hwm < 0 {
return Err(invalid_config("receive_hwm must be >= 0"));
}
if self.linger_ms < -1 {
return Err(invalid_config("linger_ms must be >= -1"));
}
Ok(())
}
fn reconnect_failure(&self) -> Result<(), ZmqMessageError> {
if self.reconnect_ivl_ms <= 0 {
return Err(invalid_config("reconnect_ivl_ms must be > 0"));
}
if self.reconnect_ivl_max_ms <= 0 {
return Err(invalid_config("reconnect_ivl_max_ms must be > 0"));
}
if self.reconnect_ivl_max_ms < self.reconnect_ivl_ms {
return Err(invalid_config(
"reconnect_ivl_max_ms must be >= reconnect_ivl_ms",
));
}
Ok(())
}
}
fn invalid_config(message: &'static str) -> ZmqMessageError {
ZmqMessageError::InvalidConfig(message)
}
pub(super) fn ensure_frame_fits(len: usize) -> Result<(), ZmqMessageError> {
let max = usize::try_from(STRUCTURED_MAX_FRAME_BYTES).unwrap_or(usize::MAX);
if len > max {
return Err(ZmqMessageError::FrameTooLarge { actual: len, max });
}
Ok(())
}
pub const STRUCTURED_MAX_FRAME_BYTES: i64 = 512 * 1024 * 1024;
struct ExclusiveSocket {
socket: zmq::Socket,
}
impl ExclusiveSocket {
fn send(&mut self, bytes: &[u8], flags: i32) -> zmq::Result<()> {
self.socket.send(bytes, flags)
}
fn recv_bytes(&mut self, flags: i32) -> zmq::Result<Vec<u8>> {
self.socket.recv_bytes(flags)
}
fn rcvmore(&mut self) -> zmq::Result<bool> {
self.socket.get_rcvmore()
}
fn option_snapshot(&self) -> zmq::Result<(i32, i32, i32, i32, i32, i64)> {
Ok((
self.socket.get_sndhwm()?,
self.socket.get_rcvhwm()?,
self.socket.get_linger()?,
self.socket.get_reconnect_ivl()?,
self.socket.get_reconnect_ivl_max()?,
self.socket.get_maxmsgsize()?,
))
}
}
enum SocketMode {
Bind,
Connect,
}
fn apply_options(socket: &zmq::Socket, config: &ZmqTransportConfig) -> Result<(), ZmqMessageError> {
socket
.set_sndhwm(config.send_hwm)
.map_err(|err| ZmqMessageError::Setup(format!("sndhwm: {err}")))?;
socket
.set_rcvhwm(config.receive_hwm)
.map_err(|err| ZmqMessageError::Setup(format!("rcvhwm: {err}")))?;
socket
.set_linger(config.linger_ms)
.map_err(|err| ZmqMessageError::Setup(format!("linger: {err}")))?;
socket
.set_reconnect_ivl(config.reconnect_ivl_ms)
.map_err(|err| ZmqMessageError::Setup(format!("reconnect_ivl: {err}")))?;
socket
.set_reconnect_ivl_max(config.reconnect_ivl_max_ms)
.map_err(|err| ZmqMessageError::Setup(format!("reconnect_ivl_max: {err}")))?;
socket
.set_maxmsgsize(STRUCTURED_MAX_FRAME_BYTES)
.map_err(|err| ZmqMessageError::Setup(format!("maxmsgsize: {err}")))?;
Ok(())
}
fn open_socket(
context: &zmq::Context,
kind: zmq::SocketType,
config: &ZmqTransportConfig,
mode: SocketMode,
) -> Result<ExclusiveSocket, ZmqMessageError> {
config.check()?;
let socket = context
.socket(kind)
.map_err(|err| ZmqMessageError::Setup(format!("socket: {err}")))?;
apply_options(&socket, config)?;
attach_socket(&socket, config, mode)?;
Ok(ExclusiveSocket { socket })
}
fn attach_socket(
socket: &zmq::Socket,
config: &ZmqTransportConfig,
mode: SocketMode,
) -> Result<(), ZmqMessageError> {
match mode {
SocketMode::Bind => bind_socket(socket, config),
SocketMode::Connect => connect_socket(socket, config),
}
}
fn bind_socket(socket: &zmq::Socket, config: &ZmqTransportConfig) -> Result<(), ZmqMessageError> {
socket
.bind(&config.endpoint)
.map_err(|err| ZmqMessageError::Setup(format!("bind {}: {err}", config.endpoint)))
}
fn connect_socket(
socket: &zmq::Socket,
config: &ZmqTransportConfig,
) -> Result<(), ZmqMessageError> {
socket
.set_subscribe(b"")
.map_err(|err| ZmqMessageError::Setup(format!("subscribe: {err}")))?;
socket
.connect(&config.endpoint)
.map_err(|err| ZmqMessageError::Setup(format!("connect {}: {err}", config.endpoint)))
}
fn lock_socket(slot: &Mutex<Option<ExclusiveSocket>>) -> MutexGuard<'_, Option<ExclusiveSocket>> {
slot.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn map_send(result: zmq::Result<()>) -> Result<(), ZmqMessageError> {
match result {
Ok(()) => Ok(()),
Err(zmq::Error::EAGAIN) => Err(ZmqMessageError::HighWaterMark),
Err(err) => Err(ZmqMessageError::Transport(err.to_string())),
}
}
fn decode_frame(bytes: &[u8]) -> Result<IpcMessage, ZmqMessageError> {
if bytes.is_empty() {
return Err(ZmqMessageError::UnexpectedFraming(FramingFault::Empty));
}
let message = decode_ipc_message_json(bytes)?;
message.validate()?;
Ok(message)
}
impl std::fmt::Debug for ZmqMessagePublisher {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("ZmqMessagePublisher")
}
}
pub struct ZmqMessagePublisher {
socket: Mutex<Option<ExclusiveSocket>>,
#[allow(dead_code)]
context: zmq::Context,
}
impl ZmqMessagePublisher {
pub fn bind(config: ZmqTransportConfig) -> Result<Self, ZmqMessageError> {
let context = shared_context();
let socket = open_socket(&context, zmq::PUB, &config, SocketMode::Bind)?;
Ok(Self {
socket: Mutex::new(Some(socket)),
context,
})
}
pub fn send(&self, message: &IpcMessage) -> Result<(), ZmqMessageError> {
self.require_open()?;
message.validate()?;
let bytes = encode_canonical_ipc_message(message).map_err(ZmqMessageError::Encode)?;
ensure_frame_fits(bytes.len())?;
self.send_bytes(&bytes)
}
fn require_open(&self) -> Result<(), ZmqMessageError> {
if lock_socket(&self.socket).is_none() {
return Err(ZmqMessageError::Closed);
}
Ok(())
}
fn send_bytes(&self, bytes: &[u8]) -> Result<(), ZmqMessageError> {
let mut guard = lock_socket(&self.socket);
let socket = guard.as_mut().ok_or(ZmqMessageError::Closed)?;
map_send(socket.send(bytes, zmq::DONTWAIT))
}
pub fn close(&self) {
*lock_socket(&self.socket) = None;
}
#[doc(hidden)]
pub fn socket_options_for_tests(
&self,
) -> Result<(i32, i32, i32, i32, i32, i64), ZmqMessageError> {
let guard = lock_socket(&self.socket);
let socket = guard.as_ref().ok_or(ZmqMessageError::Closed)?;
socket
.option_snapshot()
.map_err(|err| ZmqMessageError::Transport(err.to_string()))
}
}
impl Drop for ZmqMessagePublisher {
fn drop(&mut self) {
*lock_socket(&self.socket) = None;
}
}
impl std::fmt::Debug for ZmqMessageSubscriber {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("ZmqMessageSubscriber")
}
}
pub struct ZmqMessageSubscriber {
socket: Mutex<Option<ExclusiveSocket>>,
#[allow(dead_code)]
context: zmq::Context,
}
impl ZmqMessageSubscriber {
pub fn connect(config: ZmqTransportConfig) -> Result<Self, ZmqMessageError> {
let context = shared_context();
let socket = open_socket(&context, zmq::SUB, &config, SocketMode::Connect)?;
Ok(Self {
socket: Mutex::new(Some(socket)),
context,
})
}
pub fn try_recv(&self) -> Result<IpcMessage, ZmqMessageError> {
let bytes = self.recv_frame()?;
decode_frame(&bytes)
}
fn recv_frame(&self) -> Result<Vec<u8>, ZmqMessageError> {
let mut guard = lock_socket(&self.socket);
let read = match guard.as_mut() {
Some(socket) => read_socket_frame(socket),
None => return Err(ZmqMessageError::Closed),
};
finish_read(&mut guard, read)
}
pub fn close(&self) {
*lock_socket(&self.socket) = None;
}
#[doc(hidden)]
pub fn decode_frame_for_tests(bytes: &[u8]) -> Result<IpcMessage, ZmqMessageError> {
decode_frame(bytes)
}
pub(super) fn is_closed(&self) -> bool {
lock_socket(&self.socket).is_none()
}
#[doc(hidden)]
pub fn socket_options_for_tests(
&self,
) -> Result<(i32, i32, i32, i32, i32, i64), ZmqMessageError> {
let guard = lock_socket(&self.socket);
let socket = guard.as_ref().ok_or(ZmqMessageError::Closed)?;
socket
.option_snapshot()
.map_err(|err| ZmqMessageError::Transport(err.to_string()))
}
}
fn read_frame(socket: &mut ExclusiveSocket) -> Result<Vec<u8>, ZmqMessageError> {
match socket.recv_bytes(zmq::DONTWAIT) {
Ok(bytes) => Ok(bytes),
Err(zmq::Error::EAGAIN) => Err(ZmqMessageError::WouldBlock),
Err(err) => Err(ZmqMessageError::Transport(err.to_string())),
}
}
fn more_frames(socket: &mut ExclusiveSocket) -> Result<bool, ZmqMessageError> {
socket
.rcvmore()
.map_err(|err| ZmqMessageError::Transport(err.to_string()))
}
impl Drop for ZmqMessageSubscriber {
fn drop(&mut self) {
*lock_socket(&self.socket) = None;
}
}
enum SocketRead {
Frame(Vec<u8>),
Failed(ZmqMessageError),
Abandoned,
}
fn read_socket_frame(socket: &mut ExclusiveSocket) -> SocketRead {
match read_frame(socket) {
Ok(bytes) => assemble_frame(socket, bytes),
Err(err) => SocketRead::Failed(err),
}
}
fn assemble_frame(socket: &mut ExclusiveSocket, bytes: Vec<u8>) -> SocketRead {
match more_frames(socket) {
Ok(true) => after_multipart(socket),
Ok(false) => SocketRead::Frame(bytes),
Err(err) => SocketRead::Failed(err),
}
}
fn after_multipart(socket: &mut ExclusiveSocket) -> SocketRead {
match drain_multipart(socket) {
Ok(false) => multipart_fault(),
Ok(true) => SocketRead::Abandoned,
Err(err) => SocketRead::Failed(err),
}
}
fn multipart_fault() -> SocketRead {
SocketRead::Failed(ZmqMessageError::UnexpectedFraming(FramingFault::Multipart))
}
fn finish_read(
slot: &mut Option<ExclusiveSocket>,
read: SocketRead,
) -> Result<Vec<u8>, ZmqMessageError> {
match read {
SocketRead::Frame(bytes) => Ok(bytes),
SocketRead::Failed(err) => Err(err),
SocketRead::Abandoned => {
*slot = None;
Err(ZmqMessageError::UnexpectedFraming(FramingFault::Multipart))
}
}
}
pub(super) const MULTIPART_DRAIN_LIMIT: usize = 8;
fn drain_multipart(socket: &mut ExclusiveSocket) -> Result<bool, ZmqMessageError> {
for _ in 0..MULTIPART_DRAIN_LIMIT {
if !next_part(socket)? {
return Ok(false);
}
}
Ok(true)
}
fn next_part(socket: &mut ExclusiveSocket) -> Result<bool, ZmqMessageError> {
match socket.recv_bytes(zmq::DONTWAIT) {
Ok(_) => more_frames(socket),
Err(zmq::Error::EAGAIN) => Ok(false),
Err(err) => Err(ZmqMessageError::Transport(err.to_string())),
}
}
fn is_validation_payload(err: &serde_json::Error) -> bool {
let text = err.to_string();
validation_kinds()
.iter()
.any(|kind| text.contains(marker_prefix(*kind)))
}
fn validation_kinds() -> [ValidationKind; 6] {
[
ValidationKind::NonFinite,
ValidationKind::OutOfRange,
ValidationKind::LengthMismatch,
ValidationKind::LimitExceeded,
ValidationKind::NestedMetadata,
ValidationKind::DuplicateIdentifier,
]
}
fn marker_prefix(kind: ValidationKind) -> &'static str {
match kind {
ValidationKind::NonFinite => "non_finite at `",
ValidationKind::OutOfRange => "out_of_range at `",
ValidationKind::LengthMismatch => "length_mismatch at `",
ValidationKind::LimitExceeded => "limit_exceeded at `",
ValidationKind::NestedMetadata => "nested_metadata at `",
ValidationKind::DuplicateIdentifier => "duplicate_identifier at `",
}
}
pub(super) fn map_transport_error(err: ZmqMessageError) -> crate::BackendError {
match err {
ZmqMessageError::Validation(err) => crate::BackendError::InvalidInput(err.to_string()),
ZmqMessageError::Encode(CanonicalEncodeError::Validation(err)) => {
crate::BackendError::InvalidInput(err.to_string())
}
ZmqMessageError::Envelope(crate::EnvelopeError::Payload(err))
if is_validation_payload(&err) =>
{
crate::BackendError::InvalidInput(err.to_string())
}
other => crate::BackendError::CommunicationError(other.to_string()),
}
}
const _: fn() = assert_socket_send_sync;
fn assert_socket_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<zmq::Socket>();
assert_send::<ExclusiveSocket>();
assert_send::<ZmqMessagePublisher>();
assert_sync::<ZmqMessagePublisher>();
assert_send::<ZmqMessageSubscriber>();
assert_sync::<ZmqMessageSubscriber>();
<zmq::Socket as not_sync::IfSyncThenAmbiguous<_>>::witness();
<ExclusiveSocket as not_sync::IfSyncThenAmbiguous<_>>::witness();
}
mod not_sync {
pub trait IfSyncThenAmbiguous<Marker> {
fn witness() {}
}
pub struct NotSyncToken;
pub struct SyncToken;
impl<T: ?Sized> IfSyncThenAmbiguous<NotSyncToken> for T {}
impl<T: ?Sized + Sync> IfSyncThenAmbiguous<SyncToken> for T {}
}
#[cfg(test)]
#[path = "transport_tests.rs"]
mod coverage;