use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::cookies::CapturedCookie;
use crate::solver::CaptchaType;
#[derive(Debug, Clone, Copy)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub expired_misses: u64,
pub puts: u64,
pub invalidations: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> Option<f64> {
let lookups = self.hits + self.misses + self.expired_misses;
if lookups == 0 {
None
} else {
Some(self.hits as f64 / lookups as f64)
}
}
}
#[derive(Debug, Clone)]
pub struct CachedToken {
token: String,
method_name: &'static str,
expires_at: Instant,
cookies: Vec<CapturedCookie>,
}
pub struct TokenCache {
inner: Mutex<HashMap<(String, CaptchaType), CachedToken>>,
default_ttl: Duration,
hits: AtomicU64,
misses: AtomicU64,
expired_misses: AtomicU64,
puts: AtomicU64,
invalidations: AtomicU64,
}
impl TokenCache {
pub fn new() -> Self {
Self::with_ttl(Duration::from_secs(60))
}
pub fn with_ttl(default_ttl: Duration) -> Self {
Self {
inner: Mutex::new(HashMap::new()),
default_ttl,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
expired_misses: AtomicU64::new(0),
puts: AtomicU64::new(0),
invalidations: AtomicU64::new(0),
}
}
pub fn get(&self, domain: &str, captcha_type: &CaptchaType) -> Option<CachedToken> {
let map = self.inner.lock().unwrap();
let key = (domain.to_owned(), captcha_type.clone());
match map.get(&key) {
None => {
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
Some(entry) if entry.expires_at > Instant::now() => {
self.hits.fetch_add(1, Ordering::Relaxed);
Some(entry.clone())
}
Some(_) => {
self.expired_misses.fetch_add(1, Ordering::Relaxed);
None
}
}
}
pub fn get_token(&self, domain: &str, captcha_type: &CaptchaType) -> Option<String> {
self.get(domain, captcha_type).map(|e| e.token)
}
pub fn put(
&self,
domain: &str,
captcha_type: &CaptchaType,
token: String,
method_name: &'static str,
) {
self.put_with_ttl(domain, captcha_type, token, method_name, self.default_ttl);
}
pub fn put_with_ttl(
&self,
domain: &str,
captcha_type: &CaptchaType,
token: String,
method_name: &'static str,
ttl: Duration,
) {
self.put_full(domain, captcha_type, token, method_name, ttl, Vec::new());
}
pub fn put_full(
&self,
domain: &str,
captcha_type: &CaptchaType,
token: String,
method_name: &'static str,
ttl: Duration,
cookies: Vec<CapturedCookie>,
) {
let mut map = self.inner.lock().unwrap();
map.insert(
(domain.to_owned(), captcha_type.clone()),
CachedToken {
token,
method_name,
expires_at: Instant::now() + ttl,
cookies,
},
);
self.puts.fetch_add(1, Ordering::Relaxed);
}
pub fn invalidate(&self, domain: &str, captcha_type: &CaptchaType) {
let mut map = self.inner.lock().unwrap();
if map
.remove(&(domain.to_owned(), captcha_type.clone()))
.is_some()
{
self.invalidations.fetch_add(1, Ordering::Relaxed);
}
}
pub fn clear(&self) {
self.inner.lock().unwrap().clear();
}
pub fn len(&self) -> usize {
self.inner.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn ttl(&self) -> Duration {
self.default_ttl
}
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
expired_misses: self.expired_misses.load(Ordering::Relaxed),
puts: self.puts.load(Ordering::Relaxed),
invalidations: self.invalidations.load(Ordering::Relaxed),
}
}
pub fn reset_stats(&self) {
self.hits.store(0, Ordering::Relaxed);
self.misses.store(0, Ordering::Relaxed);
self.expired_misses.store(0, Ordering::Relaxed);
self.puts.store(0, Ordering::Relaxed);
self.invalidations.store(0, Ordering::Relaxed);
}
}
impl CachedToken {
pub fn token(&self) -> &str {
&self.token
}
pub fn method_name(&self) -> &'static str {
self.method_name
}
pub fn cookies(&self) -> &[CapturedCookie] {
&self.cookies
}
}
impl Default for TokenCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "cache/tests.rs"]
mod tests;