use futures::Stream;
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;
use fe2o3_amqp_types::primitives::Value;
use ruststream::{AckError, Subscriber};
use tokio::sync::mpsc;
use crate::address::AmqpAddress;
use crate::broker::{AmqpCore, is_at_most_once, source_for};
use crate::error::{AmqpError, box_err};
use crate::message::{
AmqpMessage, SettleCmd, SettleKind, SettleSender, headers_from_amqp, payload_from_body,
};
pub struct AmqpSubscriber {
address: String,
rx: mpsc::Receiver<Result<AmqpMessage, AmqpError>>,
}
impl std::fmt::Debug for AmqpSubscriber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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,
) -> 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.max(1) as usize);
let (settle_tx, settle_rx) = mpsc::unbounded_channel();
let addr = address.address().to_owned();
tokio::spawn(pump(Pump {
session,
receiver,
out: out_tx,
settle_tx,
settle_rx,
address: addr.clone(),
at_most_once,
}));
Ok(Self {
address: addr,
rx: out_rx,
})
}
}
impl Subscriber for AmqpSubscriber {
type Message = AmqpMessage;
type Error = AmqpError;
fn stream(&mut self) -> impl Stream<Item = Result<AmqpMessage, AmqpError>> + Send + '_ {
futures::stream::poll_fn(move |cx| self.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,
}
fn is_per_message(err: &RecvError) -> bool {
matches!(
err,
RecvError::MessageDecode(_)
| RecvError::DeliveryIdIsNone
| RecvError::DeliveryTagIsNone
| RecvError::InconsistentFieldInMultiFrameDelivery
)
}
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, },
}
} else {
tokio::select! {
biased;
cmd = p.settle_rx.recv() => {
if let Some(cmd) = cmd {
apply(&p.receiver, cmd).await;
}
}
() = p.out.closed() => break false, delivery = p.receiver.recv::<Body<Value>>() => match delivery {
Ok(delivery) => {
let (info, message) = delivery.into_parts();
let headers = headers_from_amqp(&message);
match payload_from_body(message.body, &p.address) {
Ok(payload) => {
pending = Some(if p.at_most_once {
AmqpMessage::settled(payload, headers)
} else {
AmqpMessage::unsettled(
payload,
headers,
p.settle_tx.clone(),
info,
)
});
}
Err(err) => {
if !p.at_most_once {
let _ = p.receiver.reject(info, None).await;
}
if p.out.send(Err(err)).await.is_err() {
break false;
}
}
}
}
Err(err) if is_per_message(&err) => {
let item = Err(AmqpError::Receive {
address: p.address.clone(),
source: box_err(err),
});
if p.out.send(item).await.is_err() {
break false;
}
}
Err(err) => {
let _ = p.out.send(Err(AmqpError::Receive {
address: p.address.clone(),
source: box_err(err),
}))
.await;
break true;
}
},
}
}
};
drop(p.settle_tx);
while let Some(cmd) = p.settle_rx.recv().await {
apply(&p.receiver, cmd).await;
}
if !fatal {
if 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");
}
}
async fn apply(receiver: &FeReceiver, cmd: SettleCmd) {
let result = match cmd.kind {
SettleKind::Accept => receiver.accept(cmd.info).await,
SettleKind::Release => receiver.release(cmd.info).await,
SettleKind::Reject => receiver.reject(cmd.info, None).await,
};
let _ = cmd
.done
.send(result.map_err(|e| AckError::Broker(box_err(e))));
}