use crate::error::{Result, TdbError};
use crate::transaction::two_phase_commit::Participant;
use anyhow::Context;
use chrono::{DateTime, Utc};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TpcPhase {
Init,
CanCommit,
WaitingCanCommit,
PreCommit,
WaitingPreCommit,
DoCommit,
WaitingCommit,
Committed,
Aborting,
Aborted,
TimedOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CanCommitResponse {
Yes,
No,
Timeout,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ParticipantState {
participant: Participant,
can_commit_response: Option<CanCommitResponse>,
pre_commit_acked: bool,
commit_acked: bool,
can_commit_timestamp: Option<DateTime<Utc>>,
pre_commit_timestamp: Option<DateTime<Utc>>,
commit_timestamp: Option<DateTime<Utc>>,
}
pub struct ThreePhaseCoordinator {
txn_id: String,
phase: Arc<RwLock<TpcPhase>>,
participants: Arc<Mutex<HashMap<String, ParticipantState>>>,
start_time: DateTime<Utc>,
can_commit_timeout: Duration,
pre_commit_timeout: Duration,
do_commit_timeout: Duration,
stats: Arc<Mutex<ThreePhaseStats>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ThreePhaseStats {
pub total_transactions: u64,
pub successful_commits: u64,
pub aborted_transactions: u64,
pub timed_out_transactions: u64,
pub avg_can_commit_duration_ms: f64,
pub avg_pre_commit_duration_ms: f64,
pub avg_do_commit_duration_ms: f64,
total_can_commit_duration_ms: f64,
total_pre_commit_duration_ms: f64,
total_do_commit_duration_ms: f64,
}
impl ThreePhaseCoordinator {
pub fn new(txn_id: String) -> Self {
Self {
txn_id,
phase: Arc::new(RwLock::new(TpcPhase::Init)),
participants: Arc::new(Mutex::new(HashMap::new())),
start_time: Utc::now(),
can_commit_timeout: Duration::from_secs(20),
pre_commit_timeout: Duration::from_secs(30),
do_commit_timeout: Duration::from_secs(60),
stats: Arc::new(Mutex::new(ThreePhaseStats::default())),
}
}
pub fn add_participant(&mut self, participant: Participant) {
let mut participants = self.participants.lock();
participants.insert(
participant.node_id.clone(),
ParticipantState {
participant,
can_commit_response: None,
pre_commit_acked: false,
commit_acked: false,
can_commit_timestamp: None,
pre_commit_timestamp: None,
commit_timestamp: None,
},
);
}
pub fn set_timeouts(
&mut self,
can_commit: Duration,
pre_commit: Duration,
do_commit: Duration,
) {
self.can_commit_timeout = can_commit;
self.pre_commit_timeout = pre_commit;
self.do_commit_timeout = do_commit;
}
pub async fn commit(&mut self) -> Result<bool> {
{
let mut stats = self.stats.lock();
stats.total_transactions += 1;
}
let can_commit_start = Utc::now();
let can_commit_result = self.can_commit_phase().await?;
let can_commit_duration = (Utc::now() - can_commit_start).num_milliseconds() as f64;
{
let mut stats = self.stats.lock();
stats.total_can_commit_duration_ms += can_commit_duration;
stats.avg_can_commit_duration_ms =
stats.total_can_commit_duration_ms / stats.total_transactions as f64;
}
if !can_commit_result {
self.abort_phase().await?;
{
let mut stats = self.stats.lock();
stats.aborted_transactions += 1;
}
return Ok(false);
}
let pre_commit_start = Utc::now();
let pre_commit_result = self.pre_commit_phase().await?;
let pre_commit_duration = (Utc::now() - pre_commit_start).num_milliseconds() as f64;
{
let mut stats = self.stats.lock();
stats.total_pre_commit_duration_ms += pre_commit_duration;
stats.avg_pre_commit_duration_ms =
stats.total_pre_commit_duration_ms / stats.total_transactions as f64;
}
if !pre_commit_result {
self.abort_phase().await?;
{
let mut stats = self.stats.lock();
stats.aborted_transactions += 1;
}
return Ok(false);
}
let do_commit_start = Utc::now();
let do_commit_result = self.do_commit_phase().await?;
let do_commit_duration = (Utc::now() - do_commit_start).num_milliseconds() as f64;
{
let mut stats = self.stats.lock();
stats.total_do_commit_duration_ms += do_commit_duration;
stats.avg_do_commit_duration_ms =
stats.total_do_commit_duration_ms / stats.total_transactions as f64;
if do_commit_result {
stats.successful_commits += 1;
} else {
stats.aborted_transactions += 1;
}
}
Ok(do_commit_result)
}
async fn can_commit_phase(&mut self) -> Result<bool> {
*self.phase.write() = TpcPhase::CanCommit;
let participants = self.participants.lock().clone();
let responses = self.send_can_commit_messages(&participants).await?;
*self.phase.write() = TpcPhase::WaitingCanCommit;
let all_yes = responses
.iter()
.all(|(_, response)| *response == CanCommitResponse::Yes);
Ok(all_yes)
}
async fn send_can_commit_messages(
&self,
participants: &HashMap<String, ParticipantState>,
) -> Result<HashMap<String, CanCommitResponse>> {
let mut responses = HashMap::new();
for (node_id, _state) in participants.iter() {
let response = self.request_can_commit(node_id).await?;
responses.insert(node_id.clone(), response);
let mut participants = self.participants.lock();
if let Some(pstate) = participants.get_mut(node_id) {
pstate.can_commit_response = Some(response);
pstate.can_commit_timestamp = Some(Utc::now());
}
}
Ok(responses)
}
async fn request_can_commit(&self, _node_id: &str) -> Result<CanCommitResponse> {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(CanCommitResponse::Yes)
}
async fn pre_commit_phase(&mut self) -> Result<bool> {
*self.phase.write() = TpcPhase::PreCommit;
let participants = self.participants.lock().clone();
self.send_pre_commit_messages(&participants).await?;
*self.phase.write() = TpcPhase::WaitingPreCommit;
Ok(true)
}
async fn send_pre_commit_messages(
&self,
participants: &HashMap<String, ParticipantState>,
) -> Result<()> {
for (node_id, _state) in participants.iter() {
self.send_pre_commit_message(node_id).await?;
let mut participants = self.participants.lock();
if let Some(pstate) = participants.get_mut(node_id) {
pstate.pre_commit_acked = true;
pstate.pre_commit_timestamp = Some(Utc::now());
}
}
Ok(())
}
async fn send_pre_commit_message(&self, _node_id: &str) -> Result<()> {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(())
}
async fn do_commit_phase(&mut self) -> Result<bool> {
*self.phase.write() = TpcPhase::DoCommit;
let participants = self.participants.lock().clone();
self.send_do_commit_messages(&participants).await?;
*self.phase.write() = TpcPhase::Committed;
Ok(true)
}
async fn send_do_commit_messages(
&self,
participants: &HashMap<String, ParticipantState>,
) -> Result<()> {
for (node_id, _state) in participants.iter() {
self.send_do_commit_message(node_id).await?;
let mut participants = self.participants.lock();
if let Some(pstate) = participants.get_mut(node_id) {
pstate.commit_acked = true;
pstate.commit_timestamp = Some(Utc::now());
}
}
Ok(())
}
async fn send_do_commit_message(&self, _node_id: &str) -> Result<()> {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(())
}
async fn abort_phase(&mut self) -> Result<()> {
*self.phase.write() = TpcPhase::Aborting;
let participants = self.participants.lock().clone();
self.send_abort_messages(&participants).await?;
*self.phase.write() = TpcPhase::Aborted;
Ok(())
}
async fn send_abort_messages(
&self,
participants: &HashMap<String, ParticipantState>,
) -> Result<()> {
for (node_id, _state) in participants.iter() {
self.send_abort_message(node_id).await?;
}
Ok(())
}
async fn send_abort_message(&self, _node_id: &str) -> Result<()> {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(())
}
pub fn phase(&self) -> TpcPhase {
*self.phase.read()
}
pub fn txn_id(&self) -> &str {
&self.txn_id
}
pub fn stats(&self) -> ThreePhaseStats {
self.stats.lock().clone()
}
pub fn participant_count(&self) -> usize {
self.participants.lock().len()
}
pub fn get_can_commit_responses(&self) -> HashMap<String, Option<CanCommitResponse>> {
self.participants
.lock()
.iter()
.map(|(node_id, state)| (node_id.clone(), state.can_commit_response))
.collect()
}
}
pub struct ThreePhaseParticipant {
node_id: String,
phase: Arc<RwLock<TpcPhase>>,
active_txns: Arc<Mutex<HashSet<String>>>,
pre_committed_txns: Arc<Mutex<HashSet<String>>>,
stats: Arc<Mutex<ThreePhaseParticipantStats>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ThreePhaseParticipantStats {
pub total_can_commit_requests: u64,
pub total_pre_commit_messages: u64,
pub total_commits: u64,
pub total_aborts: u64,
pub yes_responses: u64,
pub no_responses: u64,
}
impl ThreePhaseParticipant {
pub fn new(node_id: String) -> Self {
Self {
node_id,
phase: Arc::new(RwLock::new(TpcPhase::Init)),
active_txns: Arc::new(Mutex::new(HashSet::new())),
pre_committed_txns: Arc::new(Mutex::new(HashSet::new())),
stats: Arc::new(Mutex::new(ThreePhaseParticipantStats::default())),
}
}
pub async fn handle_can_commit(&self, txn_id: String) -> Result<CanCommitResponse> {
{
let mut stats = self.stats.lock();
stats.total_can_commit_requests += 1;
}
*self.phase.write() = TpcPhase::CanCommit;
let can_commit = self.can_commit(&txn_id).await?;
let response = if can_commit {
self.active_txns.lock().insert(txn_id.clone());
let mut stats = self.stats.lock();
stats.yes_responses += 1;
drop(stats);
CanCommitResponse::Yes
} else {
let mut stats = self.stats.lock();
stats.no_responses += 1;
drop(stats);
CanCommitResponse::No
};
Ok(response)
}
async fn can_commit(&self, _txn_id: &str) -> Result<bool> {
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(true)
}
pub async fn handle_pre_commit(&self, txn_id: String) -> Result<()> {
{
let mut stats = self.stats.lock();
stats.total_pre_commit_messages += 1;
}
*self.phase.write() = TpcPhase::PreCommit;
self.pre_committed_txns.lock().insert(txn_id.clone());
self.execute_pre_commit(&txn_id).await?;
*self.phase.write() = TpcPhase::WaitingCommit;
Ok(())
}
async fn execute_pre_commit(&self, _txn_id: &str) -> Result<()> {
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(())
}
pub async fn handle_do_commit(&self, txn_id: String) -> Result<()> {
*self.phase.write() = TpcPhase::DoCommit;
self.execute_commit(&txn_id).await?;
*self.phase.write() = TpcPhase::Committed;
self.active_txns.lock().remove(&txn_id);
self.pre_committed_txns.lock().remove(&txn_id);
let mut stats = self.stats.lock();
stats.total_commits += 1;
Ok(())
}
async fn execute_commit(&self, _txn_id: &str) -> Result<()> {
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(())
}
pub async fn handle_abort(&self, txn_id: String) -> Result<()> {
*self.phase.write() = TpcPhase::Aborting;
self.execute_abort(&txn_id).await?;
*self.phase.write() = TpcPhase::Aborted;
self.active_txns.lock().remove(&txn_id);
self.pre_committed_txns.lock().remove(&txn_id);
let mut stats = self.stats.lock();
stats.total_aborts += 1;
Ok(())
}
async fn execute_abort(&self, _txn_id: &str) -> Result<()> {
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(())
}
pub async fn handle_coordinator_timeout(&self, txn_id: String) -> Result<bool> {
let is_pre_committed = self.pre_committed_txns.lock().contains(&txn_id);
if is_pre_committed {
self.handle_do_commit(txn_id).await?;
Ok(true)
} else {
self.handle_abort(txn_id).await?;
Ok(false)
}
}
pub fn phase(&self) -> TpcPhase {
*self.phase.read()
}
pub fn node_id(&self) -> &str {
&self.node_id
}
pub fn stats(&self) -> ThreePhaseParticipantStats {
self.stats.lock().clone()
}
pub fn active_txn_count(&self) -> usize {
self.active_txns.lock().len()
}
pub fn pre_committed_txn_count(&self) -> usize {
self.pre_committed_txns.lock().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_3pc_coordinator_creation() {
let coordinator = ThreePhaseCoordinator::new("txn-001".to_string());
assert_eq!(coordinator.txn_id(), "txn-001");
assert_eq!(coordinator.phase(), TpcPhase::Init);
assert_eq!(coordinator.participant_count(), 0);
}
#[tokio::test]
async fn test_3pc_successful_commit() {
let mut coordinator = ThreePhaseCoordinator::new("txn-002".to_string());
coordinator.add_participant(Participant {
node_id: "node1".to_string(),
endpoint: "http://node1:8080".to_string(),
});
coordinator.add_participant(Participant {
node_id: "node2".to_string(),
endpoint: "http://node2:8080".to_string(),
});
let result = coordinator.commit().await.unwrap();
assert!(result, "Transaction should commit successfully");
assert_eq!(coordinator.phase(), TpcPhase::Committed);
let stats = coordinator.stats();
assert_eq!(stats.successful_commits, 1);
assert_eq!(stats.total_transactions, 1);
}
#[tokio::test]
async fn test_3pc_coordinator_stats() {
let mut coordinator = ThreePhaseCoordinator::new("txn-003".to_string());
coordinator.add_participant(Participant {
node_id: "node1".to_string(),
endpoint: "http://node1:8080".to_string(),
});
coordinator.commit().await.unwrap();
let stats = coordinator.stats();
assert_eq!(stats.total_transactions, 1);
assert!(stats.avg_can_commit_duration_ms > 0.0);
assert!(stats.avg_pre_commit_duration_ms > 0.0);
assert!(stats.avg_do_commit_duration_ms > 0.0);
}
#[tokio::test]
async fn test_3pc_participant_creation() {
let participant = ThreePhaseParticipant::new("node1".to_string());
assert_eq!(participant.node_id(), "node1");
assert_eq!(participant.phase(), TpcPhase::Init);
assert_eq!(participant.active_txn_count(), 0);
}
#[tokio::test]
async fn test_3pc_participant_can_commit() {
let participant = ThreePhaseParticipant::new("node1".to_string());
let response = participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
assert_eq!(response, CanCommitResponse::Yes);
assert_eq!(participant.active_txn_count(), 1);
let stats = participant.stats();
assert_eq!(stats.total_can_commit_requests, 1);
assert_eq!(stats.yes_responses, 1);
}
#[tokio::test]
async fn test_3pc_participant_pre_commit() {
let participant = ThreePhaseParticipant::new("node1".to_string());
participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_pre_commit("txn-001".to_string())
.await
.unwrap();
assert_eq!(participant.pre_committed_txn_count(), 1);
let stats = participant.stats();
assert_eq!(stats.total_pre_commit_messages, 1);
}
#[tokio::test]
async fn test_3pc_participant_commit() {
let participant = ThreePhaseParticipant::new("node1".to_string());
participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_pre_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_do_commit("txn-001".to_string())
.await
.unwrap();
assert_eq!(participant.phase(), TpcPhase::Committed);
assert_eq!(participant.active_txn_count(), 0);
assert_eq!(participant.pre_committed_txn_count(), 0);
let stats = participant.stats();
assert_eq!(stats.total_commits, 1);
}
#[tokio::test]
async fn test_3pc_participant_abort() {
let participant = ThreePhaseParticipant::new("node1".to_string());
participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_abort("txn-001".to_string())
.await
.unwrap();
assert_eq!(participant.phase(), TpcPhase::Aborted);
assert_eq!(participant.active_txn_count(), 0);
let stats = participant.stats();
assert_eq!(stats.total_aborts, 1);
}
#[tokio::test]
async fn test_3pc_coordinator_timeout() {
let mut coordinator = ThreePhaseCoordinator::new("txn-004".to_string());
coordinator.set_timeouts(
Duration::from_secs(5),
Duration::from_secs(10),
Duration::from_secs(15),
);
assert_eq!(coordinator.can_commit_timeout, Duration::from_secs(5));
assert_eq!(coordinator.pre_commit_timeout, Duration::from_secs(10));
assert_eq!(coordinator.do_commit_timeout, Duration::from_secs(15));
}
#[tokio::test]
async fn test_3pc_participant_coordinator_timeout_pre_committed() {
let participant = ThreePhaseParticipant::new("node1".to_string());
participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_pre_commit("txn-001".to_string())
.await
.unwrap();
let can_commit = participant
.handle_coordinator_timeout("txn-001".to_string())
.await
.unwrap();
assert!(
can_commit,
"Pre-committed transaction should commit on timeout"
);
assert_eq!(participant.phase(), TpcPhase::Committed);
}
#[tokio::test]
async fn test_3pc_participant_coordinator_timeout_not_pre_committed() {
let participant = ThreePhaseParticipant::new("node1".to_string());
participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
let can_commit = participant
.handle_coordinator_timeout("txn-001".to_string())
.await
.unwrap();
assert!(
!can_commit,
"Non-pre-committed transaction should abort on timeout"
);
assert_eq!(participant.phase(), TpcPhase::Aborted);
}
#[tokio::test]
async fn test_3pc_multiple_transactions() {
let participant = ThreePhaseParticipant::new("node1".to_string());
participant
.handle_can_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_pre_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_do_commit("txn-001".to_string())
.await
.unwrap();
participant
.handle_can_commit("txn-002".to_string())
.await
.unwrap();
participant
.handle_pre_commit("txn-002".to_string())
.await
.unwrap();
participant
.handle_do_commit("txn-002".to_string())
.await
.unwrap();
let stats = participant.stats();
assert_eq!(stats.total_commits, 2);
assert_eq!(stats.total_can_commit_requests, 2);
assert_eq!(stats.total_pre_commit_messages, 2);
}
#[tokio::test]
async fn test_3pc_get_can_commit_responses() {
let mut coordinator = ThreePhaseCoordinator::new("txn-005".to_string());
coordinator.add_participant(Participant {
node_id: "node1".to_string(),
endpoint: "http://node1:8080".to_string(),
});
let responses_before = coordinator.get_can_commit_responses();
assert_eq!(responses_before.get("node1").unwrap(), &None);
coordinator.commit().await.unwrap();
let responses_after = coordinator.get_can_commit_responses();
assert_eq!(
responses_after.get("node1").unwrap(),
&Some(CanCommitResponse::Yes)
);
}
}