use std::future::{Future, poll_fn};
use std::num::NonZeroUsize;
use std::panic;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex as StdMutex, PoisonError};
use std::task::{Context, Poll, ready};
use std::time::Duration;
use aliasable::boxed::AliasableBox;
use futures::Stream;
use ruststream::{Subscriber, nonzero};
use selfie::SelfieMut;
use selfie::refs::RefType;
use tokio::runtime::Handle;
use tokio::sync::{OnceCell, mpsc};
use tokio::task::JoinHandle;
use zeromq::prelude::*;
use zeromq::{Socket, ZmqError as WireError, ZmqResult};
use crate::endpoint::{Endpoint, Side};
use crate::error::{ZmqError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::{Bus, Deliveries, Registration};
use crate::message::ZmqMessage;
pub(crate) const SEND_RETRY_WINDOW: Duration = Duration::from_secs(5);
pub(crate) const SEND_RETRY_STEP: Duration = Duration::from_millis(50);
pub(crate) const BATCH_MAX_WAIT: Duration = Duration::from_millis(20);
pub(crate) const DEFAULT_READ_AHEAD: NonZeroUsize = nonzero!(1000_usize);
pub(crate) fn delivery_channel(read_ahead: NonZeroUsize) -> (DeliverySender, DeliveryReceiver) {
let (tx, rx) = mpsc::channel(read_ahead.get());
(tx, DeliveryReceiver::Driver(rx))
}
#[cfg(feature = "testing")]
#[derive(Debug)]
pub(crate) enum Transport {
Sockets,
InProcess(Arc<Bus>),
}
#[derive(Debug)]
pub(crate) struct Lifecycle {
pub(crate) endpoint: Endpoint,
#[cfg(feature = "testing")]
transport: Transport,
listener: Arc<StdMutex<Option<Arc<Listener>>>>,
binding: tokio::sync::Mutex<()>,
dialer: OnceCell<String>,
pub(crate) closed: AtomicBool,
runtime: Handle,
}
#[derive(Debug)]
pub(crate) struct Listener {
pub(crate) address: String,
pub(crate) subscription: String,
}
#[derive(Debug, Clone)]
pub(crate) enum SendTarget<'a> {
Dial(&'a str),
Listen(&'a str),
Local(Arc<Listener>),
}
impl SendTarget<'_> {
pub(crate) fn address(&self) -> &str {
match self {
Self::Dial(address) | Self::Listen(address) => address,
Self::Local(listener) => &listener.address,
}
}
}
impl Lifecycle {
pub(crate) fn new(endpoint: Endpoint) -> Self {
Self {
endpoint,
#[cfg(feature = "testing")]
transport: Transport::Sockets,
listener: Arc::new(StdMutex::new(None)),
binding: tokio::sync::Mutex::new(()),
dialer: OnceCell::new(),
closed: AtomicBool::new(false),
runtime: Handle::current(),
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(endpoint: Endpoint) -> Self {
Self {
transport: Transport::InProcess(Arc::new(Bus::default())),
..Self::new(endpoint)
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process_bus(&self) -> Option<&Arc<Bus>> {
match &self.transport {
Transport::Sockets => None,
Transport::InProcess(bus) => Some(bus),
}
}
pub(crate) fn dialer(&self) -> Option<&str> {
self.dialer.get().map(String::as_str)
}
pub(crate) fn bound_address(&self) -> Option<String> {
self.listener().map(|listener| listener.address.clone())
}
fn listener(&self) -> Option<Arc<Listener>> {
self.listener
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub(crate) async fn on_runtime<Attach, Output>(
&self,
attach: Attach,
) -> Result<Output, ZmqError>
where
Attach: Future<Output = Result<Output, ZmqError>> + Send + 'static,
Output: Send + 'static,
{
match self.runtime.spawn(attach).await {
Ok(outcome) => outcome,
Err(err) if err.is_panic() => panic::resume_unwind(err.into_panic()),
Err(_) => Err(ZmqError::NotConnected),
}
}
pub(crate) fn spawn<Task>(&self, task: Task) -> JoinHandle<()>
where
Task: Future<Output = ()> + Send + 'static,
{
self.runtime.spawn(task)
}
pub(crate) fn ensure_open(&self) -> Result<(), ZmqError> {
if self.closed.load(Ordering::Acquire) {
return Err(ZmqError::NotConnected);
}
Ok(())
}
pub(crate) async fn attach_receiver<S: Socket>(
&self,
socket: &mut S,
subscription: &str,
) -> Result<Option<BoundSlot>, ZmqError> {
match self.endpoint.side() {
Side::Bind => self
.hold_listener(subscription, async || {
socket
.bind(self.endpoint.address())
.await
.map(|resolved| resolved.to_string())
.map_err(|e| ZmqError::Endpoint {
endpoint: self.endpoint.address().to_owned(),
source: box_err(e),
})
})
.await
.map(Some),
Side::Connect => {
socket
.connect(self.endpoint.address())
.await
.map_err(|e| ZmqError::Endpoint {
endpoint: self.endpoint.address().to_owned(),
source: box_err(e),
})?;
let _ = self.dialer.set(subscription.to_owned());
Ok(None)
}
}
}
#[cfg(feature = "testing")]
pub(crate) async fn attach_in_process(
&self,
subscription: &str,
) -> Result<Option<BoundSlot>, ZmqError> {
match self.endpoint.side() {
Side::Bind => self
.hold_listener(subscription, async || {
Ok(self.endpoint.address().to_owned())
})
.await
.map(Some),
Side::Connect => {
let _ = self.dialer.set(subscription.to_owned());
Ok(None)
}
}
}
async fn hold_listener<Bind, Bound>(
&self,
subscription: &str,
bind: Bind,
) -> Result<BoundSlot, ZmqError>
where
Bind: FnOnce() -> Bound,
Bound: Future<Output = Result<String, ZmqError>>,
{
let _binding = self.binding.lock().await;
if let Some(held) = self.listener() {
return Err(endpoint_taken(subscription, &held.subscription));
}
let listener = Arc::new(Listener {
address: bind().await?,
subscription: subscription.to_owned(),
});
*self.listener.lock().unwrap_or_else(PoisonError::into_inner) = Some(Arc::clone(&listener));
Ok(BoundSlot {
slot: Arc::clone(&self.listener),
listener,
})
}
pub(crate) fn send_target(&self) -> SendTarget<'_> {
match self.endpoint.side() {
Side::Connect => SendTarget::Dial(self.endpoint.address()),
Side::Bind => self.listener().map_or_else(
|| SendTarget::Listen(self.endpoint.address()),
SendTarget::Local,
),
}
}
#[cfg(feature = "testing")]
pub(crate) fn local_listener(&self) -> Option<Arc<Listener>> {
match self.send_target() {
SendTarget::Local(listener) => Some(listener),
SendTarget::Dial(_) | SendTarget::Listen(_) => None,
}
}
pub(crate) async fn attach_sender<S: Socket>(
&self,
socket: &mut S,
) -> Result<Option<Arc<Listener>>, ZmqError> {
attach_to(socket, self.send_target()).await
}
}
pub(crate) async fn attach_to<S: Socket>(
socket: &mut S,
target: SendTarget<'_>,
) -> Result<Option<Arc<Listener>>, ZmqError> {
let outcome = match &target {
SendTarget::Listen(address) => socket.bind(address).await.map(|_| ()),
SendTarget::Dial(address) => socket.connect(address).await,
SendTarget::Local(listener) => socket.connect(&listener.address).await,
};
outcome.map_err(|e| ZmqError::Endpoint {
endpoint: target.address().to_owned(),
source: box_err(e),
})?;
Ok(match target {
SendTarget::Local(listener) => Some(listener),
SendTarget::Dial(_) | SendTarget::Listen(_) => None,
})
}
#[derive(Debug)]
pub(crate) struct BoundSlot {
slot: Arc<StdMutex<Option<Arc<Listener>>>>,
listener: Arc<Listener>,
}
impl Drop for BoundSlot {
fn drop(&mut self) {
let mut slot = self.slot.lock().unwrap_or_else(PoisonError::into_inner);
if slot
.as_ref()
.is_some_and(|held| Arc::ptr_eq(held, &self.listener))
{
*slot = None;
}
}
}
pub(crate) fn returns_to_subscription(name: &str, subscription: &str) -> ZmqError {
ZmqError::Send {
name: name.to_owned(),
reason: format!(
"this service's subscription '{subscription}' binds the queue and receives every \
message pushed into it; publish '{name}' through a queue on an endpoint of its own"
),
}
}
pub(crate) fn endpoint_taken(subscription: &str, holder: &str) -> ZmqError {
ZmqError::Invalid(format!(
"subscription '{subscription}' cannot open on the endpoint this broker binds: this \
service's subscription '{holder}' binds it, and a bound endpoint is one socket read by \
the subscription that bound it; mount '{subscription}' on a broker with an endpoint of \
its own"
))
}
pub(crate) fn dials_the_sender(
name: &str,
subscription: &str,
sender: &str,
pattern: &str,
) -> ZmqError {
ZmqError::Send {
name: name.to_owned(),
reason: format!(
"this service's subscription '{subscription}' dials the endpoint, and the {sender} \
socket it reads from there takes no message; publish '{name}' through a {pattern} \
on an endpoint of its own"
),
}
}
pub(crate) async fn send_with_retry<S: SocketSend>(
socket: &mut S,
name: &str,
message: zeromq::ZmqMessage,
) -> Result<(), ZmqError> {
retry(Plain(socket), message)
.await
.map_err(|failure| failure.into_error(name))
}
trait Offer {
fn offer(&mut self, message: zeromq::ZmqMessage) -> impl Future<Output = ZmqResult<()>> + Send;
}
struct Plain<'a, S>(&'a mut S);
impl<S: SocketSend> Offer for Plain<'_, S> {
fn offer(&mut self, message: zeromq::ZmqMessage) -> impl Future<Output = ZmqResult<()>> + Send {
self.0.send(message)
}
}
#[derive(Debug)]
pub(crate) enum SendFailure {
NoPeer,
Wire(WireError),
}
impl SendFailure {
pub(crate) fn into_error(self, name: &str) -> ZmqError {
ZmqError::Send {
name: name.to_owned(),
reason: match self {
Self::NoPeer => "no connected peer".to_owned(),
Self::Wire(err) => err.to_string(),
},
}
}
}
async fn retry<Attempt: Offer>(
mut attempt: Attempt,
message: zeromq::ZmqMessage,
) -> Result<(), SendFailure> {
let mut pending = message;
let mut deadline = None;
loop {
match attempt.offer(pending).await {
Ok(()) => return Ok(()),
Err(WireError::ReturnToSender { message, .. }) => {
match deadline {
None => deadline = Some(tokio::time::Instant::now() + SEND_RETRY_WINDOW),
Some(at) if tokio::time::Instant::now() >= at => {
return Err(SendFailure::NoPeer);
}
Some(_) => {}
}
pending = message;
tokio::time::sleep(SEND_RETRY_STEP).await;
}
Err(err) => return Err(SendFailure::Wire(err)),
}
}
}
struct ClientSend;
impl<'socket> RefType<'socket> for ClientSend {
type Ref = Pin<Box<dyn Future<Output = ZmqResult<()>> + Send + 'socket>>;
}
type Held<S> = Pin<AliasableBox<S>>;
type InFlightSend<S> = SelfieMut<'static, AliasableBox<S>, ClientSend>;
pub(crate) struct Outbox<S: 'static> {
socket: Option<Held<S>>,
send: Option<InFlightSend<S>>,
dropped: Option<String>,
}
impl<S> Outbox<S>
where
S: SocketSend + Send + Unpin + 'static,
{
pub(crate) fn new(socket: S) -> Self {
Self {
socket: Some(AliasableBox::from_unique_pin(Box::pin(socket))),
send: None,
dropped: None,
}
}
pub(crate) async fn send(
&mut self,
name: &str,
message: zeromq::ZmqMessage,
) -> Result<(), ZmqError> {
if self.send.is_some() {
self.complete_dropped().await;
}
retry(Parked { outbox: self, name }, message)
.await
.map_err(|failure| failure.into_error(name))
}
async fn complete_dropped(&mut self) {
let Some(send) = self.send.as_mut() else {
return;
};
let outcome = poll_fn(|cx| send.with_referential_mut(|send| send.as_mut().poll(cx))).await;
self.socket = self.send.take().map(SelfieMut::into_owned);
let name = self.dropped.take().unwrap_or_default();
if let Err(err) = outcome {
let error = SendFailure::Wire(err).into_error(&name);
tracing::warn!(
name = %name,
error = %error,
"a message whose publish was dropped mid-send did not leave"
);
}
}
}
struct Parked<'a, S: 'static> {
outbox: &'a mut Outbox<S>,
name: &'a str,
}
impl<S> Offer for Parked<'_, S>
where
S: SocketSend + Send + Unpin + 'static,
{
#[inline]
fn offer(&mut self, message: zeromq::ZmqMessage) -> impl Future<Output = ZmqResult<()>> + Send {
let socket = self
.outbox
.socket
.take()
.expect("a parked send completes before the next offer");
self.outbox.send = Some(SelfieMut::new(socket, |socket| {
Pin::into_inner(socket).send(message)
}));
Sending {
outbox: &mut *self.outbox,
name: self.name,
done: false,
}
}
}
struct Sending<'a, S: 'static> {
outbox: &'a mut Outbox<S>,
name: &'a str,
done: bool,
}
impl<S> Future for Sending<'_, S> {
type Output = ZmqResult<()>;
#[inline]
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<ZmqResult<()>> {
let this = &mut *self;
let send = this
.outbox
.send
.as_mut()
.expect("the send stays in the outbox until it completes");
let outcome = ready!(send.with_referential_mut(|send| send.as_mut().poll(cx)));
this.outbox.socket = this.outbox.send.take().map(SelfieMut::into_owned);
this.done = true;
Poll::Ready(outcome)
}
}
impl<S> Drop for Sending<'_, S> {
fn drop(&mut self) {
if !self.done {
self.outbox.dropped = Some(self.name.to_owned());
}
}
}
pub(crate) enum DriverHandle {
Task(JoinHandle<()>, Option<BoundSlot>),
#[cfg(feature = "testing")]
InProcess(Registration, Option<BoundSlot>),
}
#[cfg(not(feature = "testing"))]
const _: () =
assert!(size_of::<DriverHandle>() == size_of::<(JoinHandle<()>, Option<BoundSlot>)>());
impl Drop for DriverHandle {
fn drop(&mut self) {
match self {
Self::Task(task, _slot) => task.abort(),
#[cfg(feature = "testing")]
Self::InProcess(registration, _slot) => registration.give_up(),
}
}
}
pub(crate) type DeliverySender = mpsc::Sender<Result<ZmqMessage, ZmqError>>;
pub(crate) enum DeliveryReceiver {
Driver(mpsc::Receiver<Result<ZmqMessage, ZmqError>>),
#[cfg(feature = "testing")]
InProcess(Deliveries),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(
size_of::<DeliveryReceiver>() == size_of::<mpsc::Receiver<Result<ZmqMessage, ZmqError>>>()
);
impl DeliveryReceiver {
fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<ZmqMessage, ZmqError>>> {
match self {
Self::Driver(rx) => rx.poll_recv(cx),
#[cfg(feature = "testing")]
Self::InProcess(rx) => rx.poll_recv(cx),
}
}
}
pub(crate) enum Sender<S> {
Socket(S),
#[cfg(feature = "testing")]
InProcess {
bus: Arc<Bus>,
local: bool,
},
}
pub(crate) struct WireSubscriber {
pub(crate) rx: DeliveryReceiver,
pub(crate) _driver: DriverHandle,
}
impl Subscriber for WireSubscriber {
type Message = ZmqMessage;
type Error = ZmqError;
fn stream(&mut self) -> impl Stream<Item = Result<ZmqMessage, ZmqError>> + Send + '_ {
futures::stream::poll_fn(move |cx| self.rx.poll_recv(cx))
}
}
pub(crate) type SharedLifecycle = Arc<Lifecycle>;
#[cfg(test)]
mod tests {
use std::pin::pin;
use std::sync::Mutex;
use async_trait::async_trait;
use bytes::Bytes;
use futures::poll;
use tokio::sync::Notify;
use tokio::time::advance;
use super::*;
struct Handshaking {
refusals: usize,
stall: Duration,
stalled: bool,
}
#[async_trait]
impl SocketSend for Handshaking {
async fn send(&mut self, message: zeromq::ZmqMessage) -> ZmqResult<()> {
if self.refusals == 0 {
return Ok(());
}
if !self.stalled {
self.stalled = true;
advance(self.stall).await;
}
self.refusals -= 1;
Err(WireError::ReturnToSender {
reason: "no peer",
message,
})
}
}
#[tokio::test(start_paused = true)]
async fn the_retry_window_starts_when_the_peer_first_refuses() {
let mut socket = Handshaking {
refusals: 1,
stall: SEND_RETRY_WINDOW + Duration::from_secs(1),
stalled: false,
};
send_with_retry(
&mut socket,
"orders",
zeromq::ZmqMessage::from(Bytes::from_static(b"{}")),
)
.await
.expect("the peer refused once and then took the message");
}
struct Gated {
gate: Arc<Notify>,
held: bool,
sent: Arc<Mutex<Vec<Bytes>>>,
}
#[async_trait]
impl SocketSend for Gated {
async fn send(&mut self, message: zeromq::ZmqMessage) -> ZmqResult<()> {
if self.held {
self.gate.notified().await;
self.held = false;
}
let payload = message.get(0).cloned().unwrap_or_default();
self.sent.lock().expect("not poisoned").push(payload);
Ok(())
}
}
#[tokio::test]
async fn a_send_dropped_mid_flight_completes_before_the_next() {
let gate = Arc::new(Notify::new());
let sent = Arc::new(Mutex::new(Vec::new()));
let mut outbox = Outbox::new(Gated {
gate: Arc::clone(&gate),
held: true,
sent: Arc::clone(&sent),
});
{
let dropped = pin!(outbox.send(
"first",
zeromq::ZmqMessage::from(Bytes::from_static(b"first"))
));
assert!(
poll!(dropped).is_pending(),
"the socket holds the first send"
);
}
gate.notify_one();
outbox
.send(
"second",
zeromq::ZmqMessage::from(Bytes::from_static(b"second")),
)
.await
.expect("the second send leaves");
assert_eq!(
*sent.lock().expect("not poisoned"),
[Bytes::from_static(b"first"), Bytes::from_static(b"second")],
);
}
}