Skip to main content

confium_coordinator/
async_session_manager.rs

1//! Async session manager — non-blocking session lifecycle.
2//!
3//! Provides an async interface for session creation, commitment
4//! collection, and aggregation. Uses std::thread channels under the
5//! hood (not tokio) for compatibility.
6
7use crate::coordinator::coordinator::Coordinator;
8use crate::coordinator::session::{SessionId, SessionRequest, SessionState};
9use std::collections::HashMap;
10use std::sync::mpsc::{Receiver, Sender, channel};
11use std::thread;
12use std::time::Duration;
13
14/// Async session manager.
15pub struct AsyncSessionManager {
16    sender: Sender<AsyncOp>,
17}
18
19enum AsyncOp {
20    CreateSession(SessionRequest),
21    SubmitCommitment(String, String, Vec<u8>),
22    SubmitShare(String, String, Vec<u8>),
23    Shutdown,
24}
25
26impl AsyncSessionManager {
27    /// Create a new async session manager wrapping the coordinator.
28    pub fn spawn(coord: std::sync::Arc<std::sync::Mutex<Coordinator>>) -> Self {
29        let (tx, rx) = channel();
30        thread::spawn(move || {
31            Self::run_loop(coord, rx);
32        });
33        Self { sender: tx }
34    }
35
36    fn run_loop(coord: std::sync::Arc<std::sync::Mutex<Coordinator>>, rx: Receiver<AsyncOp>) {
37        while let Ok(op) = rx.recv() {
38            match op {
39                AsyncOp::CreateSession(req) => {
40                    let _ = coord.lock().unwrap().create_session(req);
41                }
42                AsyncOp::SubmitCommitment(sid, signer, bytes) => {
43                    use crate::coordinator::session::Commitment;
44                    let _ = coord.lock().unwrap().submit_commitment(
45                        &sid,
46                        Commitment {
47                            signer_id: signer,
48                            bytes,
49                            signer_signature: vec![0u8; 64],
50                            submitted_at: chrono::Utc::now(),
51                        },
52                    );
53                }
54                AsyncOp::SubmitShare(sid, signer, bytes) => {
55                    use crate::coordinator::session::Share;
56                    let _ = coord.lock().unwrap().submit_share(
57                        &sid,
58                        Share {
59                            signer_id: signer,
60                            bytes,
61                            signer_signature: vec![0u8; 64],
62                            submitted_at: chrono::Utc::now(),
63                        },
64                    );
65                }
66                AsyncOp::Shutdown => break,
67            }
68        }
69    }
70
71    /// Asynchronously create a session.
72    pub fn create_session(&self, request: SessionRequest) {
73        let _ = self.sender.send(AsyncOp::CreateSession(request));
74    }
75
76    /// Asynchronously submit a commitment.
77    pub fn submit_commitment(&self, session_id: &str, signer_id: &str, bytes: Vec<u8>) {
78        let _ = self.sender.send(AsyncOp::SubmitCommitment(
79            session_id.into(),
80            signer_id.into(),
81            bytes,
82        ));
83    }
84
85    /// Asynchronously submit a share.
86    pub fn submit_share(&self, session_id: &str, signer_id: &str, bytes: Vec<u8>) {
87        let _ = self.sender.send(AsyncOp::SubmitShare(
88            session_id.into(),
89            signer_id.into(),
90            bytes,
91        ));
92    }
93
94    /// Shutdown the background thread.
95    pub fn shutdown(&self) {
96        let _ = self.sender.send(AsyncOp::Shutdown);
97    }
98}
99
100/// Async session info: non-blocking query.
101pub struct AsyncSessionInfo {
102    pub session_id: SessionId,
103    pub state: SessionState,
104}
105
106/// Track pending async operations.
107#[derive(Default)]
108pub struct PendingOpsTracker {
109    pending: HashMap<SessionId, Vec<String>>,
110}
111
112impl PendingOpsTracker {
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    pub fn record_op(&mut self, session_id: &str, op: &str) {
118        self.pending
119            .entry(session_id.into())
120            .or_default()
121            .push(op.into());
122    }
123
124    pub fn pending_ops(&self, session_id: &str) -> Vec<String> {
125        self.pending.get(session_id).cloned().unwrap_or_default()
126    }
127
128    pub fn clear_session(&mut self, session_id: &str) {
129        self.pending.remove(session_id);
130    }
131
132    pub fn total_pending(&self) -> usize {
133        self.pending.values().map(|v| v.len()).sum()
134    }
135}
136
137/// Wait for a session to reach a target state, polling.
138pub fn wait_for_state(
139    coord: &Coordinator,
140    session_id: &str,
141    target: SessionState,
142    timeout: Duration,
143) -> bool {
144    let start = std::time::Instant::now();
145    while start.elapsed() < timeout {
146        if let Some(state) = coord.session_state(session_id) {
147            if state == target {
148                return true;
149            }
150        }
151        thread::sleep(Duration::from_millis(10));
152    }
153    false
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use std::sync::{Arc, Mutex};
160
161    #[test]
162    fn async_create_session() {
163        let coord = Arc::new(Mutex::new(Coordinator::new()));
164        let manager = AsyncSessionManager::spawn(Arc::clone(&coord));
165
166        let request = SessionRequest {
167            quorum_id: "q1".into(),
168            scheme: "CMP20".into(),
169            message: vec![0; 32],
170            threshold: 2,
171            num_parties: 3,
172            unlock_window_minutes: 60,
173            requested_by: "async-test".into(),
174        };
175        manager.create_session(request);
176
177        // Wait briefly for processing
178        thread::sleep(Duration::from_millis(50));
179        assert_eq!(coord.lock().unwrap().session_count(), 1);
180        manager.shutdown();
181    }
182
183    #[test]
184    fn pending_ops_tracker() {
185        let mut tracker = PendingOpsTracker::new();
186        tracker.record_op("s1", "create");
187        tracker.record_op("s1", "commit");
188        tracker.record_op("s2", "share");
189        assert_eq!(tracker.pending_ops("s1").len(), 2);
190        assert_eq!(tracker.pending_ops("s2").len(), 1);
191        assert_eq!(tracker.total_pending(), 3);
192        tracker.clear_session("s1");
193        assert_eq!(tracker.total_pending(), 1);
194    }
195
196    #[test]
197    fn wait_for_state_timeout() {
198        let mut coord = Coordinator::new();
199        let req = SessionRequest {
200            quorum_id: "q".into(),
201            scheme: "CMP20".into(),
202            message: vec![0; 32],
203            threshold: 2,
204            num_parties: 3,
205            unlock_window_minutes: 60,
206            requested_by: "test".into(),
207        };
208        let sid = coord.create_session(req).unwrap();
209        // Session is Pending, not Completed - timeout
210        let completed = wait_for_state(
211            &coord,
212            &sid,
213            SessionState::Completed,
214            Duration::from_millis(50),
215        );
216        assert!(!completed);
217    }
218
219    #[test]
220    fn async_commitment_submission() {
221        let coord = Arc::new(Mutex::new(Coordinator::new()));
222        let manager = AsyncSessionManager::spawn(Arc::clone(&coord));
223
224        let request = SessionRequest {
225            quorum_id: "q1".into(),
226            scheme: "CMP20".into(),
227            message: vec![0; 32],
228            threshold: 2,
229            num_parties: 3,
230            unlock_window_minutes: 60,
231            requested_by: "test".into(),
232        };
233        let sid = coord.lock().unwrap().create_session(request).unwrap();
234        manager.submit_commitment(&sid, "alice", vec![0xAA; 32]);
235        thread::sleep(Duration::from_millis(50));
236        assert_eq!(
237            coord.lock().unwrap().session_commitment_count(&sid),
238            Some(1)
239        );
240        manager.shutdown();
241    }
242
243    #[test]
244    fn async_share_submission() {
245        let coord = Arc::new(Mutex::new(Coordinator::new()));
246        let manager = AsyncSessionManager::spawn(Arc::clone(&coord));
247
248        let request = SessionRequest {
249            quorum_id: "q1".into(),
250            scheme: "CMP20".into(),
251            message: vec![0; 32],
252            threshold: 2,
253            num_parties: 3,
254            unlock_window_minutes: 60,
255            requested_by: "test".into(),
256        };
257        let sid = coord.lock().unwrap().create_session(request).unwrap();
258        manager.submit_share(&sid, "alice", vec![0xBB; 32]);
259        thread::sleep(Duration::from_millis(50));
260        assert_eq!(coord.lock().unwrap().session_share_count(&sid), Some(1));
261        manager.shutdown();
262    }
263}