use std::fmt;
use std::num::NonZeroUsize;
use futures::Stream;
#[cfg(feature = "testing")]
use futures::future::Either;
use fe2o3_amqp::link::receiver::CreditMode;
use fe2o3_amqp::link::{Receiver as FeReceiver, RecvError};
use fe2o3_amqp::session::SessionHandle;
use fe2o3_amqp_types::messaging::{Body, Modified};
use fe2o3_amqp_types::primitives::Value;
use ruststream::{AckError, BatchSubscriber, BufferedSubscriber, Subscriber};
use tokio::sync::{mpsc, watch};
use crate::address::AmqpAddress;
use crate::broker::{AmqpCore, PumpGuard, is_at_most_once, source_for};
use crate::error::{AmqpError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::BusDeliveries;
use crate::message::{
AmqpMessage, SettleCmd, SettleKind, SettleSender, headers_from_amqp, payload_from_body,
};
pub struct AmqpSubscriber {
address: String,
deliveries: BufferedSubscriber<Deliveries>,
}
impl fmt::Debug for AmqpSubscriber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AmqpSubscriber")
.field("address", &self.address)
.finish_non_exhaustive()
}
}
impl AmqpSubscriber {
#[must_use]
pub fn address(&self) -> &str {
&self.address
}
pub(crate) async fn attach(
core: &AmqpCore,
mut session: SessionHandle<()>,
address: AmqpAddress,
guard: PumpGuard,
) -> Result<Self, AmqpError> {
let at_most_once = is_at_most_once(address.settle_value());
let credit = address.credit_value();
let receiver = FeReceiver::builder()
.name(core.link_name("receiver"))
.source(source_for(&address))
.auto_accept(at_most_once)
.credit_mode(CreditMode::Auto(credit))
.attach(&mut session)
.await
.map_err(|e| AmqpError::Attach {
address: address.address().to_owned(),
source: box_err(e),
})?;
let (out_tx, out_rx) = mpsc::channel(credit as usize);
let (settle_tx, settle_rx) = mpsc::unbounded_channel();
let addr = address.address().to_owned();
core.runtime.spawn(pump(Pump {
session,
receiver,
out: out_tx,
settle_tx,
settle_rx,
address: addr.clone(),
at_most_once,
guard,
}));
Ok(Self {
address: addr,
deliveries: BufferedSubscriber::new(Deliveries::Amqp(out_rx))
.max_wait(address.batch_wait_value()),
})
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(deliveries: BusDeliveries, address: &AmqpAddress) -> Self {
Self {
address: address.address().to_owned(),
deliveries: BufferedSubscriber::new(Deliveries::InProcess(deliveries))
.max_wait(address.batch_wait_value()),
}
}
}
impl Subscriber for AmqpSubscriber {
type Message = AmqpMessage;
type Error = AmqpError;
fn stream(&mut self) -> impl Stream<Item = Result<AmqpMessage, AmqpError>> + Send + '_ {
self.deliveries.stream()
}
}
impl BatchSubscriber for AmqpSubscriber {
type Batch = Vec<AmqpMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
self.deliveries.batches(size)
}
}
type PumpReceiver = mpsc::Receiver<Result<AmqpMessage, AmqpError>>;
enum Deliveries {
Amqp(PumpReceiver),
#[cfg(feature = "testing")]
InProcess(BusDeliveries),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Deliveries>() == size_of::<PumpReceiver>());
impl Subscriber for Deliveries {
type Message = AmqpMessage;
type Error = AmqpError;
fn stream(&mut self) -> impl Stream<Item = Result<AmqpMessage, AmqpError>> + Send + '_ {
match self {
#[cfg(not(feature = "testing"))]
Self::Amqp(rx) => pump_stream(rx),
#[cfg(feature = "testing")]
Self::Amqp(rx) => Either::Left(pump_stream(rx)),
#[cfg(feature = "testing")]
Self::InProcess(deliveries) => Either::Right(deliveries.stream()),
}
}
}
fn pump_stream(
rx: &mut PumpReceiver,
) -> impl Stream<Item = Result<AmqpMessage, AmqpError>> + Send + '_ {
futures::stream::poll_fn(move |cx| rx.poll_recv(cx))
}
struct Pump {
session: SessionHandle<()>,
receiver: FeReceiver,
out: mpsc::Sender<Result<AmqpMessage, AmqpError>>,
settle_tx: SettleSender,
settle_rx: mpsc::UnboundedReceiver<SettleCmd>,
address: String,
at_most_once: bool,
guard: PumpGuard,
}
fn is_per_message(err: &RecvError) -> bool {
matches!(
err,
RecvError::MessageDecode(_)
| RecvError::DeliveryIdIsNone
| RecvError::DeliveryTagIsNone
| RecvError::InconsistentFieldInMultiFrameDelivery
| RecvError::MessageSizeExceeded(_)
)
}
async fn pump(mut p: Pump) {
let mut pending: Option<AmqpMessage> = None;
let fatal = loop {
if let Some(msg) = pending.take() {
tokio::select! {
biased;
cmd = p.settle_rx.recv() => {
if let Some(cmd) = cmd {
apply(&p.receiver, cmd).await;
}
pending = Some(msg);
}
permit = p.out.reserve() => match permit {
Ok(permit) => permit.send(Ok(msg)),
Err(_) => break false, },
() = stopped(&mut p.guard.stop) => break false, }
} else {
tokio::select! {
biased;
cmd = p.settle_rx.recv() => {
if let Some(cmd) = cmd {
apply(&p.receiver, cmd).await;
}
}
() = p.out.closed() => break false, () = stopped(&mut p.guard.stop) => break false, delivery = p.receiver.recv::<Body<Value>>() => match delivery {
Ok(delivery) => {
let (info, message) = delivery.into_parts();
let headers = headers_from_amqp(&message);
let delivery_count = message.header.as_ref().map(|h| h.delivery_count);
match payload_from_body(message.body, &p.address) {
Ok(payload) => {
pending = Some(if p.at_most_once {
AmqpMessage::settled(payload, headers, delivery_count)
} else {
AmqpMessage::unsettled(
payload,
headers,
delivery_count,
p.settle_tx.clone(),
info,
)
});
}
Err(err) => {
if !p.at_most_once {
let _ = p.receiver.reject(info, None).await;
}
if !deliver(&p.out, &mut p.guard.stop, Err(err)).await {
break false;
}
}
}
}
Err(err) if is_per_message(&err) => {
let item = Err(AmqpError::Receive {
address: p.address.clone(),
source: box_err(err),
});
if !deliver(&p.out, &mut p.guard.stop, item).await {
break false;
}
}
Err(err) => {
let error = Err(AmqpError::Receive {
address: p.address.clone(),
source: box_err(err),
});
deliver(&p.out, &mut p.guard.stop, error).await;
break true;
}
},
}
}
};
drop(p.out);
drop(p.settle_tx);
loop {
tokio::select! {
biased;
cmd = p.settle_rx.recv() => match cmd {
Some(cmd) => apply(&p.receiver, cmd).await,
None => break,
},
() = stopped(&mut p.guard.stop) => break,
}
}
if !fatal && let Err((_, err)) = p.receiver.detach().await {
tracing::debug!(address = %p.address, error = %err, "amqp receiver detach failed");
}
if let Err(err) = p.session.end().await {
tracing::debug!(address = %p.address, error = %err, "amqp session end failed");
}
drop(p.guard);
}
async fn deliver(
out: &mpsc::Sender<Result<AmqpMessage, AmqpError>>,
stop: &mut watch::Receiver<bool>,
item: Result<AmqpMessage, AmqpError>,
) -> bool {
tokio::select! {
biased;
() = stopped(stop) => false,
sent = out.send(item) => sent.is_ok(),
}
}
async fn stopped(stop: &mut watch::Receiver<bool>) {
let _ = stop.wait_for(|stopped| *stopped).await.is_ok();
}
async fn apply(receiver: &FeReceiver, cmd: SettleCmd) {
let result = match cmd.kind {
SettleKind::Accept => receiver.accept(cmd.info).await,
SettleKind::Modify => {
receiver
.modify(
cmd.info,
Modified {
delivery_failed: Some(true),
undeliverable_here: Some(false),
message_annotations: None,
},
)
.await
}
SettleKind::Reject => receiver.reject(cmd.info, None).await,
};
let _ = cmd
.done
.send(result.map_err(|e| AckError::Broker(box_err(e))));
}
#[cfg(test)]
mod tests {
use fe2o3_amqp::link::{MessageSizeExceeded, RecvError};
use super::is_per_message;
#[test]
fn an_oversized_delivery_does_not_end_the_subscription() {
let oversized = RecvError::MessageSizeExceeded(MessageSizeExceeded {
size: 2048,
max_size: 1024,
});
assert!(is_per_message(&oversized));
}
#[test]
fn a_transfer_beyond_the_credit_ends_the_subscription() {
assert!(!is_per_message(&RecvError::TransferLimitExceeded));
}
}