use std::time::Instant;
use std::{sync::atomic::AtomicBool, time::Duration};
use tokio::sync::{Notify, RwLock};
use crate::{
config::Config,
constant::{
DEFAULT_JWK_BACKGROUND_REFRESH_THRESHOLD_PERCENT, DEFAULT_JWK_CACHE_TTL,
DEFAULT_JWK_REFRESH_BACKOFF, DEFAULT_JWK_REFRESH_COOLDOWN, DEFAULT_JWK_REFRESH_MAX_RETRIES,
DEFAULT_JWK_REFRESH_TIMEOUT, DEFAULT_JWK_URL,
},
model::oauth2::EveJwtKeys,
};
#[derive(Clone)]
pub(crate) struct JwtKeyCacheConfig {
pub(crate) cache_ttl: Duration,
pub(crate) jwk_url: String,
pub(crate) refresh_backoff: Duration,
pub(crate) refresh_timeout: Duration,
pub(crate) refresh_cooldown: Duration,
pub(crate) refresh_max_retries: u32,
pub(crate) background_refresh_enabled: bool,
pub(crate) background_refresh_threshold: u64,
}
pub(crate) struct JwtKeyCache {
pub(super) cache: RwLock<Option<(EveJwtKeys, Instant)>>,
pub(super) refresh_lock: AtomicBool,
pub(super) refresh_notifier: Notify,
pub(super) last_refresh_failure: RwLock<Option<Instant>>,
pub(super) config: JwtKeyCacheConfig,
}
impl JwtKeyCacheConfig {
pub(crate) fn new() -> Self {
Self {
cache_ttl: DEFAULT_JWK_CACHE_TTL,
jwk_url: DEFAULT_JWK_URL.to_string(),
refresh_max_retries: DEFAULT_JWK_REFRESH_MAX_RETRIES,
refresh_backoff: DEFAULT_JWK_REFRESH_BACKOFF,
refresh_timeout: DEFAULT_JWK_REFRESH_TIMEOUT,
refresh_cooldown: DEFAULT_JWK_REFRESH_COOLDOWN,
background_refresh_enabled: true,
background_refresh_threshold: DEFAULT_JWK_BACKGROUND_REFRESH_THRESHOLD_PERCENT,
}
}
}
impl JwtKeyCache {
pub(crate) fn new(config: &Config) -> Self {
Self {
cache: RwLock::new(None),
refresh_lock: AtomicBool::new(false),
refresh_notifier: Notify::new(),
last_refresh_failure: RwLock::new(None),
config: config.jwt_key_cache_config.clone(),
}
}
pub(super) async fn get_keys(&self) -> Option<(EveJwtKeys, std::time::Instant)> {
trace!("Attempting to retrieve JWT keys from cache");
let cache = self.cache.read().await;
if let Some((keys, timestamp)) = &*cache {
let elapsed = timestamp.elapsed().as_secs();
debug!(
"Found JWT keys in cache: key_count={}, age={}s",
keys.keys.len(),
elapsed
);
return Some((keys.clone(), *timestamp));
}
debug!("JWT keys cache is empty, keys need to be fetched");
None
}
pub(super) async fn update_keys(&self, keys: EveJwtKeys) {
let key_count = keys.keys.len();
let mut cache = self.cache.write().await;
*cache = Some((keys, std::time::Instant::now()));
let message = format!(
"JWT keys cache successfully updated with {} keys",
&key_count
);
debug!("{}", message);
}
pub(crate) async fn clear_cache(&self) -> bool {
let message = "Attempting to clear JWT key cache";
debug!("{}", message);
let mut cache = self.cache.write().await;
if let Some((_, timestamp)) = &*cache {
let sixty_seconds_ago = Instant::now() - self.config.refresh_cooldown;
if timestamp < &sixty_seconds_ago {
let elapsed = timestamp.elapsed().as_secs();
let message = format!(
"Clearing JWT key cache of keys that were set {}s ago",
elapsed
);
info!("{}", message);
*cache = None;
true
} else {
let message = format!(
"JWT key cache not cleared due to keys being within {} seconds of age",
self.config.refresh_cooldown.as_secs()
);
debug!("{}", message);
false
}
} else {
let message = "JWT key cache is currently empty, no need to clear it.";
debug!("{}", message);
false
}
}
pub(super) fn refresh_lock_try_acquire(&self) -> bool {
let lock_acquired = self.refresh_lock.compare_exchange(
false,
true,
std::sync::atomic::Ordering::Acquire,
std::sync::atomic::Ordering::Relaxed,
);
if lock_acquired.is_ok() {
let message = "Successfully acquired JWT key refresh lock";
debug!("{}", message);
true
} else {
let message = "Failed to acquire JWT key refresh lock (already held by another thread)";
trace!("{}", message);
false
}
}
pub(super) fn refresh_lock_release_and_notify(&self) {
self.refresh_lock
.store(false, std::sync::atomic::Ordering::Release);
self.refresh_notifier.notify_waiters();
let message = "JWT key refresh lock released and waiters notified";
debug!("{}", message);
}
pub(super) async fn set_refresh_failure(&self, failure_timstamp: Option<Instant>) {
let mut failure_time = self.last_refresh_failure.write().await;
*failure_time = failure_timstamp;
}
}
#[cfg(test)]
mod cache_get_keys_tests {
use crate::Client;
use super::super::tests::create_mock_keys;
#[tokio::test]
async fn test_cache_get_keys_some() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
{
let keys = (create_mock_keys(), std::time::Instant::now());
let mut cache = jwt_key_cache.cache.write().await;
*cache = Some(keys);
}
let result = jwt_key_cache.get_keys().await;
assert!(result.is_some())
}
#[tokio::test]
async fn test_cache_get_keys_none() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let result = jwt_key_cache.get_keys().await;
assert!(result.is_none())
}
}
#[cfg(test)]
mod cache_update_keys_tests {
use crate::Client;
use super::super::tests::create_mock_keys;
#[tokio::test]
async fn test_cache_update_keys() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock_keys = create_mock_keys();
jwt_key_cache.update_keys(mock_keys).await;
let cache = jwt_key_cache.cache.read().await;
let result = &*cache;
assert!(result.is_some())
}
}
#[cfg(test)]
mod clear_cache_tests {
use std::time::{Duration, Instant};
use super::super::tests::create_mock_keys;
use crate::tests::setup;
#[tokio::test]
async fn cache_clear_success() {
let (esi_client, _) = setup().await;
{
let mock_keys = create_mock_keys();
let timestamp = Instant::now() - Duration::from_secs(61);
let mut cache = esi_client.inner.jwt_key_cache.cache.write().await;
*cache = Some((mock_keys, timestamp))
}
let cache_cleared = esi_client.inner.jwt_key_cache.clear_cache().await;
assert_eq!(cache_cleared, true);
let cache = esi_client.inner.jwt_key_cache.get_keys().await;
assert!(cache.is_none())
}
#[tokio::test]
async fn cache_clear_recent_keys() {
let (esi_client, _) = setup().await;
let mock_keys = create_mock_keys();
esi_client.inner.jwt_key_cache.update_keys(mock_keys).await;
let lock_acquired = esi_client.inner.jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock_acquired, true);
let cache_cleared = esi_client.inner.jwt_key_cache.clear_cache().await;
assert_eq!(cache_cleared, false);
let cache = esi_client.inner.jwt_key_cache.get_keys().await;
assert!(cache.is_some())
}
}
#[cfg(test)]
mod jwk_refresh_lock_try_acquire_tests {
use crate::Client;
#[test]
fn test_jwk_refresh_lock_try_acquire_success() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let lock_acquired = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock_acquired, true)
}
#[test]
fn test_jwk_refresh_lock_try_acquire_unsuccessful() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let lock_acquired = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock_acquired, true);
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let lock_acquired = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock_acquired, false)
}
}
#[cfg(test)]
mod jwk_lock_release_and_notify_tests {
use std::time::Duration;
use crate::Client;
#[tokio::test]
async fn test_jwk_refresh_lock_release_and_notify_success() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let lock = !jwt_key_cache
.refresh_lock
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::Acquire,
std::sync::atomic::Ordering::Relaxed,
)
.is_err();
assert_eq!(lock, true);
let notification = jwt_key_cache.refresh_notifier.notified();
let timeout = tokio::time::sleep(Duration::from_millis(50));
jwt_key_cache.refresh_lock_release_and_notify();
let notified = tokio::select! {
_ = notification => true,
_ = timeout => false
};
assert_eq!(notified, true);
let lock = !jwt_key_cache
.refresh_lock
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::Acquire,
std::sync::atomic::Ordering::Relaxed,
)
.is_err();
assert_eq!(lock, true)
}
}
#[cfg(test)]
mod set_refresh_failure_tests {
use crate::Client;
#[tokio::test]
async fn test_set_refresh_failure_some() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let timestamp = std::time::Instant::now();
jwt_key_cache.set_refresh_failure(Some(timestamp)).await;
let result = jwt_key_cache.last_refresh_failure.read().await;
assert!(result.is_some());
let failure_time = result.unwrap();
assert_eq!(failure_time, timestamp)
}
#[tokio::test]
async fn test_set_refresh_failure_none() {
let esi_client = Client::builder()
.user_agent("MyApp/1.0 (contact@example.com)")
.build()
.expect("Failed to build Client");
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
{
let mut failure_time = jwt_key_cache.last_refresh_failure.write().await;
*failure_time = Some(std::time::Instant::now())
}
jwt_key_cache.set_refresh_failure(None).await;
let failure_time = jwt_key_cache.last_refresh_failure.read().await;
assert!(failure_time.is_none())
}
}