use crate::consensus::paxos::{PaxosProposer, ProposalValue};
use crate::error::{Result, TdbError};
use crate::transaction::three_phase_commit::{ThreePhaseCoordinator, TpcPhase};
use crate::transaction::two_phase_commit::{Participant, TpcState, TwoPhaseCoordinator};
use anyhow::Context;
use chrono::{DateTime, Duration, Utc};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CommitProtocol {
TwoPhase,
ThreePhase,
Paxos,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CoordinatorTxnState {
Active,
Preparing,
PreCommitting,
Committing,
Committed,
Aborting,
Aborted,
TimedOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionMetadata {
pub txn_id: String,
pub protocol: CommitProtocol,
pub state: CoordinatorTxnState,
pub participants: Vec<String>,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub timeout: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParticipantNode {
pub node_id: String,
pub endpoint: String,
pub last_heartbeat: DateTime<Utc>,
pub healthy: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorConfig {
pub default_timeout: Duration,
pub max_concurrent_transactions: usize,
pub heartbeat_interval: Duration,
pub participant_timeout: Duration,
pub auto_cleanup_enabled: bool,
pub cleanup_interval: Duration,
}
impl Default for CoordinatorConfig {
fn default() -> Self {
Self {
default_timeout: Duration::minutes(5),
max_concurrent_transactions: 1000,
heartbeat_interval: Duration::seconds(10),
participant_timeout: Duration::seconds(30),
auto_cleanup_enabled: true,
cleanup_interval: Duration::minutes(1),
}
}
}
pub struct TransactionCoordinator {
id: String,
config: CoordinatorConfig,
transactions: Arc<RwLock<HashMap<String, TransactionMetadata>>>,
participants: Arc<RwLock<HashMap<String, ParticipantNode>>>,
txn_counter: Arc<Mutex<u64>>,
stats: Arc<Mutex<CoordinatorStats>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CoordinatorStats {
pub total_transactions: u64,
pub successful_commits: u64,
pub aborted_transactions: u64,
pub timed_out_transactions: u64,
pub active_transactions: u64,
pub two_phase_count: u64,
pub three_phase_count: u64,
pub paxos_count: u64,
pub avg_transaction_duration_ms: f64,
total_duration_ms: f64,
}
impl TransactionCoordinator {
pub fn new(id: String, config: CoordinatorConfig) -> Self {
Self {
id,
config,
transactions: Arc::new(RwLock::new(HashMap::new())),
participants: Arc::new(RwLock::new(HashMap::new())),
txn_counter: Arc::new(Mutex::new(0)),
stats: Arc::new(Mutex::new(CoordinatorStats::default())),
}
}
pub async fn register_participant(&mut self, node_id: String, endpoint: String) -> Result<()> {
let participant = ParticipantNode {
node_id: node_id.clone(),
endpoint,
last_heartbeat: Utc::now(),
healthy: true,
};
self.participants.write().insert(node_id, participant);
Ok(())
}
pub async fn unregister_participant(&mut self, node_id: &str) -> Result<()> {
self.participants.write().remove(node_id);
Ok(())
}
pub async fn update_heartbeat(&self, node_id: &str) -> Result<()> {
let mut participants = self.participants.write();
if let Some(participant) = participants.get_mut(node_id) {
participant.last_heartbeat = Utc::now();
participant.healthy = true;
}
Ok(())
}
pub async fn check_participant_health(&self) -> Result<()> {
let now = Utc::now();
let timeout = self.config.participant_timeout;
let mut participants = self.participants.write();
for participant in participants.values_mut() {
let elapsed = now - participant.last_heartbeat;
if elapsed > timeout {
participant.healthy = false;
}
}
Ok(())
}
pub async fn begin_transaction(&mut self, protocol: CommitProtocol) -> Result<String> {
let active_count = self
.transactions
.read()
.values()
.filter(|t| t.state == CoordinatorTxnState::Active)
.count();
if active_count >= self.config.max_concurrent_transactions {
return Err(TdbError::Other(
"Maximum concurrent transactions limit reached".to_string(),
));
}
let txn_id = self.generate_txn_id();
let participants: Vec<String> = self
.participants
.read()
.values()
.filter(|p| p.healthy)
.map(|p| p.node_id.clone())
.collect();
if participants.is_empty() {
return Err(TdbError::Other(
"No healthy participants available".to_string(),
));
}
let metadata = TransactionMetadata {
txn_id: txn_id.clone(),
protocol,
state: CoordinatorTxnState::Active,
participants: participants.clone(),
start_time: Utc::now(),
end_time: None,
timeout: self.config.default_timeout,
};
self.transactions.write().insert(txn_id.clone(), metadata);
let mut stats = self.stats.lock();
stats.total_transactions += 1;
stats.active_transactions += 1;
match protocol {
CommitProtocol::TwoPhase => stats.two_phase_count += 1,
CommitProtocol::ThreePhase => stats.three_phase_count += 1,
CommitProtocol::Paxos => stats.paxos_count += 1,
}
Ok(txn_id)
}
pub async fn commit_transaction(&mut self, txn_id: &str) -> Result<bool> {
let metadata = {
let transactions = self.transactions.read();
transactions
.get(txn_id)
.cloned()
.ok_or_else(|| TdbError::Other(format!("Transaction not found: {}", txn_id)))?
};
if metadata.state != CoordinatorTxnState::Active {
return Err(TdbError::Other(format!(
"Transaction {} is not active (state: {:?})",
txn_id, metadata.state
)));
}
let result = match metadata.protocol {
CommitProtocol::TwoPhase => self.execute_two_phase_commit(&metadata).await?,
CommitProtocol::ThreePhase => self.execute_three_phase_commit(&metadata).await?,
CommitProtocol::Paxos => self.execute_paxos_commit(&metadata).await?,
};
let final_state = if result {
CoordinatorTxnState::Committed
} else {
CoordinatorTxnState::Aborted
};
self.update_transaction_state(txn_id, final_state).await?;
let duration = Utc::now()
.signed_duration_since(metadata.start_time)
.num_milliseconds() as f64;
let mut stats = self.stats.lock();
stats.active_transactions -= 1;
stats.total_duration_ms += duration;
if result {
stats.successful_commits += 1;
} else {
stats.aborted_transactions += 1;
}
stats.avg_transaction_duration_ms =
stats.total_duration_ms / stats.total_transactions as f64;
Ok(result)
}
pub async fn abort_transaction(&mut self, txn_id: &str) -> Result<()> {
self.update_transaction_state(txn_id, CoordinatorTxnState::Aborting)
.await?;
self.update_transaction_state(txn_id, CoordinatorTxnState::Aborted)
.await?;
let mut stats = self.stats.lock();
stats.active_transactions -= 1;
stats.aborted_transactions += 1;
Ok(())
}
async fn execute_two_phase_commit(&self, metadata: &TransactionMetadata) -> Result<bool> {
let mut coordinator = TwoPhaseCoordinator::new(metadata.txn_id.clone());
for node_id in &metadata.participants {
if let Some(participant) = self.participants.read().get(node_id) {
coordinator.add_participant(Participant {
node_id: participant.node_id.clone(),
endpoint: participant.endpoint.clone(),
});
}
}
coordinator.commit().await
}
async fn execute_three_phase_commit(&self, metadata: &TransactionMetadata) -> Result<bool> {
let mut coordinator = ThreePhaseCoordinator::new(metadata.txn_id.clone());
for node_id in &metadata.participants {
if let Some(participant) = self.participants.read().get(node_id) {
coordinator.add_participant(Participant {
node_id: participant.node_id.clone(),
endpoint: participant.endpoint.clone(),
});
}
}
coordinator.commit().await
}
async fn execute_paxos_commit(&self, metadata: &TransactionMetadata) -> Result<bool> {
let mut proposer = PaxosProposer::new(self.id.clone());
for node_id in &metadata.participants {
proposer.add_acceptor(node_id.clone());
}
let value = ProposalValue::TxnDecision {
txn_id: metadata.txn_id.clone(),
commit: true,
};
let result = proposer.propose(value).await?;
match result {
ProposalValue::TxnDecision { commit, .. } => Ok(commit),
_ => Ok(false),
}
}
async fn update_transaction_state(
&self,
txn_id: &str,
new_state: CoordinatorTxnState,
) -> Result<()> {
let mut transactions = self.transactions.write();
if let Some(metadata) = transactions.get_mut(txn_id) {
metadata.state = new_state;
if matches!(
new_state,
CoordinatorTxnState::Committed
| CoordinatorTxnState::Aborted
| CoordinatorTxnState::TimedOut
) {
metadata.end_time = Some(Utc::now());
}
}
Ok(())
}
fn generate_txn_id(&self) -> String {
let mut counter = self.txn_counter.lock();
*counter += 1;
format!("{}-txn-{:08x}", self.id, *counter)
}
pub fn get_transaction(&self, txn_id: &str) -> Option<TransactionMetadata> {
self.transactions.read().get(txn_id).cloned()
}
pub fn get_active_transactions(&self) -> Vec<TransactionMetadata> {
self.transactions
.read()
.values()
.filter(|t| t.state == CoordinatorTxnState::Active)
.cloned()
.collect()
}
pub async fn cleanup_completed_transactions(&self) -> Result<u64> {
let now = Utc::now();
let mut transactions = self.transactions.write();
let to_remove: Vec<String> = transactions
.iter()
.filter(|(_, metadata)| {
matches!(
metadata.state,
CoordinatorTxnState::Committed
| CoordinatorTxnState::Aborted
| CoordinatorTxnState::TimedOut
) && metadata
.end_time
.map(|end| now - end > self.config.cleanup_interval)
.unwrap_or(false)
})
.map(|(id, _)| id.clone())
.collect();
let count = to_remove.len() as u64;
for txn_id in to_remove {
transactions.remove(&txn_id);
}
Ok(count)
}
pub fn id(&self) -> &str {
&self.id
}
pub fn stats(&self) -> CoordinatorStats {
self.stats.lock().clone()
}
pub fn participant_count(&self) -> usize {
self.participants.read().len()
}
pub fn healthy_participant_count(&self) -> usize {
self.participants
.read()
.values()
.filter(|p| p.healthy)
.count()
}
pub fn active_transaction_count(&self) -> usize {
self.transactions
.read()
.values()
.filter(|t| t.state == CoordinatorTxnState::Active)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_coordinator_creation() {
let config = CoordinatorConfig::default();
let coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
assert_eq!(coordinator.id(), "coordinator-1");
assert_eq!(coordinator.participant_count(), 0);
assert_eq!(coordinator.active_transaction_count(), 0);
}
#[tokio::test]
async fn test_register_participants() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
coordinator
.register_participant("node2".to_string(), "http://node2:8080".to_string())
.await
.unwrap();
assert_eq!(coordinator.participant_count(), 2);
assert_eq!(coordinator.healthy_participant_count(), 2);
}
#[tokio::test]
async fn test_begin_transaction() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
let txn_id = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
assert!(txn_id.starts_with("coordinator-1-txn-"));
assert_eq!(coordinator.active_transaction_count(), 1);
let metadata = coordinator.get_transaction(&txn_id).unwrap();
assert_eq!(metadata.protocol, CommitProtocol::TwoPhase);
assert_eq!(metadata.state, CoordinatorTxnState::Active);
}
#[tokio::test]
async fn test_commit_transaction_2pc() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
coordinator
.register_participant("node2".to_string(), "http://node2:8080".to_string())
.await
.unwrap();
let txn_id = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
let result = coordinator.commit_transaction(&txn_id).await.unwrap();
assert!(result, "Transaction should commit successfully");
let metadata = coordinator.get_transaction(&txn_id).unwrap();
assert_eq!(metadata.state, CoordinatorTxnState::Committed);
let stats = coordinator.stats();
assert_eq!(stats.successful_commits, 1);
assert_eq!(stats.two_phase_count, 1);
}
#[tokio::test]
async fn test_commit_transaction_3pc() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
let txn_id = coordinator
.begin_transaction(CommitProtocol::ThreePhase)
.await
.unwrap();
let result = coordinator.commit_transaction(&txn_id).await.unwrap();
assert!(result);
let stats = coordinator.stats();
assert_eq!(stats.three_phase_count, 1);
}
#[tokio::test]
async fn test_commit_transaction_paxos() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
coordinator
.register_participant("node2".to_string(), "http://node2:8080".to_string())
.await
.unwrap();
coordinator
.register_participant("node3".to_string(), "http://node3:8080".to_string())
.await
.unwrap();
let txn_id = coordinator
.begin_transaction(CommitProtocol::Paxos)
.await
.unwrap();
let result = coordinator.commit_transaction(&txn_id).await.unwrap();
assert!(result);
let stats = coordinator.stats();
assert_eq!(stats.paxos_count, 1);
}
#[tokio::test]
async fn test_abort_transaction() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
let txn_id = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
coordinator.abort_transaction(&txn_id).await.unwrap();
let metadata = coordinator.get_transaction(&txn_id).unwrap();
assert_eq!(metadata.state, CoordinatorTxnState::Aborted);
let stats = coordinator.stats();
assert_eq!(stats.aborted_transactions, 1);
}
#[tokio::test]
async fn test_heartbeat_tracking() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
assert_eq!(coordinator.healthy_participant_count(), 1);
coordinator.update_heartbeat("node1").await.unwrap();
assert_eq!(coordinator.healthy_participant_count(), 1);
}
#[tokio::test]
async fn test_concurrent_transaction_limit() {
let config = CoordinatorConfig {
max_concurrent_transactions: 2,
..Default::default()
};
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
let result = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cleanup_completed_transactions() {
let config = CoordinatorConfig {
cleanup_interval: Duration::milliseconds(100),
..Default::default()
};
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
let txn_id = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
coordinator.commit_transaction(&txn_id).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
let cleaned = coordinator.cleanup_completed_transactions().await.unwrap();
assert_eq!(cleaned, 1);
}
#[tokio::test]
async fn test_get_active_transactions() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
let txn_id1 = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
let txn_id2 = coordinator
.begin_transaction(CommitProtocol::ThreePhase)
.await
.unwrap();
let active = coordinator.get_active_transactions();
assert_eq!(active.len(), 2);
coordinator.commit_transaction(&txn_id1).await.unwrap();
let active = coordinator.get_active_transactions();
assert_eq!(active.len(), 1);
assert_eq!(active[0].txn_id, txn_id2);
}
#[tokio::test]
async fn test_coordinator_stats() {
let config = CoordinatorConfig::default();
let mut coordinator = TransactionCoordinator::new("coordinator-1".to_string(), config);
coordinator
.register_participant("node1".to_string(), "http://node1:8080".to_string())
.await
.unwrap();
let txn1 = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
coordinator.commit_transaction(&txn1).await.unwrap();
let txn2 = coordinator
.begin_transaction(CommitProtocol::ThreePhase)
.await
.unwrap();
coordinator.commit_transaction(&txn2).await.unwrap();
let txn3 = coordinator
.begin_transaction(CommitProtocol::TwoPhase)
.await
.unwrap();
coordinator.abort_transaction(&txn3).await.unwrap();
let stats = coordinator.stats();
assert_eq!(stats.total_transactions, 3);
assert_eq!(stats.successful_commits, 2);
assert_eq!(stats.aborted_transactions, 1);
assert_eq!(stats.two_phase_count, 2);
assert_eq!(stats.three_phase_count, 1);
assert!(stats.avg_transaction_duration_ms > 0.0);
}
}