use bytes::{BufMut, Bytes, BytesMut};
use crate::amqp_composite;
use crate::api::producer::Producer;
use crate::codec::described::descriptors;
use crate::codec::encode::encode_descriptor;
use crate::codec::{Encode, from_slice, to_vec};
use crate::error::{ErrorKind, SendError};
use crate::types::messaging::{DeliveryState, Outcome};
pub type TxnId = Bytes;
pub mod capabilities {
pub const LOCAL_TRANSACTIONS: &str = "amqp:local-transactions";
pub const DISTRIBUTED_TRANSACTIONS: &str = "amqp:distributed-transactions";
pub const MULTI_TXNS_PER_SSN: &str = "amqp:multi-txns-per-ssn";
pub const MULTI_SSNS_PER_TXN: &str = "amqp:multi-ssns-per-txn";
}
amqp_composite! {
pub struct Declare : descriptors::DECLARE => {
global_id: Option<Bytes> = opt(),
}
}
amqp_composite! {
pub struct Discharge : descriptors::DISCHARGE => {
txn_id: Bytes = req("txn-id"),
fail: bool = default(false),
}
}
amqp_composite! {
pub struct Declared : descriptors::DECLARED => {
txn_id: Bytes = req("txn-id"),
}
}
amqp_composite! {
pub struct TransactionalState : descriptors::TRANSACTIONAL_STATE => {
txn_id: Bytes = req("txn-id"),
outcome: Option<Outcome> = opt(),
}
}
fn control_message<T: Encode>(content: &T) -> Bytes {
let mut buf = BytesMut::new();
buf.put_u8(crate::codec::codes::DESCRIBED);
encode_descriptor(&mut buf, descriptors::AMQP_VALUE);
content.encode(&mut buf);
buf.freeze()
}
pub fn transactional_state(txn_id: TxnId, outcome: Option<Outcome>) -> DeliveryState {
let ts = TransactionalState { txn_id, outcome };
let value = from_slice::<crate::codec::Value>(&to_vec(&ts))
.expect("transactional-state encodes to a valid described value");
DeliveryState::Other(value)
}
fn declared_txn_id(state: &DeliveryState) -> Option<TxnId> {
if let DeliveryState::Other(value) = state {
let bytes = to_vec(value);
if let Ok(declared) = from_slice::<Declared>(&bytes) {
return Some(declared.txn_id);
}
}
None
}
#[derive(Debug)]
pub struct TransactionController {
control: Producer,
}
impl TransactionController {
pub fn new(control: Producer) -> Self {
TransactionController { control }
}
pub async fn declare(&self) -> Result<TxnId, SendError> {
let body = control_message(&Declare { global_id: None });
let outcome = self.control.send_bytes(body, false).await?;
declared_txn_id(&outcome).ok_or_else(|| {
SendError::msg(
ErrorKind::ProtocolViolation,
"coordinator did not return a declared outcome",
)
})
}
pub async fn discharge(&self, txn_id: TxnId, fail: bool) -> Result<(), SendError> {
let body = control_message(&Discharge { txn_id, fail });
let outcome = self.control.send_bytes(body, false).await?;
match outcome {
DeliveryState::Accepted(_) => Ok(()),
DeliveryState::Rejected(r) => {
let mut msg = String::from("transaction discharge was rejected");
if let Some(e) = &r.error {
msg.push_str(": ");
msg.push_str(&e.to_string());
}
Err(SendError::msg(ErrorKind::ProtocolViolation, msg))
}
other => Err(SendError::msg(
ErrorKind::ProtocolViolation,
format!("unexpected discharge outcome: {other:?}"),
)),
}
}
pub async fn commit(&self, txn_id: TxnId) -> Result<(), SendError> {
self.discharge(txn_id, false).await
}
pub async fn rollback(&self, txn_id: TxnId) -> Result<(), SendError> {
self.discharge(txn_id, true).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::{Decode, Symbol};
use crate::types::messaging::{Accepted, TargetArchetype};
fn rt<T: Encode + Decode + PartialEq + std::fmt::Debug>(v: T) {
let back: T = from_slice(&to_vec(&v)).unwrap();
assert_eq!(v, back);
}
#[test]
fn txn_types_round_trip() {
rt(Declare { global_id: None });
rt(Declare {
global_id: Some(Bytes::from_static(b"global")),
});
rt(Discharge {
txn_id: Bytes::from_static(b"txn-1"),
fail: true,
});
rt(Declared {
txn_id: Bytes::from_static(b"txn-1"),
});
rt(TransactionalState {
txn_id: Bytes::from_static(b"txn-1"),
outcome: Some(Outcome::Accepted(Accepted::default())),
});
}
#[test]
fn declared_outcome_extraction() {
let declared = Declared {
txn_id: Bytes::from_static(b"abc"),
};
let bytes = to_vec(&declared);
let state: DeliveryState = from_slice(&bytes).unwrap();
assert!(matches!(state, DeliveryState::Other(_)));
assert_eq!(declared_txn_id(&state), Some(Bytes::from_static(b"abc")));
}
#[test]
fn transactional_state_round_trips_to_txn_state() {
let st = transactional_state(Bytes::from_static(b"txn-1"), None);
assert!(matches!(st, DeliveryState::Other(_)));
let ts: TransactionalState = from_slice(&to_vec(&st)).unwrap();
assert_eq!(ts.txn_id, Bytes::from_static(b"txn-1"));
}
#[test]
fn coordinator_target_uses_capability() {
let coord = crate::types::messaging::Coordinator {
capabilities: vec![Symbol::new(capabilities::LOCAL_TRANSACTIONS)],
};
let archetype = TargetArchetype::Coordinator(coord);
rt(archetype);
}
}