#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::fmt;
use std::future::{Future, ready};
use std::sync::Arc;
use fe2o3_amqp::transaction::{
Controller, OwnedTransaction, TransactionDischarge, TransactionPosting,
};
use fe2o3_amqp_types::definitions::SenderSettleMode;
use fe2o3_amqp_types::transaction::Coordinator;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::{OutgoingFor, PairError, PublishPolicy, Publisher, Take, TransactionalPublisher};
#[cfg(feature = "asyncapi")]
use crate::bindings;
use tokio::sync::Mutex;
use crate::broker::{AmqpCore, ConnectedAmqpBroker, Link};
use crate::error::{AmqpError, box_err};
#[cfg(feature = "testing")]
use crate::in_process::TxnBuffer;
use crate::message::to_amqp_message;
use crate::publisher::accepted;
#[derive(Debug, Clone, Copy, Default)]
#[must_use]
pub struct AmqpTransactionalPublish;
impl PublishPolicy<ConnectedAmqpBroker> for AmqpTransactionalPublish {
type Live = AmqpTxnPublisher;
fn pair(
self,
connected: &ConnectedAmqpBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(connected.transactional_publisher()))
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self, channel: &str) -> Bindings {
bindings::target(channel)
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self, _channel: &str) -> Bindings {
bindings::transactional_posting()
}
#[cfg(feature = "asyncapi")]
fn reply_address_location(&self) -> Option<&'static str> {
Some(bindings::REPLY_ADDRESS_LOCATION)
}
}
impl ConnectedAmqpBroker {
#[must_use]
pub fn transactional_publisher(&self) -> AmqpTxnPublisher {
let transport = match &self.link {
Link::Amqp(core) => Transport::Amqp(AmqpTxn {
core: Arc::clone(core),
txn: Mutex::new(None),
}),
#[cfg(feature = "testing")]
Link::InProcess(bus) => Transport::InProcess(TxnBuffer::new(Arc::clone(bus))),
};
AmqpTxnPublisher { transport }
}
}
pub struct AmqpTxnPublisher {
transport: Transport,
}
#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
enum Transport {
Amqp(AmqpTxn),
#[cfg(feature = "testing")]
InProcess(TxnBuffer),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Transport>() == size_of::<AmqpTxn>());
struct AmqpTxn {
core: Arc<AmqpCore>,
txn: Mutex<Option<OwnedTransaction>>,
}
impl fmt::Debug for AmqpTxnPublisher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AmqpTxnPublisher").finish_non_exhaustive()
}
}
impl Publisher for AmqpTxnPublisher {
type Payload = Take;
type Error = AmqpError;
type Options = ();
async fn publish(
&self,
msg: OutgoingFor<'_, Take>,
_options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let live = match &self.transport {
Transport::Amqp(live) => live,
#[cfg(feature = "testing")]
Transport::InProcess(buffer) => return buffer.publish(msg),
};
live.core.ensure_open()?;
let address = msg.name();
let sender = live.core.sender_for(address).await?;
let message = to_amqp_message(msg);
let txn = live.txn.lock().await;
if let Some(txn) = txn.as_ref() {
let outcome = sender
.with(async |sender| {
txn.post(sender, message)
.await
.map_err(|e| AmqpError::Publish {
address: address.to_owned(),
source: box_err(e),
})
})
.await?;
accepted(outcome, address)
} else {
drop(txn);
crate::publisher::send_message(&sender, address, message).await
}
}
}
impl TransactionalPublisher for AmqpTxnPublisher {
#[allow(clippy::significant_drop_tightening)]
async fn begin_transaction(&self) -> Result<(), Self::Error> {
let live = match &self.transport {
Transport::Amqp(live) => live,
#[cfg(feature = "testing")]
Transport::InProcess(buffer) => return buffer.begin(),
};
live.core.ensure_open()?;
let mut slot = live.txn.lock().await;
if slot.is_some() {
return Err(AmqpError::Transaction(
"a transaction is already open on this publisher".into(),
));
}
let txn = {
let mut session = live.core.session.lock().await;
let controller = Controller::builder()
.name(live.core.link_name("txn"))
.coordinator(Coordinator::default())
.sender_settle_mode(SenderSettleMode::Mixed)
.attach(&mut session)
.await
.map_err(|e| AmqpError::Transaction(e.to_string()))?;
OwnedTransaction::declare_with_controller(controller, None)
.await
.map_err(|e| AmqpError::Transaction(e.to_string()))?
};
*slot = Some(txn);
Ok(())
}
async fn commit(&self) -> Result<(), Self::Error> {
let live = match &self.transport {
Transport::Amqp(live) => live,
#[cfg(feature = "testing")]
Transport::InProcess(buffer) => return buffer.commit(),
};
let txn = live.txn.lock().await.take().ok_or_else(|| {
AmqpError::Transaction("no transaction is open on this publisher".into())
})?;
txn.commit()
.await
.map_err(|e| AmqpError::Transaction(e.to_string()))
}
async fn abort(&self) -> Result<(), Self::Error> {
let live = match &self.transport {
Transport::Amqp(live) => live,
#[cfg(feature = "testing")]
Transport::InProcess(buffer) => return buffer.abort(),
};
let txn = live.txn.lock().await.take().ok_or_else(|| {
AmqpError::Transaction("no transaction is open on this publisher".into())
})?;
txn.rollback()
.await
.map_err(|e| AmqpError::Transaction(e.to_string()))
}
}