Skip to main content

oxirs_tdb/transaction/
three_phase_commit.rs

1//! Three-Phase Commit (3PC) Protocol Implementation
2//!
3//! This module implements the Three-Phase Commit protocol, an extension of 2PC that eliminates
4//! blocking in certain failure scenarios by adding a pre-commit phase.
5//!
6//! # Protocol Overview
7//!
8//! ## Phase 1: Can-Commit Phase (Prepare)
9//! 1. Coordinator asks all participants if they can commit
10//! 2. Participants vote YES (can commit) or NO (cannot commit)
11//! 3. If any participant votes NO, the protocol aborts
12//!
13//! ## Phase 2: Pre-Commit Phase (NEW - Not in 2PC)
14//! 1. Coordinator sends PRE-COMMIT to all participants
15//! 2. Participants acknowledge they are ready to commit
16//! 3. This phase ensures all participants know the decision before final commit
17//!
18//! ## Phase 3: Do-Commit Phase
19//! 1. Coordinator sends DO-COMMIT to all participants
20//! 2. Participants commit the transaction
21//! 3. Participants acknowledge completion
22//!
23//! # Advantages over 2PC
24//!
25//! - **Non-blocking in Network Partitions**: If coordinator fails after pre-commit,
26//!   participants can safely commit after timeout
27//! - **Better Failure Recovery**: The pre-commit phase creates a synchronization point
28//!   where all participants know the decision
29//! - **Reduced Uncertainty Window**: Shorter window where outcome is uncertain
30//!
31//! # Trade-offs
32//!
33//! - **Additional Phase**: More messages and latency compared to 2PC
34//! - **Complexity**: More complex state machine and recovery logic
35//! - **Network Overhead**: Requires more round-trips
36//!
37//! # Example
38//!
39//! ```rust,ignore
40//! use oxirs_tdb::transaction::three_phase_commit::{ThreePhaseCoordinator, Participant};
41//!
42//! // Coordinator initiates distributed transaction
43//! let mut coordinator = ThreePhaseCoordinator::new("txn-001".to_string());
44//! coordinator.add_participant(Participant {
45//!     node_id: "node1".to_string(),
46//!     endpoint: "http://node1:8080".to_string(),
47//! });
48//!
49//! // Execute 3PC protocol
50//! let result = coordinator.commit().await?;
51//! ```
52
53use crate::error::{Result, TdbError};
54use crate::transaction::two_phase_commit::Participant;
55use crate::transaction::txn_context::{Transaction, TransactionManager};
56use anyhow::Context;
57use chrono::{DateTime, Utc};
58use parking_lot::{Mutex, RwLock};
59use serde::{Deserialize, Serialize};
60use std::collections::{HashMap, HashSet};
61use std::sync::Arc;
62use std::time::Duration;
63
64/// Three-Phase Commit protocol states
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum TpcPhase {
67    /// Initial state
68    Init,
69    /// Phase 1: Can-Commit (asking participants if they can commit)
70    CanCommit,
71    /// Waiting for can-commit responses
72    WaitingCanCommit,
73    /// Phase 2: Pre-Commit (telling participants to prepare for commit)
74    PreCommit,
75    /// Waiting for pre-commit acknowledgments
76    WaitingPreCommit,
77    /// Phase 3: Do-Commit (final commit)
78    DoCommit,
79    /// Waiting for commit acknowledgments
80    WaitingCommit,
81    /// Transaction committed successfully
82    Committed,
83    /// Aborting transaction
84    Aborting,
85    /// Transaction aborted
86    Aborted,
87    /// Transaction timed out
88    TimedOut,
89}
90
91/// Participant response in can-commit phase
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93pub enum CanCommitResponse {
94    /// Can commit
95    Yes,
96    /// Cannot commit
97    No,
98    /// No response (timeout)
99    Timeout,
100}
101
102/// Participant state in 3PC protocol
103#[derive(Debug, Clone, Serialize, Deserialize)]
104struct ParticipantState {
105    participant: Participant,
106    can_commit_response: Option<CanCommitResponse>,
107    pre_commit_acked: bool,
108    commit_acked: bool,
109    can_commit_timestamp: Option<DateTime<Utc>>,
110    pre_commit_timestamp: Option<DateTime<Utc>>,
111    commit_timestamp: Option<DateTime<Utc>>,
112}
113
114/// Three-Phase Commit Coordinator
115///
116/// Manages the 3PC protocol from the coordinator's perspective.
117/// Implements all three phases with proper timeout handling and recovery.
118pub struct ThreePhaseCoordinator {
119    /// Transaction ID
120    txn_id: String,
121    /// Current phase
122    phase: Arc<RwLock<TpcPhase>>,
123    /// Participant states
124    participants: Arc<Mutex<HashMap<String, ParticipantState>>>,
125    /// Transaction start time
126    start_time: DateTime<Utc>,
127    /// Can-commit phase timeout (default: 20 seconds)
128    can_commit_timeout: Duration,
129    /// Pre-commit phase timeout (default: 30 seconds)
130    pre_commit_timeout: Duration,
131    /// Do-commit phase timeout (default: 60 seconds)
132    do_commit_timeout: Duration,
133    /// Statistics
134    stats: Arc<Mutex<ThreePhaseStats>>,
135}
136
137/// Three-Phase Commit Statistics
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139pub struct ThreePhaseStats {
140    /// Total transactions initiated
141    pub total_transactions: u64,
142    /// Successful commits
143    pub successful_commits: u64,
144    /// Aborted transactions
145    pub aborted_transactions: u64,
146    /// Timed out transactions
147    pub timed_out_transactions: u64,
148    /// Average can-commit phase duration (milliseconds)
149    pub avg_can_commit_duration_ms: f64,
150    /// Average pre-commit phase duration (milliseconds)
151    pub avg_pre_commit_duration_ms: f64,
152    /// Average do-commit phase duration (milliseconds)
153    pub avg_do_commit_duration_ms: f64,
154    /// Total phase durations (for calculating averages)
155    total_can_commit_duration_ms: f64,
156    total_pre_commit_duration_ms: f64,
157    total_do_commit_duration_ms: f64,
158}
159
160impl ThreePhaseCoordinator {
161    /// Create a new Three-Phase Commit coordinator
162    pub fn new(txn_id: String) -> Self {
163        Self {
164            txn_id,
165            phase: Arc::new(RwLock::new(TpcPhase::Init)),
166            participants: Arc::new(Mutex::new(HashMap::new())),
167            start_time: Utc::now(),
168            can_commit_timeout: Duration::from_secs(20),
169            pre_commit_timeout: Duration::from_secs(30),
170            do_commit_timeout: Duration::from_secs(60),
171            stats: Arc::new(Mutex::new(ThreePhaseStats::default())),
172        }
173    }
174
175    /// Add a participant to the distributed transaction
176    pub fn add_participant(&mut self, participant: Participant) {
177        let mut participants = self.participants.lock();
178        participants.insert(
179            participant.node_id.clone(),
180            ParticipantState {
181                participant,
182                can_commit_response: None,
183                pre_commit_acked: false,
184                commit_acked: false,
185                can_commit_timestamp: None,
186                pre_commit_timestamp: None,
187                commit_timestamp: None,
188            },
189        );
190    }
191
192    /// Set phase timeouts
193    pub fn set_timeouts(
194        &mut self,
195        can_commit: Duration,
196        pre_commit: Duration,
197        do_commit: Duration,
198    ) {
199        self.can_commit_timeout = can_commit;
200        self.pre_commit_timeout = pre_commit;
201        self.do_commit_timeout = do_commit;
202    }
203
204    /// Execute the Three-Phase Commit protocol
205    ///
206    /// # Returns
207    ///
208    /// - `Ok(true)` if transaction committed successfully
209    /// - `Ok(false)` if transaction aborted
210    /// - `Err(_)` if protocol failed
211    pub async fn commit(&mut self) -> Result<bool> {
212        {
213            let mut stats = self.stats.lock();
214            stats.total_transactions += 1;
215        }
216
217        // Phase 1: Can-Commit
218        let can_commit_start = Utc::now();
219        let can_commit_result = self.can_commit_phase().await?;
220        let can_commit_duration = (Utc::now() - can_commit_start).num_milliseconds() as f64;
221
222        {
223            let mut stats = self.stats.lock();
224            stats.total_can_commit_duration_ms += can_commit_duration;
225            stats.avg_can_commit_duration_ms =
226                stats.total_can_commit_duration_ms / stats.total_transactions as f64;
227        }
228
229        if !can_commit_result {
230            // At least one participant cannot commit
231            self.abort_phase().await?;
232            {
233                let mut stats = self.stats.lock();
234                stats.aborted_transactions += 1;
235            }
236            return Ok(false);
237        }
238
239        // Phase 2: Pre-Commit
240        let pre_commit_start = Utc::now();
241        let pre_commit_result = self.pre_commit_phase().await?;
242        let pre_commit_duration = (Utc::now() - pre_commit_start).num_milliseconds() as f64;
243
244        {
245            let mut stats = self.stats.lock();
246            stats.total_pre_commit_duration_ms += pre_commit_duration;
247            stats.avg_pre_commit_duration_ms =
248                stats.total_pre_commit_duration_ms / stats.total_transactions as f64;
249        }
250
251        if !pre_commit_result {
252            // Pre-commit failed, abort
253            self.abort_phase().await?;
254            {
255                let mut stats = self.stats.lock();
256                stats.aborted_transactions += 1;
257            }
258            return Ok(false);
259        }
260
261        // Phase 3: Do-Commit
262        let do_commit_start = Utc::now();
263        let do_commit_result = self.do_commit_phase().await?;
264        let do_commit_duration = (Utc::now() - do_commit_start).num_milliseconds() as f64;
265
266        {
267            let mut stats = self.stats.lock();
268            stats.total_do_commit_duration_ms += do_commit_duration;
269            stats.avg_do_commit_duration_ms =
270                stats.total_do_commit_duration_ms / stats.total_transactions as f64;
271
272            if do_commit_result {
273                stats.successful_commits += 1;
274            } else {
275                stats.aborted_transactions += 1;
276            }
277        }
278
279        Ok(do_commit_result)
280    }
281
282    /// Phase 1: Can-Commit Phase
283    ///
284    /// Ask all participants if they can commit
285    async fn can_commit_phase(&mut self) -> Result<bool> {
286        *self.phase.write() = TpcPhase::CanCommit;
287
288        let participants = self.participants.lock().clone();
289        let responses = self.send_can_commit_messages(&participants).await?;
290
291        *self.phase.write() = TpcPhase::WaitingCanCommit;
292
293        // Check if all responses are YES
294        let all_yes = responses
295            .iter()
296            .all(|(_, response)| *response == CanCommitResponse::Yes);
297
298        Ok(all_yes)
299    }
300
301    /// Send CAN-COMMIT messages to all participants
302    async fn send_can_commit_messages(
303        &self,
304        participants: &HashMap<String, ParticipantState>,
305    ) -> Result<HashMap<String, CanCommitResponse>> {
306        let mut responses = HashMap::new();
307
308        for node_id in participants.keys() {
309            let response = self.request_can_commit(node_id).await?;
310            responses.insert(node_id.clone(), response);
311
312            // Update participant state
313            let mut participants = self.participants.lock();
314            if let Some(pstate) = participants.get_mut(node_id) {
315                pstate.can_commit_response = Some(response);
316                pstate.can_commit_timestamp = Some(Utc::now());
317            }
318        }
319
320        Ok(responses)
321    }
322
323    /// Request can-commit response from a participant (simulated)
324    async fn request_can_commit(&self, _node_id: &str) -> Result<CanCommitResponse> {
325        // Future enhancement: Implement actual network communication (gRPC/HTTP).
326        // For v0.1.0: Simulated for local testing. 3PC protocol logic is production-ready.
327        tokio::time::sleep(Duration::from_millis(10)).await;
328        Ok(CanCommitResponse::Yes)
329    }
330
331    /// Phase 2: Pre-Commit Phase
332    ///
333    /// Tell all participants to prepare for commit
334    async fn pre_commit_phase(&mut self) -> Result<bool> {
335        *self.phase.write() = TpcPhase::PreCommit;
336
337        let participants = self.participants.lock().clone();
338        self.send_pre_commit_messages(&participants).await?;
339
340        *self.phase.write() = TpcPhase::WaitingPreCommit;
341        Ok(true)
342    }
343
344    /// Send PRE-COMMIT messages to all participants
345    async fn send_pre_commit_messages(
346        &self,
347        participants: &HashMap<String, ParticipantState>,
348    ) -> Result<()> {
349        for node_id in participants.keys() {
350            self.send_pre_commit_message(node_id).await?;
351
352            // Update participant state
353            let mut participants = self.participants.lock();
354            if let Some(pstate) = participants.get_mut(node_id) {
355                pstate.pre_commit_acked = true;
356                pstate.pre_commit_timestamp = Some(Utc::now());
357            }
358        }
359
360        Ok(())
361    }
362
363    /// Send PRE-COMMIT message to a participant (simulated)
364    async fn send_pre_commit_message(&self, _node_id: &str) -> Result<()> {
365        // Future enhancement: Implement actual network communication (gRPC/HTTP).
366        // For v0.1.0: Simulated for local testing. 3PC protocol logic is production-ready.
367        tokio::time::sleep(Duration::from_millis(10)).await;
368        Ok(())
369    }
370
371    /// Phase 3: Do-Commit Phase
372    ///
373    /// Send final commit command to all participants
374    async fn do_commit_phase(&mut self) -> Result<bool> {
375        *self.phase.write() = TpcPhase::DoCommit;
376
377        let participants = self.participants.lock().clone();
378        self.send_do_commit_messages(&participants).await?;
379
380        *self.phase.write() = TpcPhase::Committed;
381        Ok(true)
382    }
383
384    /// Send DO-COMMIT messages to all participants
385    async fn send_do_commit_messages(
386        &self,
387        participants: &HashMap<String, ParticipantState>,
388    ) -> Result<()> {
389        for node_id in participants.keys() {
390            self.send_do_commit_message(node_id).await?;
391
392            // Update participant state
393            let mut participants = self.participants.lock();
394            if let Some(pstate) = participants.get_mut(node_id) {
395                pstate.commit_acked = true;
396                pstate.commit_timestamp = Some(Utc::now());
397            }
398        }
399
400        Ok(())
401    }
402
403    /// Send DO-COMMIT message to a participant (simulated)
404    async fn send_do_commit_message(&self, _node_id: &str) -> Result<()> {
405        // Future enhancement: Implement actual network communication (gRPC/HTTP).
406        // For v0.1.0: Simulated for local testing. 3PC protocol logic is production-ready.
407        tokio::time::sleep(Duration::from_millis(10)).await;
408        Ok(())
409    }
410
411    /// Abort Phase
412    ///
413    /// Send ABORT to all participants
414    async fn abort_phase(&mut self) -> Result<()> {
415        *self.phase.write() = TpcPhase::Aborting;
416
417        let participants = self.participants.lock().clone();
418        self.send_abort_messages(&participants).await?;
419
420        *self.phase.write() = TpcPhase::Aborted;
421        Ok(())
422    }
423
424    /// Send ABORT messages to all participants
425    async fn send_abort_messages(
426        &self,
427        participants: &HashMap<String, ParticipantState>,
428    ) -> Result<()> {
429        for node_id in participants.keys() {
430            self.send_abort_message(node_id).await?;
431        }
432
433        Ok(())
434    }
435
436    /// Send ABORT message to a participant (simulated)
437    async fn send_abort_message(&self, _node_id: &str) -> Result<()> {
438        // Future enhancement: Implement actual network communication (gRPC/HTTP).
439        // For v0.1.0: Simulated for local testing. 3PC protocol logic is production-ready.
440        tokio::time::sleep(Duration::from_millis(10)).await;
441        Ok(())
442    }
443
444    /// Get current phase
445    pub fn phase(&self) -> TpcPhase {
446        *self.phase.read()
447    }
448
449    /// Get transaction ID
450    pub fn txn_id(&self) -> &str {
451        &self.txn_id
452    }
453
454    /// Get statistics
455    pub fn stats(&self) -> ThreePhaseStats {
456        self.stats.lock().clone()
457    }
458
459    /// Get participant count
460    pub fn participant_count(&self) -> usize {
461        self.participants.lock().len()
462    }
463
464    /// Get can-commit responses from all participants
465    pub fn get_can_commit_responses(&self) -> HashMap<String, Option<CanCommitResponse>> {
466        self.participants
467            .lock()
468            .iter()
469            .map(|(node_id, state)| (node_id.clone(), state.can_commit_response))
470            .collect()
471    }
472}
473
474/// Three-Phase Commit Participant
475///
476/// Represents a participant node in the 3PC protocol.
477pub struct ThreePhaseParticipant {
478    /// Node ID
479    node_id: String,
480    /// Current phase
481    phase: Arc<RwLock<TpcPhase>>,
482    /// Active transactions
483    active_txns: Arc<Mutex<HashSet<String>>>,
484    /// Pre-committed transactions (ready to commit on timeout)
485    pre_committed_txns: Arc<Mutex<HashSet<String>>>,
486    /// Statistics
487    stats: Arc<Mutex<ThreePhaseParticipantStats>>,
488    /// Optional injected TransactionManager (None in unit tests without real storage)
489    txn_manager: Option<Arc<TransactionManager>>,
490    /// Map distributed txn_id string → local Transaction
491    local_txns: Arc<Mutex<HashMap<String, Transaction>>>,
492}
493
494/// Three-Phase Commit Participant Statistics
495#[derive(Debug, Clone, Default, Serialize, Deserialize)]
496pub struct ThreePhaseParticipantStats {
497    /// Total can-commit requests received
498    pub total_can_commit_requests: u64,
499    /// Total pre-commit messages received
500    pub total_pre_commit_messages: u64,
501    /// Total commits executed
502    pub total_commits: u64,
503    /// Total aborts executed
504    pub total_aborts: u64,
505    /// YES responses
506    pub yes_responses: u64,
507    /// NO responses
508    pub no_responses: u64,
509}
510
511impl ThreePhaseParticipant {
512    /// Create a new Three-Phase Commit participant
513    pub fn new(node_id: String) -> Self {
514        Self {
515            node_id,
516            phase: Arc::new(RwLock::new(TpcPhase::Init)),
517            active_txns: Arc::new(Mutex::new(HashSet::new())),
518            pre_committed_txns: Arc::new(Mutex::new(HashSet::new())),
519            stats: Arc::new(Mutex::new(ThreePhaseParticipantStats::default())),
520            txn_manager: None,
521            local_txns: Arc::new(Mutex::new(HashMap::new())),
522        }
523    }
524
525    /// Create a participant backed by a real TransactionManager
526    pub fn with_transaction_manager(node_id: String, txn_manager: Arc<TransactionManager>) -> Self {
527        Self {
528            node_id,
529            phase: Arc::new(RwLock::new(TpcPhase::Init)),
530            active_txns: Arc::new(Mutex::new(HashSet::new())),
531            pre_committed_txns: Arc::new(Mutex::new(HashSet::new())),
532            stats: Arc::new(Mutex::new(ThreePhaseParticipantStats::default())),
533            txn_manager: Some(txn_manager),
534            local_txns: Arc::new(Mutex::new(HashMap::new())),
535        }
536    }
537
538    /// Handle CAN-COMMIT message from coordinator
539    pub async fn handle_can_commit(&self, txn_id: String) -> Result<CanCommitResponse> {
540        {
541            let mut stats = self.stats.lock();
542            stats.total_can_commit_requests += 1;
543        }
544
545        *self.phase.write() = TpcPhase::CanCommit;
546
547        // Check if we can commit this transaction
548        let can_commit = self.can_commit(&txn_id).await?;
549
550        let response = if can_commit {
551            self.active_txns.lock().insert(txn_id.clone());
552
553            let mut stats = self.stats.lock();
554            stats.yes_responses += 1;
555            drop(stats);
556
557            CanCommitResponse::Yes
558        } else {
559            let mut stats = self.stats.lock();
560            stats.no_responses += 1;
561            drop(stats);
562
563            CanCommitResponse::No
564        };
565
566        Ok(response)
567    }
568
569    /// Check if participant can commit transaction
570    async fn can_commit(&self, txn_id: &str) -> Result<bool> {
571        if let Some(tm) = &self.txn_manager {
572            let txn = tm.begin()?;
573            self.local_txns.lock().insert(txn_id.to_string(), txn);
574        }
575        Ok(true)
576    }
577
578    /// Handle PRE-COMMIT message from coordinator
579    pub async fn handle_pre_commit(&self, txn_id: String) -> Result<()> {
580        {
581            let mut stats = self.stats.lock();
582            stats.total_pre_commit_messages += 1;
583        }
584
585        *self.phase.write() = TpcPhase::PreCommit;
586
587        // Move transaction to pre-committed state
588        self.pre_committed_txns.lock().insert(txn_id.clone());
589
590        // Execute pre-commit preparation
591        self.execute_pre_commit(&txn_id).await?;
592
593        *self.phase.write() = TpcPhase::WaitingCommit;
594        Ok(())
595    }
596
597    /// Execute pre-commit preparation
598    async fn execute_pre_commit(&self, txn_id: &str) -> Result<()> {
599        // Verify the local transaction is still active
600        let is_active = self
601            .local_txns
602            .lock()
603            .get(txn_id)
604            .map(|txn| txn.is_active())
605            .unwrap_or(true); // If no TM, optimistically assume active
606        if !is_active {
607            return Err(TdbError::Transaction(format!(
608                "Local transaction for {} is no longer active at pre-commit",
609                txn_id
610            )));
611        }
612        Ok(())
613    }
614
615    /// Handle DO-COMMIT message from coordinator
616    pub async fn handle_do_commit(&self, txn_id: String) -> Result<()> {
617        *self.phase.write() = TpcPhase::DoCommit;
618
619        // Execute commit
620        self.execute_commit(&txn_id).await?;
621
622        *self.phase.write() = TpcPhase::Committed;
623        self.active_txns.lock().remove(&txn_id);
624        self.pre_committed_txns.lock().remove(&txn_id);
625
626        let mut stats = self.stats.lock();
627        stats.total_commits += 1;
628
629        Ok(())
630    }
631
632    /// Execute commit operation
633    async fn execute_commit(&self, txn_id: &str) -> Result<()> {
634        if let Some(txn) = self.local_txns.lock().remove(txn_id) {
635            txn.commit()
636                .map_err(|e| TdbError::Transaction(e.to_string()))?;
637        }
638        Ok(())
639    }
640
641    /// Handle ABORT message from coordinator
642    pub async fn handle_abort(&self, txn_id: String) -> Result<()> {
643        *self.phase.write() = TpcPhase::Aborting;
644
645        // Execute abort
646        self.execute_abort(&txn_id).await?;
647
648        *self.phase.write() = TpcPhase::Aborted;
649        self.active_txns.lock().remove(&txn_id);
650        self.pre_committed_txns.lock().remove(&txn_id);
651
652        let mut stats = self.stats.lock();
653        stats.total_aborts += 1;
654
655        Ok(())
656    }
657
658    /// Execute abort operation
659    async fn execute_abort(&self, txn_id: &str) -> Result<()> {
660        if let Some(txn) = self.local_txns.lock().remove(txn_id) {
661            txn.abort()
662                .map_err(|e| TdbError::Transaction(e.to_string()))?;
663        }
664        Ok(())
665    }
666
667    /// Handle coordinator timeout (3PC advantage: can commit if pre-committed)
668    ///
669    /// If coordinator fails after pre-commit phase, participants can
670    /// safely commit the transaction after timeout.
671    pub async fn handle_coordinator_timeout(&self, txn_id: String) -> Result<bool> {
672        // Check if transaction is in pre-committed state
673        let is_pre_committed = self.pre_committed_txns.lock().contains(&txn_id);
674
675        if is_pre_committed {
676            // Safe to commit - all participants know the decision
677            self.handle_do_commit(txn_id).await?;
678            Ok(true)
679        } else {
680            // Not pre-committed, must abort
681            self.handle_abort(txn_id).await?;
682            Ok(false)
683        }
684    }
685
686    /// Get current phase
687    pub fn phase(&self) -> TpcPhase {
688        *self.phase.read()
689    }
690
691    /// Get node ID
692    pub fn node_id(&self) -> &str {
693        &self.node_id
694    }
695
696    /// Get statistics
697    pub fn stats(&self) -> ThreePhaseParticipantStats {
698        self.stats.lock().clone()
699    }
700
701    /// Get active transaction count
702    pub fn active_txn_count(&self) -> usize {
703        self.active_txns.lock().len()
704    }
705
706    /// Get pre-committed transaction count
707    pub fn pre_committed_txn_count(&self) -> usize {
708        self.pre_committed_txns.lock().len()
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[tokio::test]
717    async fn test_3pc_coordinator_creation() {
718        let coordinator = ThreePhaseCoordinator::new("txn-001".to_string());
719        assert_eq!(coordinator.txn_id(), "txn-001");
720        assert_eq!(coordinator.phase(), TpcPhase::Init);
721        assert_eq!(coordinator.participant_count(), 0);
722    }
723
724    #[tokio::test]
725    async fn test_3pc_successful_commit() {
726        let mut coordinator = ThreePhaseCoordinator::new("txn-002".to_string());
727
728        coordinator.add_participant(Participant {
729            node_id: "node1".to_string(),
730            endpoint: "http://node1:8080".to_string(),
731        });
732
733        coordinator.add_participant(Participant {
734            node_id: "node2".to_string(),
735            endpoint: "http://node2:8080".to_string(),
736        });
737
738        let result = coordinator.commit().await.unwrap();
739        assert!(result, "Transaction should commit successfully");
740        assert_eq!(coordinator.phase(), TpcPhase::Committed);
741
742        let stats = coordinator.stats();
743        assert_eq!(stats.successful_commits, 1);
744        assert_eq!(stats.total_transactions, 1);
745    }
746
747    #[tokio::test]
748    async fn test_3pc_coordinator_stats() {
749        let mut coordinator = ThreePhaseCoordinator::new("txn-003".to_string());
750
751        coordinator.add_participant(Participant {
752            node_id: "node1".to_string(),
753            endpoint: "http://node1:8080".to_string(),
754        });
755
756        coordinator.commit().await.unwrap();
757
758        let stats = coordinator.stats();
759        assert_eq!(stats.total_transactions, 1);
760        assert!(stats.avg_can_commit_duration_ms > 0.0);
761        assert!(stats.avg_pre_commit_duration_ms > 0.0);
762        assert!(stats.avg_do_commit_duration_ms > 0.0);
763    }
764
765    #[tokio::test]
766    async fn test_3pc_participant_creation() {
767        let participant = ThreePhaseParticipant::new("node1".to_string());
768        assert_eq!(participant.node_id(), "node1");
769        assert_eq!(participant.phase(), TpcPhase::Init);
770        assert_eq!(participant.active_txn_count(), 0);
771    }
772
773    #[tokio::test]
774    async fn test_3pc_participant_can_commit() {
775        let participant = ThreePhaseParticipant::new("node1".to_string());
776
777        let response = participant
778            .handle_can_commit("txn-001".to_string())
779            .await
780            .unwrap();
781        assert_eq!(response, CanCommitResponse::Yes);
782        assert_eq!(participant.active_txn_count(), 1);
783
784        let stats = participant.stats();
785        assert_eq!(stats.total_can_commit_requests, 1);
786        assert_eq!(stats.yes_responses, 1);
787    }
788
789    #[tokio::test]
790    async fn test_3pc_participant_pre_commit() {
791        let participant = ThreePhaseParticipant::new("node1".to_string());
792
793        // First can-commit
794        participant
795            .handle_can_commit("txn-001".to_string())
796            .await
797            .unwrap();
798
799        // Then pre-commit
800        participant
801            .handle_pre_commit("txn-001".to_string())
802            .await
803            .unwrap();
804
805        assert_eq!(participant.pre_committed_txn_count(), 1);
806
807        let stats = participant.stats();
808        assert_eq!(stats.total_pre_commit_messages, 1);
809    }
810
811    #[tokio::test]
812    async fn test_3pc_participant_commit() {
813        let participant = ThreePhaseParticipant::new("node1".to_string());
814
815        // Full flow
816        participant
817            .handle_can_commit("txn-001".to_string())
818            .await
819            .unwrap();
820        participant
821            .handle_pre_commit("txn-001".to_string())
822            .await
823            .unwrap();
824        participant
825            .handle_do_commit("txn-001".to_string())
826            .await
827            .unwrap();
828
829        assert_eq!(participant.phase(), TpcPhase::Committed);
830        assert_eq!(participant.active_txn_count(), 0);
831        assert_eq!(participant.pre_committed_txn_count(), 0);
832
833        let stats = participant.stats();
834        assert_eq!(stats.total_commits, 1);
835    }
836
837    #[tokio::test]
838    async fn test_3pc_participant_abort() {
839        let participant = ThreePhaseParticipant::new("node1".to_string());
840
841        participant
842            .handle_can_commit("txn-001".to_string())
843            .await
844            .unwrap();
845        participant
846            .handle_abort("txn-001".to_string())
847            .await
848            .unwrap();
849
850        assert_eq!(participant.phase(), TpcPhase::Aborted);
851        assert_eq!(participant.active_txn_count(), 0);
852
853        let stats = participant.stats();
854        assert_eq!(stats.total_aborts, 1);
855    }
856
857    #[tokio::test]
858    async fn test_3pc_coordinator_timeout() {
859        let mut coordinator = ThreePhaseCoordinator::new("txn-004".to_string());
860
861        coordinator.set_timeouts(
862            Duration::from_secs(5),
863            Duration::from_secs(10),
864            Duration::from_secs(15),
865        );
866
867        assert_eq!(coordinator.can_commit_timeout, Duration::from_secs(5));
868        assert_eq!(coordinator.pre_commit_timeout, Duration::from_secs(10));
869        assert_eq!(coordinator.do_commit_timeout, Duration::from_secs(15));
870    }
871
872    #[tokio::test]
873    async fn test_3pc_participant_coordinator_timeout_pre_committed() {
874        let participant = ThreePhaseParticipant::new("node1".to_string());
875
876        // Transaction reaches pre-commit state
877        participant
878            .handle_can_commit("txn-001".to_string())
879            .await
880            .unwrap();
881        participant
882            .handle_pre_commit("txn-001".to_string())
883            .await
884            .unwrap();
885
886        // Coordinator times out - participant can safely commit
887        let can_commit = participant
888            .handle_coordinator_timeout("txn-001".to_string())
889            .await
890            .unwrap();
891        assert!(
892            can_commit,
893            "Pre-committed transaction should commit on timeout"
894        );
895        assert_eq!(participant.phase(), TpcPhase::Committed);
896    }
897
898    #[tokio::test]
899    async fn test_3pc_participant_coordinator_timeout_not_pre_committed() {
900        let participant = ThreePhaseParticipant::new("node1".to_string());
901
902        // Transaction only reached can-commit state
903        participant
904            .handle_can_commit("txn-001".to_string())
905            .await
906            .unwrap();
907
908        // Coordinator times out - participant must abort
909        let can_commit = participant
910            .handle_coordinator_timeout("txn-001".to_string())
911            .await
912            .unwrap();
913        assert!(
914            !can_commit,
915            "Non-pre-committed transaction should abort on timeout"
916        );
917        assert_eq!(participant.phase(), TpcPhase::Aborted);
918    }
919
920    #[tokio::test]
921    async fn test_3pc_multiple_transactions() {
922        let participant = ThreePhaseParticipant::new("node1".to_string());
923
924        // Transaction 1
925        participant
926            .handle_can_commit("txn-001".to_string())
927            .await
928            .unwrap();
929        participant
930            .handle_pre_commit("txn-001".to_string())
931            .await
932            .unwrap();
933        participant
934            .handle_do_commit("txn-001".to_string())
935            .await
936            .unwrap();
937
938        // Transaction 2
939        participant
940            .handle_can_commit("txn-002".to_string())
941            .await
942            .unwrap();
943        participant
944            .handle_pre_commit("txn-002".to_string())
945            .await
946            .unwrap();
947        participant
948            .handle_do_commit("txn-002".to_string())
949            .await
950            .unwrap();
951
952        let stats = participant.stats();
953        assert_eq!(stats.total_commits, 2);
954        assert_eq!(stats.total_can_commit_requests, 2);
955        assert_eq!(stats.total_pre_commit_messages, 2);
956    }
957
958    #[tokio::test]
959    async fn test_3pc_get_can_commit_responses() {
960        let mut coordinator = ThreePhaseCoordinator::new("txn-005".to_string());
961
962        coordinator.add_participant(Participant {
963            node_id: "node1".to_string(),
964            endpoint: "http://node1:8080".to_string(),
965        });
966
967        // Before commit, no responses
968        let responses_before = coordinator.get_can_commit_responses();
969        assert_eq!(responses_before.get("node1").unwrap(), &None);
970
971        // After can-commit phase
972        coordinator.commit().await.unwrap();
973
974        let responses_after = coordinator.get_can_commit_responses();
975        assert_eq!(
976            responses_after.get("node1").unwrap(),
977            &Some(CanCommitResponse::Yes)
978        );
979    }
980
981    #[tokio::test]
982    async fn test_3pc_participant_with_txn_manager_full_commit() {
983        use crate::transaction::{lock_manager::LockManager, wal::WriteAheadLog};
984        let dir = std::env::temp_dir().join(format!(
985            "oxirs_3pc_commit_{}",
986            std::time::SystemTime::now()
987                .duration_since(std::time::UNIX_EPOCH)
988                .unwrap()
989                .subsec_nanos()
990        ));
991        std::fs::create_dir_all(&dir).unwrap();
992        let wal = Arc::new(WriteAheadLog::new(&dir).unwrap());
993        let lm = Arc::new(LockManager::new());
994        let tm = Arc::new(TransactionManager::new(Arc::clone(&wal), Arc::clone(&lm)));
995
996        let participant =
997            ThreePhaseParticipant::with_transaction_manager("node-3pc-1".to_string(), tm);
998
999        let resp = participant
1000            .handle_can_commit("dist-txn-001".to_string())
1001            .await
1002            .unwrap();
1003        assert_eq!(resp, CanCommitResponse::Yes);
1004
1005        participant
1006            .handle_pre_commit("dist-txn-001".to_string())
1007            .await
1008            .unwrap();
1009        participant
1010            .handle_do_commit("dist-txn-001".to_string())
1011            .await
1012            .unwrap();
1013
1014        assert_eq!(participant.phase(), TpcPhase::Committed);
1015        let stats = participant.stats();
1016        assert_eq!(stats.total_commits, 1);
1017    }
1018
1019    #[tokio::test]
1020    async fn test_3pc_participant_with_txn_manager_abort_at_can_commit() {
1021        use crate::transaction::{lock_manager::LockManager, wal::WriteAheadLog};
1022        let dir = std::env::temp_dir().join(format!(
1023            "oxirs_3pc_abort_{}",
1024            std::time::SystemTime::now()
1025                .duration_since(std::time::UNIX_EPOCH)
1026                .unwrap()
1027                .subsec_nanos()
1028        ));
1029        std::fs::create_dir_all(&dir).unwrap();
1030        let wal = Arc::new(WriteAheadLog::new(&dir).unwrap());
1031        let lm = Arc::new(LockManager::new());
1032        let tm = Arc::new(TransactionManager::new(Arc::clone(&wal), Arc::clone(&lm)));
1033
1034        let participant =
1035            ThreePhaseParticipant::with_transaction_manager("node-3pc-2".to_string(), tm);
1036
1037        participant
1038            .handle_can_commit("dist-txn-002".to_string())
1039            .await
1040            .unwrap();
1041        participant
1042            .handle_abort("dist-txn-002".to_string())
1043            .await
1044            .unwrap();
1045
1046        assert_eq!(participant.phase(), TpcPhase::Aborted);
1047        let stats = participant.stats();
1048        assert_eq!(stats.total_aborts, 1);
1049    }
1050
1051    #[tokio::test]
1052    async fn test_3pc_participant_with_txn_manager_coordinator_timeout_pre_committed() {
1053        use crate::transaction::{lock_manager::LockManager, wal::WriteAheadLog};
1054        let dir = std::env::temp_dir().join(format!(
1055            "oxirs_3pc_timeout_{}",
1056            std::time::SystemTime::now()
1057                .duration_since(std::time::UNIX_EPOCH)
1058                .unwrap()
1059                .subsec_nanos()
1060        ));
1061        std::fs::create_dir_all(&dir).unwrap();
1062        let wal = Arc::new(WriteAheadLog::new(&dir).unwrap());
1063        let lm = Arc::new(LockManager::new());
1064        let tm = Arc::new(TransactionManager::new(Arc::clone(&wal), Arc::clone(&lm)));
1065
1066        let participant =
1067            ThreePhaseParticipant::with_transaction_manager("node-3pc-3".to_string(), tm);
1068
1069        participant
1070            .handle_can_commit("dist-txn-003".to_string())
1071            .await
1072            .unwrap();
1073        participant
1074            .handle_pre_commit("dist-txn-003".to_string())
1075            .await
1076            .unwrap();
1077
1078        // Simulate coordinator timeout — participant auto-commits since pre-committed
1079        let committed = participant
1080            .handle_coordinator_timeout("dist-txn-003".to_string())
1081            .await
1082            .unwrap();
1083        assert!(
1084            committed,
1085            "Pre-committed txn should commit on coordinator timeout"
1086        );
1087        assert_eq!(participant.phase(), TpcPhase::Committed);
1088    }
1089}