Skip to main content

oximedia_distributed/
twopc.rs

1//! Two-phase commit (2PC) coordinator.
2//!
3//! Implements the classic two-phase commit protocol for distributed
4//! atomic transactions.  The coordinator drives the prepare and commit/abort
5//! phases across a set of named participants.
6
7use std::collections::{HashMap, HashSet};
8
9/// Phase of the two-phase commit protocol.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TwoPhaseState {
12    /// Initial state; no transaction in progress.
13    Idle,
14    /// Prepare messages have been sent; awaiting votes.
15    Preparing,
16    /// All participants voted yes; commit in progress.
17    Committing,
18    /// At least one participant voted no, or abort was requested.
19    Aborting,
20    /// All participants acknowledged the commit.
21    Committed,
22    /// All participants acknowledged the abort.
23    Aborted,
24}
25
26/// The coordinator for a single two-phase commit transaction.
27///
28/// Tracks participant votes and drives the protocol from prepare through
29/// commit or abort.  Participants are identified by arbitrary `u64` IDs.
30pub struct TwoPhaseCoordinator {
31    /// Current protocol state.
32    state: TwoPhaseState,
33    /// Set of participant IDs that must vote.
34    participants: Vec<u64>,
35    /// Votes collected during the prepare phase (true = yes, false = no).
36    votes: HashMap<u64, bool>,
37    /// Set of participants that acknowledged the final decision.
38    acks: HashSet<u64>,
39}
40
41impl TwoPhaseCoordinator {
42    /// Create a new, idle coordinator with no participants.
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            state: TwoPhaseState::Idle,
47            participants: Vec::new(),
48            votes: HashMap::new(),
49            acks: HashSet::new(),
50        }
51    }
52
53    /// Return the current protocol state.
54    #[must_use]
55    pub fn state(&self) -> TwoPhaseState {
56        self.state
57    }
58
59    /// Broadcast a `prepare` message to all `participants`.
60    ///
61    /// Transitions from `Idle` → `Preparing`.  In this in-process simulation
62    /// all participants immediately vote "yes" unless overridden via
63    /// [`Self::record_vote`].  Returns `true` if all simulated immediate votes agree,
64    /// `false` if any participant immediately votes "no".
65    ///
66    /// # Arguments
67    ///
68    /// * `participants` - Slice of participant IDs to include in this transaction.
69    pub fn prepare(&mut self, participants: &[u64]) -> bool {
70        self.participants = participants.to_vec();
71        self.votes.clear();
72        self.acks.clear();
73
74        if participants.is_empty() {
75            // No participants → vacuously prepared
76            self.state = TwoPhaseState::Preparing;
77            return true;
78        }
79
80        self.state = TwoPhaseState::Preparing;
81
82        // Simulate immediate "yes" votes from all participants
83        for &p in participants {
84            self.votes.insert(p, true);
85        }
86
87        self.all_voted_yes()
88    }
89
90    /// Record a vote from participant `id`.
91    ///
92    /// Must be called while in the `Preparing` state.  Returns `false` if the
93    /// coordinator is not in the `Preparing` state or `id` is not a known
94    /// participant.
95    pub fn record_vote(&mut self, id: u64, vote: bool) -> bool {
96        if self.state != TwoPhaseState::Preparing {
97            return false;
98        }
99        if !self.participants.contains(&id) {
100            return false;
101        }
102        self.votes.insert(id, vote);
103        true
104    }
105
106    /// Commit the transaction.
107    ///
108    /// Transitions to `Committing` if all participants voted yes, then
109    /// immediately transitions to `Committed` (simulating synchronous acks).
110    ///
111    /// Returns `true` on success, `false` if the protocol state does not allow
112    /// commit (e.g. not all votes are yes).
113    pub fn commit(&mut self) -> bool {
114        if self.state != TwoPhaseState::Preparing || !self.all_voted_yes() {
115            return false;
116        }
117        self.state = TwoPhaseState::Committing;
118        // Simulate all participants acknowledging
119        for &p in &self.participants {
120            self.acks.insert(p);
121        }
122        self.state = TwoPhaseState::Committed;
123        true
124    }
125
126    /// Abort the transaction.
127    ///
128    /// Can be called from `Preparing` or `Committing`.  Transitions through
129    /// `Aborting` → `Aborted`.  Returns `false` if already in a terminal state.
130    pub fn abort(&mut self) -> bool {
131        match self.state {
132            TwoPhaseState::Idle | TwoPhaseState::Preparing | TwoPhaseState::Committing => {
133                self.state = TwoPhaseState::Aborting;
134                // Simulate all participants acknowledging the abort
135                for &p in &self.participants {
136                    self.acks.insert(p);
137                }
138                self.state = TwoPhaseState::Aborted;
139                true
140            }
141            _ => false,
142        }
143    }
144
145    /// Reset the coordinator to `Idle` so it can be reused.
146    pub fn reset(&mut self) {
147        self.state = TwoPhaseState::Idle;
148        self.participants.clear();
149        self.votes.clear();
150        self.acks.clear();
151    }
152
153    /// Return `true` if all participants in the `votes` map voted yes.
154    fn all_voted_yes(&self) -> bool {
155        if self.participants.is_empty() {
156            return true;
157        }
158        self.participants
159            .iter()
160            .all(|p| self.votes.get(p).copied().unwrap_or(false))
161    }
162
163    /// Number of yes votes collected so far.
164    #[must_use]
165    pub fn yes_vote_count(&self) -> usize {
166        self.votes.values().filter(|&&v| v).count()
167    }
168
169    /// Number of no votes collected so far.
170    #[must_use]
171    pub fn no_vote_count(&self) -> usize {
172        self.votes.values().filter(|&&v| !v).count()
173    }
174}
175
176impl Default for TwoPhaseCoordinator {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_new_state_is_idle() {
188        let coord = TwoPhaseCoordinator::new();
189        assert_eq!(coord.state(), TwoPhaseState::Idle);
190    }
191
192    #[test]
193    fn test_prepare_transitions_to_preparing() {
194        let mut coord = TwoPhaseCoordinator::new();
195        let result = coord.prepare(&[1, 2]);
196        assert!(result); // all auto yes votes
197        assert_eq!(coord.state(), TwoPhaseState::Preparing);
198    }
199
200    #[test]
201    fn test_commit_after_prepare_succeeds() {
202        let mut coord = TwoPhaseCoordinator::new();
203        coord.prepare(&[1, 2, 3]);
204        let ok = coord.commit();
205        assert!(ok);
206        assert_eq!(coord.state(), TwoPhaseState::Committed);
207    }
208
209    #[test]
210    fn test_abort_after_prepare() {
211        let mut coord = TwoPhaseCoordinator::new();
212        coord.prepare(&[1, 2]);
213        let ok = coord.abort();
214        assert!(ok);
215        assert_eq!(coord.state(), TwoPhaseState::Aborted);
216    }
217
218    #[test]
219    fn test_no_vote_prevents_commit() {
220        let mut coord = TwoPhaseCoordinator::new();
221        coord.prepare(&[1, 2, 3]);
222        coord.record_vote(2, false); // participant 2 votes no
223        let ok = coord.commit();
224        assert!(!ok);
225        assert_eq!(coord.state(), TwoPhaseState::Preparing); // unchanged
226    }
227
228    #[test]
229    fn test_abort_after_no_vote() {
230        let mut coord = TwoPhaseCoordinator::new();
231        coord.prepare(&[1, 2]);
232        coord.record_vote(1, false);
233        coord.abort();
234        assert_eq!(coord.state(), TwoPhaseState::Aborted);
235    }
236
237    #[test]
238    fn test_abort_terminal_state_returns_false() {
239        let mut coord = TwoPhaseCoordinator::new();
240        coord.prepare(&[1]);
241        coord.commit();
242        assert_eq!(coord.state(), TwoPhaseState::Committed);
243        let ok = coord.abort();
244        assert!(!ok);
245    }
246
247    #[test]
248    fn test_reset_allows_reuse() {
249        let mut coord = TwoPhaseCoordinator::new();
250        coord.prepare(&[1]);
251        coord.commit();
252        coord.reset();
253        assert_eq!(coord.state(), TwoPhaseState::Idle);
254        coord.prepare(&[2, 3]);
255        assert_eq!(coord.state(), TwoPhaseState::Preparing);
256        coord.commit();
257        assert_eq!(coord.state(), TwoPhaseState::Committed);
258    }
259
260    #[test]
261    fn test_record_vote_unknown_participant() {
262        let mut coord = TwoPhaseCoordinator::new();
263        coord.prepare(&[1]);
264        let ok = coord.record_vote(99, true); // not a participant
265        assert!(!ok);
266    }
267
268    #[test]
269    fn test_record_vote_wrong_state() {
270        let mut coord = TwoPhaseCoordinator::new();
271        let ok = coord.record_vote(1, true); // Idle state
272        assert!(!ok);
273    }
274
275    #[test]
276    fn test_empty_participants_commit() {
277        let mut coord = TwoPhaseCoordinator::new();
278        coord.prepare(&[]);
279        coord.commit();
280        assert_eq!(coord.state(), TwoPhaseState::Committed);
281    }
282
283    #[test]
284    fn test_vote_counts() {
285        let mut coord = TwoPhaseCoordinator::new();
286        coord.prepare(&[1, 2, 3]);
287        coord.record_vote(3, false);
288        assert_eq!(coord.yes_vote_count(), 2);
289        assert_eq!(coord.no_vote_count(), 1);
290    }
291}