#[derive(Clone)]
pub struct KeyPool {
keys: std::sync::Arc<Vec<String>>,
index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
rotations: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl KeyPool {
pub fn new(primary: &str, extra: &[String]) -> Self {
let mut keys: Vec<String> = Vec::with_capacity(1 + extra.len());
keys.push(primary.to_string());
for key in extra {
let key = key.trim();
if !key.is_empty() && !keys.iter().any(|k| k == key) {
keys.push(key.to_string());
}
}
KeyPool {
keys: std::sync::Arc::new(keys),
index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
rotations: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
}
}
pub fn current(&self) -> String {
let idx = self.index.load(std::sync::atomic::Ordering::Relaxed) % self.keys.len();
self.keys[idx].clone()
}
pub fn advance(&self) -> bool {
if self.keys.len() <= 1 {
return false;
}
self.index
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.rotations
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
true
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn rotations(&self) -> u64 {
self.rotations.load(std::sync::atomic::Ordering::Relaxed)
}
}
pub(crate) fn is_key_rotation_status(status: u16) -> bool {
matches!(status, 401 | 403 | 429)
}
#[cfg(test)]
mod key_pool_tests {
use super::*;
#[test]
fn key_pool_rotates_round_robin_and_dedupes() {
let pool = KeyPool::new("k1", &["k2".into(), " ".into(), "k1".into(), "k3".into()]);
assert_eq!(pool.len(), 3);
assert_eq!(pool.current(), "k1");
assert!(pool.advance());
assert_eq!(pool.current(), "k2");
assert!(pool.advance());
assert_eq!(pool.current(), "k3");
assert!(pool.advance());
assert_eq!(pool.current(), "k1");
assert_eq!(pool.rotations(), 3);
}
#[test]
fn single_key_pool_never_advances() {
let pool = KeyPool::new("only", &[]);
assert!(!pool.advance());
assert_eq!(pool.current(), "only");
assert_eq!(pool.rotations(), 0);
}
#[test]
fn rotation_statuses() {
assert!(is_key_rotation_status(401));
assert!(is_key_rotation_status(403));
assert!(is_key_rotation_status(429));
assert!(!is_key_rotation_status(500));
}
}