use crate::error::{Result, TdbError};
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, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ProposalNumber {
pub round: u64,
pub proposer_id: u64,
}
impl ProposalNumber {
pub fn new(round: u64, proposer_id: u64) -> Self {
Self { round, proposer_id }
}
pub fn next_round(&self) -> Self {
Self {
round: self.round + 1,
proposer_id: self.proposer_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProposalValue {
Noop,
Data(Vec<u8>),
ConfigChange(String),
TxnDecision {
txn_id: String,
commit: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Proposal {
pub number: ProposalNumber,
pub value: ProposalValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Promise {
pub acceptor_id: String,
pub promised_number: ProposalNumber,
pub accepted_proposal: Option<Proposal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcceptResponse {
pub acceptor_id: String,
pub accepted_number: ProposalNumber,
pub accepted: bool,
}
pub struct PaxosProposer {
id: String,
numeric_id: u64,
current_round: Arc<Mutex<u64>>,
acceptors: Arc<Mutex<HashSet<String>>>,
promises: Arc<Mutex<HashMap<String, Promise>>>,
stats: Arc<Mutex<PaxosProposerStats>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PaxosProposerStats {
pub total_proposals: u64,
pub successful_consensus: u64,
pub failed_proposals: u64,
pub avg_rounds_to_consensus: f64,
total_rounds: u64,
}
impl PaxosProposer {
pub fn new(id: String) -> Self {
let numeric_id = Self::hash_id(&id);
Self {
id,
numeric_id,
current_round: Arc::new(Mutex::new(0)),
acceptors: Arc::new(Mutex::new(HashSet::new())),
promises: Arc::new(Mutex::new(HashMap::new())),
stats: Arc::new(Mutex::new(PaxosProposerStats::default())),
}
}
fn hash_id(id: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
pub fn add_acceptor(&mut self, acceptor_id: String) {
self.acceptors.lock().insert(acceptor_id);
}
fn majority_count(&self) -> usize {
let total = self.acceptors.lock().len();
(total / 2) + 1
}
pub async fn propose(&mut self, initial_value: ProposalValue) -> Result<ProposalValue> {
{
let mut stats = self.stats.lock();
stats.total_proposals += 1;
}
let mut value = initial_value;
let mut rounds = 0;
const MAX_ROUNDS: u32 = 10;
for _ in 0..MAX_ROUNDS {
rounds += 1;
let proposal_number = self.next_proposal_number();
let prepare_result = self.prepare_phase(proposal_number).await?;
if !prepare_result.0 {
continue;
}
if let Some(prev_value) = prepare_result.1 {
value = prev_value;
}
let accept_result = self.accept_phase(proposal_number, value.clone()).await?;
if accept_result {
let mut stats = self.stats.lock();
stats.successful_consensus += 1;
stats.total_rounds += rounds;
stats.avg_rounds_to_consensus =
stats.total_rounds as f64 / stats.successful_consensus as f64;
return Ok(value);
}
}
let mut stats = self.stats.lock();
stats.failed_proposals += 1;
Err(TdbError::Other(format!(
"Failed to achieve consensus after {} rounds",
MAX_ROUNDS
)))
}
fn next_proposal_number(&self) -> ProposalNumber {
let mut round = self.current_round.lock();
*round += 1;
ProposalNumber::new(*round, self.numeric_id)
}
async fn prepare_phase(
&self,
proposal_number: ProposalNumber,
) -> Result<(bool, Option<ProposalValue>)> {
self.promises.lock().clear();
let acceptors = self.acceptors.lock().clone();
let majority = self.majority_count();
for acceptor_id in acceptors.iter() {
let promise = self.send_prepare(acceptor_id, proposal_number).await?;
self.promises.lock().insert(acceptor_id.clone(), promise);
}
let promises = self.promises.lock();
if promises.len() < majority {
return Ok((false, None));
}
let highest_accepted = promises
.values()
.filter_map(|p| p.accepted_proposal.as_ref())
.max_by_key(|p| p.number);
let previous_value = highest_accepted.map(|p| p.value.clone());
Ok((true, previous_value))
}
async fn send_prepare(
&self,
acceptor_id: &str,
proposal_number: ProposalNumber,
) -> Result<Promise> {
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(Promise {
acceptor_id: acceptor_id.to_string(),
promised_number: proposal_number,
accepted_proposal: None,
})
}
async fn accept_phase(
&self,
proposal_number: ProposalNumber,
value: ProposalValue,
) -> Result<bool> {
let acceptors = self.acceptors.lock().clone();
let majority = self.majority_count();
let mut accept_count = 0;
for acceptor_id in acceptors.iter() {
let response = self
.send_accept(acceptor_id, proposal_number, value.clone())
.await?;
if response.accepted {
accept_count += 1;
}
}
Ok(accept_count >= majority)
}
async fn send_accept(
&self,
acceptor_id: &str,
proposal_number: ProposalNumber,
_value: ProposalValue,
) -> Result<AcceptResponse> {
tokio::time::sleep(Duration::from_millis(5)).await;
Ok(AcceptResponse {
acceptor_id: acceptor_id.to_string(),
accepted_number: proposal_number,
accepted: true,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn stats(&self) -> PaxosProposerStats {
self.stats.lock().clone()
}
pub fn acceptor_count(&self) -> usize {
self.acceptors.lock().len()
}
}
pub struct PaxosAcceptor {
id: String,
promised_number: Arc<RwLock<Option<ProposalNumber>>>,
accepted_proposal: Arc<RwLock<Option<Proposal>>>,
stats: Arc<Mutex<PaxosAcceptorStats>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PaxosAcceptorStats {
pub total_prepares: u64,
pub total_accepts: u64,
pub total_promises: u64,
pub total_accepted: u64,
pub total_rejections: u64,
}
impl PaxosAcceptor {
pub fn new(id: String) -> Self {
Self {
id,
promised_number: Arc::new(RwLock::new(None)),
accepted_proposal: Arc::new(RwLock::new(None)),
stats: Arc::new(Mutex::new(PaxosAcceptorStats::default())),
}
}
pub async fn handle_prepare(&self, proposal_number: ProposalNumber) -> Result<Option<Promise>> {
let mut stats = self.stats.lock();
stats.total_prepares += 1;
drop(stats);
let mut promised = self.promised_number.write();
if let Some(current_promised) = *promised {
if proposal_number <= current_promised {
let mut stats = self.stats.lock();
stats.total_rejections += 1;
return Ok(None);
}
}
*promised = Some(proposal_number);
let mut stats = self.stats.lock();
stats.total_promises += 1;
drop(stats);
let accepted = self.accepted_proposal.read().clone();
Ok(Some(Promise {
acceptor_id: self.id.clone(),
promised_number: proposal_number,
accepted_proposal: accepted,
}))
}
pub async fn handle_accept(&self, proposal: Proposal) -> Result<bool> {
let mut stats = self.stats.lock();
stats.total_accepts += 1;
drop(stats);
let promised = self.promised_number.read();
if let Some(promised_num) = *promised {
if proposal.number < promised_num {
let mut stats = self.stats.lock();
stats.total_rejections += 1;
return Ok(false);
}
}
drop(promised);
*self.accepted_proposal.write() = Some(proposal);
let mut stats = self.stats.lock();
stats.total_accepted += 1;
Ok(true)
}
pub fn id(&self) -> &str {
&self.id
}
pub fn accepted_proposal(&self) -> Option<Proposal> {
self.accepted_proposal.read().clone()
}
pub fn stats(&self) -> PaxosAcceptorStats {
self.stats.lock().clone()
}
}
pub struct PaxosLearner {
id: String,
accepted_proposals: Arc<Mutex<HashMap<String, Proposal>>>,
learned_value: Arc<RwLock<Option<ProposalValue>>>,
required_acceptors: usize,
}
impl PaxosLearner {
pub fn new(id: String, total_acceptors: usize) -> Self {
let required_acceptors = (total_acceptors / 2) + 1;
Self {
id,
accepted_proposals: Arc::new(Mutex::new(HashMap::new())),
learned_value: Arc::new(RwLock::new(None)),
required_acceptors,
}
}
pub async fn learn_from_acceptor(
&self,
acceptor_id: String,
proposal: Proposal,
) -> Result<Option<ProposalValue>> {
let mut accepted = self.accepted_proposals.lock();
accepted.insert(acceptor_id, proposal.clone());
let count = accepted
.values()
.filter(|p| p.number == proposal.number && p.value == proposal.value)
.count();
if count >= self.required_acceptors {
*self.learned_value.write() = Some(proposal.value.clone());
return Ok(Some(proposal.value));
}
Ok(None)
}
pub fn id(&self) -> &str {
&self.id
}
pub fn learned_value(&self) -> Option<ProposalValue> {
self.learned_value.read().clone()
}
pub fn has_learned(&self) -> bool {
self.learned_value.read().is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proposal_number_ordering() {
let p1 = ProposalNumber::new(1, 100);
let p2 = ProposalNumber::new(2, 100);
let p3 = ProposalNumber::new(2, 101);
assert!(p1 < p2);
assert!(p2 < p3);
assert!(p1 < p3);
}
#[test]
fn test_proposal_number_next_round() {
let p1 = ProposalNumber::new(5, 100);
let p2 = p1.next_round();
assert_eq!(p2.round, 6);
assert_eq!(p2.proposer_id, 100);
}
#[tokio::test]
async fn test_paxos_proposer_creation() {
let proposer = PaxosProposer::new("proposer-1".to_string());
assert_eq!(proposer.id(), "proposer-1");
assert_eq!(proposer.acceptor_count(), 0);
}
#[tokio::test]
async fn test_paxos_proposer_add_acceptors() {
let mut proposer = PaxosProposer::new("proposer-1".to_string());
proposer.add_acceptor("acceptor-1".to_string());
proposer.add_acceptor("acceptor-2".to_string());
proposer.add_acceptor("acceptor-3".to_string());
assert_eq!(proposer.acceptor_count(), 3);
assert_eq!(proposer.majority_count(), 2);
}
#[tokio::test]
async fn test_paxos_proposer_successful_consensus() {
let mut proposer = PaxosProposer::new("proposer-1".to_string());
proposer.add_acceptor("acceptor-1".to_string());
proposer.add_acceptor("acceptor-2".to_string());
proposer.add_acceptor("acceptor-3".to_string());
let value = ProposalValue::Data(vec![1, 2, 3, 4]);
let result = proposer.propose(value.clone()).await.unwrap();
assert_eq!(result, value);
let stats = proposer.stats();
assert_eq!(stats.successful_consensus, 1);
assert_eq!(stats.total_proposals, 1);
}
#[tokio::test]
async fn test_paxos_acceptor_creation() {
let acceptor = PaxosAcceptor::new("acceptor-1".to_string());
assert_eq!(acceptor.id(), "acceptor-1");
assert!(acceptor.accepted_proposal().is_none());
}
#[tokio::test]
async fn test_paxos_acceptor_handle_prepare() {
let acceptor = PaxosAcceptor::new("acceptor-1".to_string());
let proposal_number = ProposalNumber::new(1, 100);
let promise = acceptor
.handle_prepare(proposal_number)
.await
.unwrap()
.unwrap();
assert_eq!(promise.acceptor_id, "acceptor-1");
assert_eq!(promise.promised_number, proposal_number);
assert!(promise.accepted_proposal.is_none());
let stats = acceptor.stats();
assert_eq!(stats.total_prepares, 1);
assert_eq!(stats.total_promises, 1);
}
#[tokio::test]
async fn test_paxos_acceptor_reject_lower_prepare() {
let acceptor = PaxosAcceptor::new("acceptor-1".to_string());
let p1 = ProposalNumber::new(5, 100);
acceptor.handle_prepare(p1).await.unwrap();
let p2 = ProposalNumber::new(3, 100);
let result = acceptor.handle_prepare(p2).await.unwrap();
assert!(result.is_none(), "Should reject lower proposal number");
let stats = acceptor.stats();
assert_eq!(stats.total_rejections, 1);
}
#[tokio::test]
async fn test_paxos_acceptor_handle_accept() {
let acceptor = PaxosAcceptor::new("acceptor-1".to_string());
let proposal_number = ProposalNumber::new(1, 100);
acceptor.handle_prepare(proposal_number).await.unwrap();
let proposal = Proposal {
number: proposal_number,
value: ProposalValue::Data(vec![1, 2, 3]),
};
let accepted = acceptor.handle_accept(proposal.clone()).await.unwrap();
assert!(accepted);
let stats = acceptor.stats();
assert_eq!(stats.total_accepts, 1);
assert_eq!(stats.total_accepted, 1);
assert_eq!(acceptor.accepted_proposal().unwrap().value, proposal.value);
}
#[tokio::test]
async fn test_paxos_acceptor_reject_lower_accept() {
let acceptor = PaxosAcceptor::new("acceptor-1".to_string());
let p1 = ProposalNumber::new(5, 100);
acceptor.handle_prepare(p1).await.unwrap();
let p2 = ProposalNumber::new(3, 100);
let proposal = Proposal {
number: p2,
value: ProposalValue::Data(vec![1, 2, 3]),
};
let accepted = acceptor.handle_accept(proposal).await.unwrap();
assert!(!accepted, "Should reject lower proposal number");
}
#[tokio::test]
async fn test_paxos_learner_creation() {
let learner = PaxosLearner::new("learner-1".to_string(), 3);
assert_eq!(learner.id(), "learner-1");
assert!(!learner.has_learned());
assert_eq!(learner.required_acceptors, 2);
}
#[tokio::test]
async fn test_paxos_learner_learn_from_majority() {
let learner = PaxosLearner::new("learner-1".to_string(), 3);
let proposal_number = ProposalNumber::new(1, 100);
let value = ProposalValue::Data(vec![1, 2, 3]);
let proposal = Proposal {
number: proposal_number,
value: value.clone(),
};
let result = learner
.learn_from_acceptor("acceptor-1".to_string(), proposal.clone())
.await
.unwrap();
assert!(result.is_none(), "Should need more acceptors");
let result = learner
.learn_from_acceptor("acceptor-2".to_string(), proposal.clone())
.await
.unwrap();
assert!(result.is_some(), "Should learn value with majority");
assert_eq!(result.unwrap(), value);
assert!(learner.has_learned());
assert_eq!(learner.learned_value().unwrap(), value);
}
#[tokio::test]
async fn test_proposal_value_types() {
let noop = ProposalValue::Noop;
let data = ProposalValue::Data(vec![1, 2, 3]);
let config = ProposalValue::ConfigChange("new-config".to_string());
let txn = ProposalValue::TxnDecision {
txn_id: "txn-001".to_string(),
commit: true,
};
assert_ne!(noop, data);
assert_ne!(data, config);
assert_ne!(config, txn);
}
#[tokio::test]
async fn test_paxos_proposer_stats() {
let mut proposer = PaxosProposer::new("proposer-1".to_string());
proposer.add_acceptor("acceptor-1".to_string());
proposer.add_acceptor("acceptor-2".to_string());
proposer.add_acceptor("acceptor-3".to_string());
let value1 = ProposalValue::Data(vec![1, 2, 3]);
proposer.propose(value1).await.unwrap();
let value2 = ProposalValue::Data(vec![4, 5, 6]);
proposer.propose(value2).await.unwrap();
let stats = proposer.stats();
assert_eq!(stats.total_proposals, 2);
assert_eq!(stats.successful_consensus, 2);
assert!(stats.avg_rounds_to_consensus > 0.0);
}
}