oxicode_sdk/coordination/
consensus.rs1use parking_lot::RwLock;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct VoteResult {
10 pub decided: bool,
12 pub decision: Option<String>,
14 pub tally: HashMap<String, usize>,
16 pub votes_received: usize,
18 pub total_voters: usize,
20}
21
22struct VoteSession {
24 voters: Vec<String>,
25 threshold: f32,
26 votes: HashMap<String, String>,
27}
28
29pub struct Consensus {
33 sessions: RwLock<HashMap<String, VoteSession>>,
34}
35
36impl Consensus {
37 pub fn new() -> Self {
39 Self {
40 sessions: RwLock::new(HashMap::new()),
41 }
42 }
43
44 pub fn start(&self, vote_id: &str, voters: Vec<String>, threshold: f32) {
48 self.sessions.write().insert(
49 vote_id.to_string(),
50 VoteSession {
51 voters,
52 threshold,
53 votes: HashMap::new(),
54 },
55 );
56 }
57
58 pub fn vote(
62 &self,
63 vote_id: &str,
64 voter: &str,
65 value: String,
66 ) -> crate::error::SdkResult<VoteResult> {
67 let mut sessions = self.sessions.write();
68 let session =
69 sessions
70 .get_mut(vote_id)
71 .ok_or_else(|| crate::error::SdkError::VoteNotFound {
72 vote_id: vote_id.to_string(),
73 })?;
74
75 if !session.voters.iter().any(|v| v == voter) {
77 return Err(crate::error::SdkError::InvalidState {
78 entity: "vote".into(),
79 reason: format!("voter '{}' not in voter list", voter),
80 });
81 }
82
83 session.votes.insert(voter.to_string(), value);
84 Ok(self.compute_result(session))
85 }
86
87 pub fn status(&self, vote_id: &str) -> Option<VoteResult> {
89 let sessions = self.sessions.read();
90 sessions.get(vote_id).map(|s| self.compute_result(s))
91 }
92
93 fn compute_result(&self, session: &VoteSession) -> VoteResult {
94 let mut tally: HashMap<String, usize> = HashMap::new();
95 for value in session.votes.values() {
96 *tally.entry(value.clone()).or_insert(0) += 1;
97 }
98
99 let votes_received = session.votes.len();
100 let total_voters = session.voters.len();
101 let required = (session.threshold * total_voters as f32).ceil() as usize;
102
103 let (best_value, best_count) = tally
105 .iter()
106 .max_by_key(|(_, count)| *count)
107 .map(|(v, c)| (v.clone(), *c))
108 .unwrap_or_default();
109
110 let decided = best_count >= required && votes_received > 0;
111 let decision = if decided { Some(best_value) } else { None };
112
113 VoteResult {
114 decided,
115 decision,
116 tally,
117 votes_received,
118 total_voters,
119 }
120 }
121}
122
123impl Default for Consensus {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn majority_vote() {
135 let c = Consensus::new();
136 c.start("v1", vec!["a".into(), "b".into(), "c".into()], 0.5);
137
138 c.vote("v1", "a", "yes".into()).unwrap();
139 let r = c.vote("v1", "b", "yes".into()).unwrap();
140 assert!(r.decided);
141 assert_eq!(r.decision.unwrap(), "yes");
142 }
143
144 #[test]
145 fn unanimity_required() {
146 let c = Consensus::new();
147 c.start("v2", vec!["a".into(), "b".into()], 1.0);
148
149 c.vote("v2", "a", "yes".into()).unwrap();
150 let r = c.status("v2").unwrap();
151 assert!(!r.decided);
152
153 c.vote("v2", "b", "yes".into()).unwrap();
154 let r = c.status("v2").unwrap();
155 assert!(r.decided);
156 }
157
158 #[test]
159 fn split_vote_no_majority() {
160 let c = Consensus::new();
161 c.start("v3", vec!["a".into(), "b".into()], 0.6);
162
163 c.vote("v3", "a", "yes".into()).unwrap();
164 c.vote("v3", "b", "no".into()).unwrap();
165 let r = c.status("v3").unwrap();
166 assert!(!r.decided);
167 }
168
169 #[test]
170 fn invalid_voter() {
171 let c = Consensus::new();
172 c.start("v4", vec!["a".into()], 0.5);
173 let result = c.vote("v4", "intruder", "yes".into());
174 assert!(result.is_err());
175 }
176
177 #[test]
178 fn vote_not_found() {
179 let c = Consensus::new();
180 let result = c.vote("nonexistent", "a", "yes".into());
181 assert!(result.is_err());
182 }
183
184 #[test]
185 fn vote_result_tally() {
186 let c = Consensus::new();
187 c.start("v5", vec!["a".into(), "b".into(), "c".into()], 0.5);
188
189 c.vote("v5", "a", "x".into()).unwrap();
190 c.vote("v5", "b", "y".into()).unwrap();
191 c.vote("v5", "c", "x".into()).unwrap();
192
193 let r = c.status("v5").unwrap();
194 assert!(r.decided);
195 assert_eq!(r.decision.unwrap(), "x");
196 assert_eq!(*r.tally.get("x").unwrap(), 2);
197 assert_eq!(*r.tally.get("y").unwrap(), 1);
198 }
199}