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()
}
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(_) => Err(SendError::msg(
ErrorKind::ProtocolViolation,
"transaction discharge was rejected",
)),
_ => Ok(()),
}
}
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 coordinator_target_uses_capability() {
let coord = crate::types::messaging::Coordinator {
capabilities: vec![Symbol::new(capabilities::LOCAL_TRANSACTIONS)],
};
let archetype = TargetArchetype::Coordinator(coord);
rt(archetype);
}
}