use std::sync::Arc;
use parking_lot::Mutex;
use tokio::sync::Notify;
use super::TransactionState;
use crate::error::{KrafkaError, Result};
#[derive(Debug)]
struct Inner {
state: TransactionState,
pending: u64,
failed: Option<KrafkaError>,
}
#[derive(Debug)]
pub(crate) struct TxnGate {
inner: Mutex<Inner>,
drained: Notify,
}
impl TxnGate {
pub(crate) fn new() -> Self {
Self {
inner: Mutex::new(Inner {
state: TransactionState::Uninitialized,
pending: 0,
failed: None,
}),
drained: Notify::new(),
}
}
pub(crate) fn state(&self) -> TransactionState {
self.inner.lock().state
}
pub(crate) fn set(&self, state: TransactionState) {
self.inner.lock().state = state;
}
pub(crate) fn transition(
&self,
from: &[TransactionState],
to: TransactionState,
) -> std::result::Result<TransactionState, TransactionState> {
let mut inner = self.inner.lock();
if from.contains(&inner.state) {
let left = inner.state;
inner.state = to;
Ok(left)
} else {
Err(inner.state)
}
}
pub(crate) fn begin(&self) -> std::result::Result<(), TransactionState> {
let mut inner = self.inner.lock();
if inner.state != TransactionState::Ready {
return Err(inner.state);
}
inner.state = TransactionState::Open;
inner.failed = None;
Ok(())
}
pub(crate) fn check_open(&self, operation: &str) -> Result<()> {
let inner = self.inner.lock();
Self::open_or_refuse(&inner, operation)
}
fn open_or_refuse(inner: &Inner, operation: &str) -> Result<()> {
if let Some(failed) = &inner.failed {
return Err(abortable(operation, failed));
}
match inner.state {
TransactionState::Open => Ok(()),
TransactionState::Fatal => Err(KrafkaError::fenced(format!(
"cannot {operation}: the transactional producer hit a fatal error"
))),
state => Err(KrafkaError::illegal_state(format!(
"cannot {operation} in transaction state {state}"
))),
}
}
pub(crate) fn admit(self: &Arc<Self>) -> Result<TxnTicket> {
let mut inner = self.inner.lock();
Self::open_or_refuse(&inner, "send")?;
inner.pending += 1;
Ok(TxnTicket {
gate: Some(Arc::clone(self)),
})
}
pub(crate) fn failure(&self, operation: &str) -> Option<KrafkaError> {
self.inner
.lock()
.failed
.as_ref()
.map(|failed| abortable(operation, failed))
}
pub(crate) fn has_failed(&self) -> bool {
self.inner.lock().failed.is_some()
}
pub(crate) fn fail(&self, error: KrafkaError) {
let mut inner = self.inner.lock();
if inner.failed.is_none() {
inner.failed = Some(error);
}
}
pub(crate) async fn drained(&self) {
loop {
let notified = self.drained.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.inner.lock().pending == 0 {
return;
}
notified.await;
}
}
fn resolve(&self, failure: Option<KrafkaError>) {
let mut inner = self.inner.lock();
inner.pending = inner.pending.saturating_sub(1);
if let Some(error) = failure
&& inner.failed.is_none()
{
inner.failed = Some(error);
}
if inner.pending == 0 {
self.drained.notify_waiters();
}
}
}
fn abortable(operation: &str, failed: &KrafkaError) -> KrafkaError {
KrafkaError::transaction_abortable(format!(
"cannot {operation}: a send in this transaction failed ({failed}); abort the transaction"
))
}
#[derive(Debug)]
pub(crate) struct TxnTicket {
gate: Option<Arc<TxnGate>>,
}
impl TxnTicket {
pub(crate) fn complete(mut self, result: std::result::Result<(), &KrafkaError>) {
if let Some(gate) = self.gate.take() {
gate.resolve(result.err().cloned());
}
}
}
impl Drop for TxnTicket {
fn drop(&mut self) {
if let Some(gate) = self.gate.take() {
gate.resolve(Some(KrafkaError::illegal_state(
"a transactional send was dropped before its outcome was known",
)));
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use std::time::Duration;
fn open_gate() -> Arc<TxnGate> {
let gate = Arc::new(TxnGate::new());
gate.set(TransactionState::Ready);
gate.begin().unwrap();
gate
}
#[tokio::test]
async fn a_refused_send_does_not_release_the_wait() {
let gate = open_gate();
let in_flight = gate.admit().unwrap();
gate.transition(&[TransactionState::Open], TransactionState::Committing)
.unwrap();
assert!(gate.admit().is_err(), "committing refuses sends");
let wait = tokio::time::timeout(Duration::from_millis(20), gate.drained()).await;
assert!(wait.is_err(), "the admitted send is still pending");
in_flight.complete(Ok(()));
tokio::time::timeout(Duration::from_secs(1), gate.drained())
.await
.unwrap();
}
#[test]
fn the_first_failure_refuses_later_sends_and_commit() {
let gate = open_gate();
let a = gate.admit().unwrap();
let b = gate.admit().unwrap();
a.complete(Err(&KrafkaError::config("first")));
b.complete(Err(&KrafkaError::config("second")));
let refused = gate.admit().unwrap_err();
assert!(refused.requires_abort(), "{refused}");
assert!(refused.to_string().contains("first"), "{refused}");
assert!(
gate.failure("commit")
.unwrap()
.to_string()
.contains("first")
);
}
#[test]
fn a_dropped_ticket_counts_as_a_failure() {
let gate = open_gate();
drop(gate.admit().unwrap());
assert!(gate.has_failed());
}
#[test]
fn begin_clears_the_failure() {
let gate = open_gate();
gate.admit()
.unwrap()
.complete(Err(&KrafkaError::config("x")));
gate.set(TransactionState::Ready);
gate.begin().unwrap();
assert!(!gate.has_failed());
assert!(gate.admit().is_ok());
}
}