use std::collections::HashMap;
use std::sync::Arc;
use fe2o3_amqp::Sender;
use fe2o3_amqp::transaction::{Controller, OwnedTransaction, TransactionDischarge};
use fe2o3_amqp_types::definitions::SenderSettleMode;
use fe2o3_amqp_types::transaction::Coordinator;
use ruststream::{OutgoingMessage, PairError, PublishPolicy, Publisher, TransactionalPublisher};
use tokio::sync::Mutex;
use crate::broker::{AmqpCore, ConnectedAmqpBroker};
use crate::error::{AmqpError, box_err};
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;
async fn pair(self, connected: &ConnectedAmqpBroker) -> Result<Self::Live, PairError> {
Ok(connected.transactional_publisher())
}
}
impl ConnectedAmqpBroker {
#[must_use]
pub fn transactional_publisher(&self) -> AmqpTxnPublisher {
AmqpTxnPublisher::new(Arc::clone(&self.core))
}
}
pub struct AmqpTxnPublisher {
core: Arc<AmqpCore>,
senders: Mutex<HashMap<String, Arc<Mutex<Sender>>>>,
txn: Mutex<Option<OwnedTransaction>>,
}
impl std::fmt::Debug for AmqpTxnPublisher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AmqpTxnPublisher").finish_non_exhaustive()
}
}
impl AmqpTxnPublisher {
pub(crate) fn new(core: Arc<AmqpCore>) -> Self {
Self {
core,
senders: Mutex::new(HashMap::new()),
txn: Mutex::new(None),
}
}
#[allow(clippy::significant_drop_tightening)]
async fn sender_for(&self, address: &str) -> Result<Arc<Mutex<Sender>>, AmqpError> {
let mut senders = self.senders.lock().await;
if let Some(sender) = senders.get(address) {
return Ok(Arc::clone(sender));
}
let sender = ConnectedAmqpBroker::attach_sender(&self.core, address).await?;
let sender = Arc::new(Mutex::new(sender));
senders.insert(address.to_owned(), Arc::clone(&sender));
Ok(sender)
}
}
impl Publisher for AmqpTxnPublisher {
type Error = AmqpError;
async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
self.core.ensure_open()?;
let sender = self.sender_for(msg.name()).await?;
let message = to_amqp_message(&msg);
let txn = self.txn.lock().await;
if let Some(txn) = txn.as_ref() {
let outcome = {
let mut sender = sender.lock().await;
txn.post(&mut sender, message)
.await
.map_err(|e| AmqpError::Publish {
address: msg.name().to_owned(),
source: box_err(e),
})?
};
accepted(outcome, msg.name())
} else {
drop(txn);
crate::publisher::send_message(&sender, msg.name(), message).await
}
}
}
impl TransactionalPublisher for AmqpTxnPublisher {
#[allow(clippy::significant_drop_tightening)]
async fn begin_transaction(&self) -> Result<(), Self::Error> {
self.core.ensure_open()?;
let mut slot = self.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 = self.core.session.lock().await;
let controller = Controller::builder()
.name(self.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 txn = self.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 txn = self.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()))
}
}