use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShardTxError {
PrepareFailed {
shard_id: String,
reason: String,
},
CommitFailed {
shard_id: String,
reason: String,
},
RollbackFailed {
shard_id: String,
reason: String,
},
}
impl std::fmt::Display for ShardTxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ShardTxError::PrepareFailed { shard_id, reason } => {
write!(f, "prepare failed on shard {}: {}", shard_id, reason)
}
ShardTxError::CommitFailed { shard_id, reason } => {
write!(f, "commit failed on shard {}: {}", shard_id, reason)
}
ShardTxError::RollbackFailed { shard_id, reason } => {
write!(f, "rollback failed on shard {}: {}", shard_id, reason)
}
}
}
}
impl std::error::Error for ShardTxError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShardTxResult {
pub shard_id: String,
pub success: bool,
pub error: Option<String>,
}
pub struct ShardParticipant {
pub shard_id: String,
pub prepare: Box<dyn Fn() -> Result<(), String> + Send + Sync>,
pub commit: Box<dyn Fn() -> Result<(), String> + Send + Sync>,
pub rollback: Box<dyn Fn() -> Result<(), String> + Send + Sync>,
}
impl ShardParticipant {
pub fn new<P, C, R>(shard_id: &str, prepare: P, commit: C, rollback: R) -> Self
where
P: Fn() -> Result<(), String> + Send + Sync + 'static,
C: Fn() -> Result<(), String> + Send + Sync + 'static,
R: Fn() -> Result<(), String> + Send + Sync + 'static,
{
Self {
shard_id: shard_id.to_string(),
prepare: Box::new(prepare),
commit: Box::new(commit),
rollback: Box::new(rollback),
}
}
}
impl std::fmt::Debug for ShardParticipant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShardParticipant")
.field("shard_id", &self.shard_id)
.field("prepare", &"<closure>")
.field("commit", &"<closure>")
.field("rollback", &"<closure>")
.finish()
}
}
pub struct ShardTransactionCoordinator {
participants: Vec<ShardParticipant>,
max_retries: u32,
}
impl ShardTransactionCoordinator {
pub fn new() -> Self {
Self {
participants: Vec::new(),
max_retries: 0,
}
}
pub fn with_max_retries(mut self, n: u32) -> Self {
self.max_retries = n;
self
}
pub fn add_participant(&mut self, p: ShardParticipant) {
self.participants.push(p);
}
pub fn participant_count(&self) -> usize {
self.participants.len()
}
pub fn execute_2pc(&mut self) -> Result<(), ShardTxError> {
let mut prepared: HashSet<usize> = HashSet::new();
for (i, p) in self.participants.iter().enumerate() {
match (p.prepare)() {
Ok(()) => {
prepared.insert(i);
}
Err(reason) => {
for &idx in &prepared {
let _ = (self.participants[idx].rollback)();
}
return Err(ShardTxError::PrepareFailed {
shard_id: p.shard_id.clone(),
reason,
});
}
}
}
for p in &self.participants {
if let Err(reason) = (p.commit)() {
eprintln!(
"[ERROR] shard_id={}, reason={}, msg=\"Cross-shard transaction commit failed - manual intervention required\"",
p.shard_id, reason
);
return Err(ShardTxError::CommitFailed {
shard_id: p.shard_id.clone(),
reason,
});
}
}
Ok(())
}
pub fn execute_best_effort(&mut self) -> Vec<ShardTxResult> {
let mut results = Vec::with_capacity(self.participants.len());
for p in &self.participants {
let mut success = false;
let mut last_err: Option<String> = None;
for attempt in 0..=self.max_retries {
match (p.commit)() {
Ok(()) => {
success = true;
last_err = None;
break;
}
Err(reason) => {
last_err = Some(reason);
if attempt < self.max_retries {
continue;
}
}
}
}
results.push(ShardTxResult {
shard_id: p.shard_id.clone(),
success,
error: last_err,
});
}
results
}
}
impl Default for ShardTransactionCoordinator {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ShardTransactionCoordinator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShardTransactionCoordinator")
.field("participant_count", &self.participants.len())
.field("max_retries", &self.max_retries)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn test_2pc_all_succeed() {
let commit_count = Arc::new(Mutex::new(0u32));
let mut coord = ShardTransactionCoordinator::new();
let c0 = commit_count.clone();
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
move || {
*c0.lock().unwrap() += 1;
Ok(())
},
|| Ok(()),
));
let c1 = commit_count.clone();
coord.add_participant(ShardParticipant::new(
"s1",
|| Ok(()),
move || {
*c1.lock().unwrap() += 1;
Ok(())
},
|| Ok(()),
));
let result = coord.execute_2pc();
assert!(result.is_ok());
assert_eq!(
*commit_count.lock().unwrap(),
2,
"两个 participant 都成功时 commit 应被调用 2 次"
);
}
#[test]
fn test_2pc_empty_participants() {
let mut coord = ShardTransactionCoordinator::new();
let result = coord.execute_2pc();
assert!(result.is_ok());
assert_eq!(
coord.participant_count(),
0,
"空 participant 列表执行后计数应仍为 0"
);
}
#[test]
fn test_2pc_single_participant_success() {
let commit_count = Arc::new(Mutex::new(0u32));
let mut coord = ShardTransactionCoordinator::new();
let c = commit_count.clone();
coord.add_participant(ShardParticipant::new(
"only",
|| Ok(()),
move || {
*c.lock().unwrap() += 1;
Ok(())
},
|| Ok(()),
));
let result = coord.execute_2pc();
assert!(result.is_ok());
assert_eq!(
*commit_count.lock().unwrap(),
1,
"单 participant 成功时 commit 应被调用 1 次"
);
}
#[test]
fn test_2pc_prepare_fails_rolls_back_prepared() {
let rolled_back = Arc::new(Mutex::new(false));
let rb = rolled_back.clone();
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
|| Ok(()),
move || {
*rb.lock().unwrap() = true;
Ok(())
},
));
coord.add_participant(ShardParticipant::new(
"s1",
|| Err("prepare failed".to_string()),
|| Ok(()),
|| Ok(()),
));
let result = coord.execute_2pc();
assert!(matches!(result, Err(ShardTxError::PrepareFailed { .. })));
assert!(
*rolled_back.lock().unwrap(),
"s0 should have been rolled back"
);
if let Err(ShardTxError::PrepareFailed { shard_id, reason }) = result {
assert_eq!(shard_id, "s1");
assert_eq!(reason, "prepare failed");
}
}
#[test]
fn test_2pc_single_participant_prepare_fails() {
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new(
"only",
|| Err("nope".to_string()),
|| Ok(()),
|| Ok(()),
));
let result = coord.execute_2pc();
assert!(matches!(result, Err(ShardTxError::PrepareFailed { .. })));
}
#[test]
fn test_2pc_prepare_fails_at_third_rolls_back_first_two() {
let rollback_count = Arc::new(Mutex::new(0u32));
let mut coord = ShardTransactionCoordinator::new();
for i in 0..2 {
let rc = rollback_count.clone();
coord.add_participant(ShardParticipant::new(
&format!("s{}", i),
|| Ok(()),
|| Ok(()),
move || {
*rc.lock().unwrap() += 1;
Ok(())
},
));
}
coord.add_participant(ShardParticipant::new(
"s2",
|| Err("fail".to_string()),
|| Ok(()),
|| Ok(()),
));
let result = coord.execute_2pc();
assert!(matches!(result, Err(ShardTxError::PrepareFailed { .. })));
assert_eq!(*rollback_count.lock().unwrap(), 2);
}
#[test]
fn test_2pc_commit_fails() {
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new("s0", || Ok(()), || Ok(()), || Ok(())));
coord.add_participant(ShardParticipant::new(
"s1",
|| Ok(()),
|| Err("commit failed".to_string()),
|| Ok(()),
));
let result = coord.execute_2pc();
match result {
Err(ShardTxError::CommitFailed { shard_id, reason }) => {
assert_eq!(shard_id, "s1");
assert_eq!(reason, "commit failed");
}
other => panic!("expected CommitFailed, got {:?}", other),
}
}
#[test]
fn test_2pc_commit_fails_at_first() {
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
|| Err("first commit fails".to_string()),
|| Ok(()),
));
coord.add_participant(ShardParticipant::new("s1", || Ok(()), || Ok(()), || Ok(())));
let result = coord.execute_2pc();
assert!(matches!(result, Err(ShardTxError::CommitFailed { .. })));
}
#[test]
fn test_best_effort_all_succeed() {
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new("s0", || Ok(()), || Ok(()), || Ok(())));
coord.add_participant(ShardParticipant::new("s1", || Ok(()), || Ok(()), || Ok(())));
let results = coord.execute_best_effort();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| r.success));
assert!(results.iter().all(|r| r.error.is_none()));
}
#[test]
fn test_best_effort_partial_failure_continues() {
let committed = Arc::new(Mutex::new(Vec::new()));
let c0 = committed.clone();
let c1 = committed.clone();
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
move || {
c0.lock().unwrap().push("s0");
Ok(())
},
|| Ok(()),
));
coord.add_participant(ShardParticipant::new(
"s1",
|| Ok(()),
|| Err("commit failed".to_string()),
|| Ok(()),
));
coord.add_participant(ShardParticipant::new(
"s2",
|| Ok(()),
move || {
c1.lock().unwrap().push("s2");
Ok(())
},
|| Ok(()),
));
let results = coord.execute_best_effort();
assert_eq!(results.len(), 3);
assert!(results[0].success, "s0 should succeed");
assert!(!results[1].success, "s1 should fail");
assert!(results[2].success, "s2 should succeed even after s1 failed");
let committed = committed.lock().unwrap();
assert!(committed.contains(&"s0"));
assert!(committed.contains(&"s2"));
}
#[test]
fn test_best_effort_all_fail() {
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
|| Err("e0".to_string()),
|| Ok(()),
));
coord.add_participant(ShardParticipant::new(
"s1",
|| Ok(()),
|| Err("e1".to_string()),
|| Ok(()),
));
let results = coord.execute_best_effort();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| !r.success));
assert_eq!(results[0].error.as_deref(), Some("e0"));
assert_eq!(results[1].error.as_deref(), Some("e1"));
}
#[test]
fn test_best_effort_empty() {
let mut coord = ShardTransactionCoordinator::new();
let results = coord.execute_best_effort();
assert!(results.is_empty());
}
#[test]
fn test_best_effort_single() {
let mut coord = ShardTransactionCoordinator::new();
coord.add_participant(ShardParticipant::new(
"only",
|| Ok(()),
|| Ok(()),
|| Ok(()),
));
let results = coord.execute_best_effort();
assert_eq!(results.len(), 1);
assert!(results[0].success);
}
#[test]
fn test_best_effort_with_retries() {
let attempts = Arc::new(Mutex::new(0u32));
let a = attempts.clone();
let mut coord = ShardTransactionCoordinator::new().with_max_retries(2);
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
move || {
let mut g = a.lock().unwrap();
*g += 1;
if *g < 3 {
Err("retry".to_string())
} else {
Ok(())
}
},
|| Ok(()),
));
let results = coord.execute_best_effort();
assert_eq!(results.len(), 1);
assert!(
results[0].success,
"should succeed after 2 retries (3rd attempt)"
);
assert_eq!(*attempts.lock().unwrap(), 3);
}
#[test]
fn test_best_effort_retry_exhausted() {
let mut coord = ShardTransactionCoordinator::new().with_max_retries(2);
coord.add_participant(ShardParticipant::new(
"s0",
|| Ok(()),
|| Err("always fails".to_string()),
|| Ok(()),
));
let results = coord.execute_best_effort();
assert_eq!(results.len(), 1);
assert!(!results[0].success);
assert_eq!(results[0].error.as_deref(), Some("always fails"));
}
#[test]
fn test_shard_tx_error_display() {
let e1 = ShardTxError::PrepareFailed {
shard_id: "s0".to_string(),
reason: "boom".to_string(),
};
assert!(format!("{}", e1).contains("prepare failed on shard s0: boom"));
let e2 = ShardTxError::CommitFailed {
shard_id: "s1".to_string(),
reason: "kaboom".to_string(),
};
assert!(format!("{}", e2).contains("commit failed on shard s1: kaboom"));
let e3 = ShardTxError::RollbackFailed {
shard_id: "s2".to_string(),
reason: "oops".to_string(),
};
assert!(format!("{}", e3).contains("rollback failed on shard s2: oops"));
}
#[test]
fn test_coordinator_debug() {
let mut coord = ShardTransactionCoordinator::new().with_max_retries(3);
coord.add_participant(ShardParticipant::new("s0", || Ok(()), || Ok(()), || Ok(())));
let s = format!("{:?}", coord);
assert!(s.contains("ShardTransactionCoordinator"));
assert!(s.contains("participant_count"));
assert!(s.contains("max_retries"));
}
#[test]
fn test_participant_count() {
let mut coord = ShardTransactionCoordinator::new();
assert_eq!(coord.participant_count(), 0);
coord.add_participant(ShardParticipant::new("a", || Ok(()), || Ok(()), || Ok(())));
coord.add_participant(ShardParticipant::new("b", || Ok(()), || Ok(()), || Ok(())));
assert_eq!(coord.participant_count(), 2);
}
}