1use parking_lot::RwLock;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
15pub struct RelayReservation {
16 pub relay_peer_id: String,
18 pub relay_addr: String,
20 pub reserved_at_ms: u64,
22 pub expires_at_ms: u64,
24 pub voucher: Option<Vec<u8>>,
26}
27
28impl RelayReservation {
29 #[inline]
31 pub fn is_expired(&self, now_ms: u64) -> bool {
32 now_ms >= self.expires_at_ms
33 }
34
35 #[inline]
37 pub fn ttl_remaining_ms(&self, now_ms: u64) -> u64 {
38 self.expires_at_ms.saturating_sub(now_ms)
39 }
40}
41
42#[derive(Debug)]
46pub enum RelayError {
47 AtCapacity {
49 max: usize,
51 },
52 AlreadyExists {
54 relay_peer_id: String,
56 },
57}
58
59impl std::fmt::Display for RelayError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::AtCapacity { max } => {
63 write!(f, "relay manager at capacity (max {max} reservations)")
64 }
65 Self::AlreadyExists { relay_peer_id } => {
66 write!(
67 f,
68 "relay reservation already exists for peer {relay_peer_id}"
69 )
70 }
71 }
72 }
73}
74
75impl std::error::Error for RelayError {}
76
77pub struct RelayManager {
85 reservations: RwLock<HashMap<String, RelayReservation>>,
87 max_reservations: usize,
89}
90
91impl RelayManager {
92 pub fn new(max_reservations: usize) -> Self {
94 Self {
95 reservations: RwLock::new(HashMap::new()),
96 max_reservations,
97 }
98 }
99
100 pub fn add_reservation(&self, reservation: RelayReservation) -> Result<(), RelayError> {
107 let mut map = self.reservations.write();
108 if map.contains_key(&reservation.relay_peer_id) {
109 return Err(RelayError::AlreadyExists {
110 relay_peer_id: reservation.relay_peer_id.clone(),
111 });
112 }
113 if map.len() >= self.max_reservations {
114 return Err(RelayError::AtCapacity {
115 max: self.max_reservations,
116 });
117 }
118 map.insert(reservation.relay_peer_id.clone(), reservation);
119 Ok(())
120 }
121
122 pub fn remove_reservation(&self, relay_peer_id: &str) -> bool {
126 self.reservations.write().remove(relay_peer_id).is_some()
127 }
128
129 pub fn get_reservation(&self, relay_peer_id: &str) -> Option<RelayReservation> {
131 self.reservations.read().get(relay_peer_id).cloned()
132 }
133
134 pub fn active_reservations(&self, now_ms: u64) -> Vec<RelayReservation> {
136 self.reservations
137 .read()
138 .values()
139 .filter(|r| !r.is_expired(now_ms))
140 .cloned()
141 .collect()
142 }
143
144 pub fn expired_reservations(&self, now_ms: u64) -> Vec<String> {
146 self.reservations
147 .read()
148 .values()
149 .filter(|r| r.is_expired(now_ms))
150 .map(|r| r.relay_peer_id.clone())
151 .collect()
152 }
153
154 pub fn prune_expired(&self, now_ms: u64) -> usize {
156 let mut map = self.reservations.write();
157 let before = map.len();
158 map.retain(|_, r| !r.is_expired(now_ms));
159 before - map.len()
160 }
161
162 pub fn reservation_count(&self) -> usize {
164 self.reservations.read().len()
165 }
166
167 pub fn is_at_capacity(&self) -> bool {
169 self.reservations.read().len() >= self.max_reservations
170 }
171}
172
173#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn make_reservation(peer_id: &str, reserved_at: u64, expires_at: u64) -> RelayReservation {
180 RelayReservation {
181 relay_peer_id: peer_id.to_string(),
182 relay_addr: format!("/ip4/127.0.0.1/tcp/4001/p2p/{peer_id}"),
183 reserved_at_ms: reserved_at,
184 expires_at_ms: expires_at,
185 voucher: None,
186 }
187 }
188
189 #[test]
190 fn test_add_and_get_reservation() {
191 let mgr = RelayManager::new(4);
192 let res = make_reservation("peer1", 1000, 5000);
193 mgr.add_reservation(res).expect("should succeed");
194 let got = mgr.get_reservation("peer1").expect("should be present");
195 assert_eq!(got.relay_peer_id, "peer1");
196 assert_eq!(got.reserved_at_ms, 1000);
197 assert_eq!(got.expires_at_ms, 5000);
198 }
199
200 #[test]
201 fn test_remove_reservation() {
202 let mgr = RelayManager::new(4);
203 mgr.add_reservation(make_reservation("peer1", 0, 9999))
204 .expect("add ok");
205 assert!(mgr.remove_reservation("peer1"));
206 assert!(!mgr.remove_reservation("peer1")); assert!(mgr.get_reservation("peer1").is_none());
208 }
209
210 #[test]
211 fn test_prune_expired() {
212 let mgr = RelayManager::new(10);
213 mgr.add_reservation(make_reservation("alive", 0, 10_000))
214 .expect("ok");
215 mgr.add_reservation(make_reservation("dead1", 0, 100))
216 .expect("ok");
217 mgr.add_reservation(make_reservation("dead2", 0, 200))
218 .expect("ok");
219
220 let removed = mgr.prune_expired(500);
221 assert_eq!(removed, 2);
222 assert_eq!(mgr.reservation_count(), 1);
223 assert!(mgr.get_reservation("alive").is_some());
224 }
225
226 #[test]
227 fn test_active_reservations_filters_expired() {
228 let mgr = RelayManager::new(10);
229 mgr.add_reservation(make_reservation("alive", 0, 10_000))
230 .expect("ok");
231 mgr.add_reservation(make_reservation("expired", 0, 50))
232 .expect("ok");
233
234 let active = mgr.active_reservations(100);
235 assert_eq!(active.len(), 1);
236 assert_eq!(active[0].relay_peer_id, "alive");
237 }
238
239 #[test]
240 fn test_capacity_limit() {
241 let mgr = RelayManager::new(2);
242 mgr.add_reservation(make_reservation("p1", 0, 9999))
243 .expect("ok");
244 mgr.add_reservation(make_reservation("p2", 0, 9999))
245 .expect("ok");
246 let err = mgr
247 .add_reservation(make_reservation("p3", 0, 9999))
248 .expect_err("should fail at capacity");
249 assert!(
250 matches!(err, RelayError::AtCapacity { max: 2 }),
251 "expected AtCapacity, got {err}"
252 );
253 }
254
255 #[test]
256 fn test_duplicate_reservation_error() {
257 let mgr = RelayManager::new(4);
258 mgr.add_reservation(make_reservation("peer1", 0, 9999))
259 .expect("first add ok");
260 let err = mgr
261 .add_reservation(make_reservation("peer1", 100, 9999))
262 .expect_err("duplicate should fail");
263 assert!(
264 matches!(err, RelayError::AlreadyExists { .. }),
265 "expected AlreadyExists, got {err}"
266 );
267 }
268
269 #[test]
270 fn test_reservation_ttl_remaining() {
271 let res = make_reservation("p", 0, 5000);
272 assert_eq!(res.ttl_remaining_ms(3000), 2000);
273 assert_eq!(res.ttl_remaining_ms(5000), 0); assert_eq!(res.ttl_remaining_ms(6000), 0); }
276
277 #[test]
278 fn test_reservation_is_expired() {
279 let res = make_reservation("p", 0, 5000);
280 assert!(!res.is_expired(4999));
281 assert!(res.is_expired(5000));
282 assert!(res.is_expired(9999));
283 }
284}