confium_coordinator/coordinator/
reaper.rs1use crate::coordinator::audit::AuditEvent;
9use crate::coordinator::coordinator::Coordinator;
10use crate::coordinator::session::SessionState;
11use chrono::Utc;
12use std::sync::{Arc, Mutex};
13use std::thread;
14use std::time::Duration;
15
16#[derive(Debug, Clone)]
18pub struct ReaperConfig {
19 pub scan_interval: Duration,
21}
22
23impl Default for ReaperConfig {
24 fn default() -> Self {
25 Self {
26 scan_interval: Duration::from_secs(60),
27 }
28 }
29}
30
31pub struct SessionReaper {
34 coordinator: Arc<Mutex<Coordinator>>,
35 config: ReaperConfig,
36 running: Arc<std::sync::atomic::AtomicBool>,
37}
38
39impl SessionReaper {
40 pub fn new(coordinator: Arc<Mutex<Coordinator>>, config: ReaperConfig) -> Self {
42 Self {
43 coordinator,
44 config,
45 running: Arc::new(std::sync::atomic::AtomicBool::new(false)),
46 }
47 }
48
49 pub fn start(&self) {
51 self.running
52 .store(true, std::sync::atomic::Ordering::SeqCst);
53 let coordinator = Arc::clone(&self.coordinator);
54 let interval = self.config.scan_interval;
55 let running = Arc::clone(&self.running);
56
57 thread::spawn(move || {
58 while running.load(std::sync::atomic::Ordering::SeqCst) {
59 reap_expired_sessions(&coordinator);
60 thread::sleep(interval);
61 }
62 });
63 }
64
65 pub fn stop(&self) {
67 self.running
68 .store(false, std::sync::atomic::Ordering::SeqCst);
69 }
70
71 pub fn reap_once(&self) -> usize {
74 reap_expired_sessions(&self.coordinator)
75 }
76}
77
78fn reap_expired_sessions(coordinator: &Arc<Mutex<Coordinator>>) -> usize {
79 let mut coord = coordinator.lock().unwrap();
80 let now = Utc::now();
81
82 let pending: Vec<String> = coord
83 .session_ids()
84 .into_iter()
85 .filter(|sid| coord.session_state(sid) == Some(SessionState::Pending))
86 .collect();
87
88 let mut expired_count = 0;
89 for sid in pending {
90 let should_expire = coord
91 .session_mut(&sid)
92 .map(|s| !s.is_unlocked(now))
93 .unwrap_or(false);
94 if should_expire {
95 coord.set_session_state(&sid, SessionState::Expired);
96 coord
97 .audit_log_mut()
98 .append(sid.clone(), AuditEvent::Expired);
99 expired_count += 1;
100 tracing::info!(session = %sid, "session expired by reaper");
101 }
102 }
103 expired_count
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::coordinator::coordinator::Coordinator;
110 use crate::coordinator::session::SessionRequest;
111
112 fn make_request(unlock_minutes: i64) -> SessionRequest {
113 SessionRequest {
114 quorum_id: "q".into(),
115 scheme: "CMP20".into(),
116 message: vec![0; 32],
117 threshold: 2,
118 num_parties: 3,
119 unlock_window_minutes: unlock_minutes as u32,
120 requested_by: "test".into(),
121 }
122 }
123
124 #[test]
125 fn reap_expires_past_unlock_window() {
126 let coord = Arc::new(Mutex::new(Coordinator::new()));
127 coord
128 .lock()
129 .unwrap()
130 .create_session(make_request(0))
131 .unwrap();
132 std::thread::sleep(Duration::from_millis(1100));
133 let reaper = SessionReaper::new(Arc::clone(&coord), ReaperConfig::default());
134 let count = reaper.reap_once();
135 assert_eq!(count, 1);
136 }
137
138 #[test]
139 fn reap_preserves_active_sessions() {
140 let coord = Arc::new(Mutex::new(Coordinator::new()));
141 coord
142 .lock()
143 .unwrap()
144 .create_session(make_request(240))
145 .unwrap();
146 let reaper = SessionReaper::new(Arc::clone(&coord), ReaperConfig::default());
147 let count = reaper.reap_once();
148 assert_eq!(count, 0);
149 }
150
151 #[test]
152 fn reap_does_not_touch_completed_sessions() {
153 let coord = Arc::new(Mutex::new(Coordinator::new()));
154 let sid = coord
155 .lock()
156 .unwrap()
157 .create_session(make_request(0))
158 .unwrap();
159 coord
160 .lock()
161 .unwrap()
162 .set_session_state(&sid, SessionState::Completed);
163 std::thread::sleep(Duration::from_millis(1100));
164 let reaper = SessionReaper::new(Arc::clone(&coord), ReaperConfig::default());
165 let count = reaper.reap_once();
166 assert_eq!(count, 0);
167 }
168
169 #[test]
170 fn reap_multiple_expired() {
171 let coord = Arc::new(Mutex::new(Coordinator::new()));
172 coord
173 .lock()
174 .unwrap()
175 .create_session(make_request(0))
176 .unwrap();
177 coord
178 .lock()
179 .unwrap()
180 .create_session(make_request(0))
181 .unwrap();
182 coord
183 .lock()
184 .unwrap()
185 .create_session(make_request(240))
186 .unwrap();
187 std::thread::sleep(Duration::from_millis(1100));
188 let reaper = SessionReaper::new(Arc::clone(&coord), ReaperConfig::default());
189 let count = reaper.reap_once();
190 assert_eq!(count, 2);
191 }
192
193 #[test]
194 fn reap_empty_coordinator_returns_zero() {
195 let coord = Arc::new(Mutex::new(Coordinator::new()));
196 let reaper = SessionReaper::new(coord, ReaperConfig::default());
197 assert_eq!(reaper.reap_once(), 0);
198 }
199
200 #[test]
201 fn config_default_scan_interval_is_60s() {
202 let config = ReaperConfig::default();
203 assert_eq!(config.scan_interval, Duration::from_secs(60));
204 }
205}