use std::time::Instant;
use ::tokio::time::Duration;
use crate::error::{Error, OAuthError};
use crate::model::oauth2::EveJwtKeys;
use crate::oauth2::jwk::cache::JwtKeyCache;
use super::util::check_refresh_cooldown;
use super::{fetch_and_update_cache, JwkApi};
impl<'a> JwkApi<'a> {
pub(super) async fn wait_for_ongoing_refresh(&self) -> Result<EveJwtKeys, Error> {
let esi_client = self.client;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let config = &jwt_key_cache.config;
let start_time = Instant::now();
debug!("Waiting for another thread to refresh JWT keys");
let notify_future = jwt_key_cache.refresh_notifier.notified();
trace!("Created notification future for JWT key refresh wait");
let refresh_timeout = config.refresh_timeout;
let refresh_success = tokio::select! {
_ = notify_future => {true}
_ = tokio::time::sleep(refresh_timeout) => {false}
};
let elapsed = start_time.elapsed();
if !refresh_success {
let error_message = format!(
"Timed out after waiting {}ms for JWT key refresh.",
elapsed.as_millis()
);
debug!(error_message);
return Err(Error::OAuthError(OAuthError::JwtKeyRefreshTimeout(
error_message,
)));
}
if let Some((keys, timestamp)) = jwt_key_cache.get_keys().await {
let elapsed_seconds = timestamp.elapsed().as_millis();
if elapsed_seconds < config.cache_ttl.as_millis() {
debug!(
"Successfully retrieved JWT keys from cache after waiting {}ms for refresh",
elapsed.as_millis()
);
return Ok(keys);
}
}
let error_message = format!(
"JWT key cache still empty after waiting {}ms for refresh. Likely due to a failure to refresh the keys.",
elapsed.as_millis()
);
debug!("{}", error_message);
Err(Error::OAuthError(OAuthError::JwtKeyRefreshFailure(
error_message,
)))
}
pub(super) async fn trigger_background_jwt_refresh(&self) -> bool {
let esi_client = self.client;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
if check_refresh_cooldown(jwt_key_cache).await.is_some() {
debug!("Respecting refresh cooldown, delaying JWT key refresh");
return false;
}
if !jwt_key_cache.refresh_lock_try_acquire() {
debug!("JWT key refresh already in progress");
return false;
}
debug!("Triggering background JWT refresh task");
let client_ref = esi_client.inner.clone();
tokio::spawn(async move {
refresh_jwt_keys(&client_ref.reqwest_client, &client_ref.jwt_key_cache, 0).await
});
debug!("Background JWT key refresh task started");
true
}
}
pub(super) async fn refresh_jwt_keys(
reqwest_client: &reqwest::Client,
jwt_key_cache: &JwtKeyCache,
max_retries: u32,
) -> Result<EveJwtKeys, Error> {
let config = &jwt_key_cache.config;
let start_time = std::time::Instant::now();
trace!("Fetching JWT keys from JWK URL: {}", &config.jwk_url);
let mut result = fetch_and_update_cache(reqwest_client, jwt_key_cache).await;
let mut retry_attempts = 0;
while result.is_err() && retry_attempts < max_retries {
let backoff_duration = Duration::from_millis(
config.refresh_backoff.as_millis() as u64 * 2u64.pow(retry_attempts),
);
debug!(
"JWT key fetch failed. Retrying ({}/{}) after {}ms",
retry_attempts + 1,
config.refresh_max_retries,
backoff_duration.as_millis()
);
tokio::time::sleep(backoff_duration).await;
debug!(
"Retry attempt # {}: fetching JWT keys after backoff",
retry_attempts + 1
);
result = fetch_and_update_cache(reqwest_client, jwt_key_cache).await;
retry_attempts += 1;
}
jwt_key_cache.refresh_lock_release_and_notify();
let elapsed = start_time.elapsed();
match result {
Ok(keys) => {
info!(
"Successfully fetched and cached {} JWT keys for token validation (took {}ms)",
keys.keys.len(),
elapsed.as_millis()
);
jwt_key_cache.set_refresh_failure(None).await;
debug!("Cleared previous JWT key refresh failure timestamp");
Ok(keys)
}
Err(err) => {
error!(
"JWT key refresh failed after {}ms: attempts={}, backoff_period={}ms, error={:?}",
elapsed.as_millis(),
retry_attempts,
config.refresh_backoff.as_millis(),
err
);
jwt_key_cache
.set_refresh_failure(Some(std::time::Instant::now()))
.await;
debug!("Recorded JWT key refresh failure timestamp");
Err(err)
}
}
}
#[cfg(test)]
mod wait_for_ongoing_refresh_tests {
use crate::error::Error;
use crate::oauth2::error::OAuthError;
use crate::tests::setup;
use super::super::tests::{create_mock_keys, get_jwk_internal_server_error_response};
#[tokio::test]
async fn test_wait_for_refresh_success() {
let (esi_client, mut mock_server) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock = get_jwk_internal_server_error_response(&mut mock_server, 0);
let lock = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock, true);
let (tx, rx) = tokio::sync::oneshot::channel();
let keys = create_mock_keys();
let keys_clone = keys.clone();
let client_ref = esi_client.inner.clone();
tokio::spawn(async move {
let _ = tx.send(());
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
client_ref.jwt_key_cache.update_keys(keys_clone).await;
client_ref.jwt_key_cache.refresh_lock_release_and_notify();
});
rx.await.expect("Failed to receive ready signal");
let result = esi_client.oauth2().jwk().wait_for_ongoing_refresh().await;
mock.assert();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_wait_for_refresh_failure_empty_cache() {
let (esi_client, mut mock_server) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock = get_jwk_internal_server_error_response(&mut mock_server, 0);
let lock = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock, true);
let (tx, rx) = tokio::sync::oneshot::channel();
let client_ref = esi_client.inner.clone();
tokio::spawn(async move {
let _ = tx.send(());
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
client_ref.jwt_key_cache.refresh_lock_release_and_notify();
});
rx.await.expect("Failed to receive ready signal");
let result = esi_client.oauth2().jwk().wait_for_ongoing_refresh().await;
mock.assert();
assert!(result.is_err());
assert!(matches!(
result,
Err(Error::OAuthError(OAuthError::JwtKeyRefreshFailure(_)))
));
}
#[tokio::test]
async fn test_wait_for_refresh_failure_expired_cache() {
let (esi_client, mut mock_server) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock = get_jwk_internal_server_error_response(&mut mock_server, 0);
{
let expired_timestamp =
std::time::Instant::now() - std::time::Duration::from_secs(3601);
let mut cache = jwt_key_cache.cache.write().await;
*cache = Some((create_mock_keys(), expired_timestamp));
}
let lock = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock, true);
let (tx, rx) = tokio::sync::oneshot::channel();
let client_ref = esi_client.inner.clone();
tokio::spawn(async move {
let _ = tx.send(());
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
client_ref.jwt_key_cache.refresh_lock_release_and_notify();
});
rx.await.expect("Failed to receive ready signal");
let result = esi_client.oauth2().jwk().wait_for_ongoing_refresh().await;
mock.assert();
assert!(result.is_err());
assert!(matches!(
result,
Err(Error::OAuthError(OAuthError::JwtKeyRefreshFailure(_)))
));
}
#[tokio::test]
async fn test_wait_for_refresh_timeout() {
let (esi_client, mut mock_server) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock = get_jwk_internal_server_error_response(&mut mock_server, 0);
let lock = jwt_key_cache.refresh_lock_try_acquire();
assert_eq!(lock, true);
let result = esi_client.oauth2().jwk().wait_for_ongoing_refresh().await;
mock.assert();
assert!(result.is_err());
assert!(matches!(
result,
Err(Error::OAuthError(OAuthError::JwtKeyRefreshTimeout(_)))
));
}
}
#[cfg(test)]
mod trigger_background_jwt_refresh_test {
use std::time::Duration;
use crate::tests::setup;
#[tokio::test]
async fn test_background_refresh() {
let (esi_client, _) = setup().await;
let result = esi_client
.oauth2()
.jwk()
.trigger_background_jwt_refresh()
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(result, true);
}
#[tokio::test]
async fn test_background_refresh_cooldown() {
let (esi_client, _) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
{
let last_failure = std::time::Instant::now() - std::time::Duration::from_secs(30);
let mut failure_time = jwt_key_cache.last_refresh_failure.write().await;
*failure_time = Some(last_failure);
}
let refresh_triggered = esi_client
.oauth2()
.jwk()
.trigger_background_jwt_refresh()
.await;
assert_eq!(refresh_triggered, false)
}
#[tokio::test]
async fn test_background_refresh_already_in_progress() {
let (esi_client, _) = setup().await;
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 refresh_triggered = esi_client
.oauth2()
.jwk()
.trigger_background_jwt_refresh()
.await;
assert_eq!(refresh_triggered, false);
}
}
#[cfg(test)]
mod refresh_jwt_keys_tests {
use crate::tests::setup;
use crate::{error::Error, oauth2::jwk::refresh::refresh_jwt_keys};
use super::super::tests::{get_jwk_internal_server_error_response, get_jwk_success_response};
#[tokio::test]
async fn test_refresh_keys_success() {
let (esi_client, mut mock_server) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock = get_jwk_success_response(&mut mock_server, 1);
let result = refresh_jwt_keys(
&esi_client.inner.reqwest_client,
&esi_client.inner.jwt_key_cache,
esi_client.inner.jwt_key_cache.config.refresh_max_retries,
)
.await;
mock.assert();
assert!(result.is_ok());
let cache = jwt_key_cache.cache.read().await;
assert!(*&cache.is_some())
}
#[tokio::test]
async fn test_refresh_keys_failure() {
let (esi_client, mut mock_server) = setup().await;
let mock = get_jwk_internal_server_error_response(&mut mock_server, 3);
let result = refresh_jwt_keys(
&esi_client.inner.reqwest_client,
&esi_client.inner.jwt_key_cache,
esi_client.inner.jwt_key_cache.config.refresh_max_retries,
)
.await;
mock.assert();
assert!(result.is_err());
assert!(matches!(result, Err(Error::ReqwestError(_))));
assert!(
matches!(result, Err(Error::ReqwestError(ref e)) if e.status() == Some(reqwest::StatusCode::INTERNAL_SERVER_ERROR))
);
}
#[tokio::test]
async fn test_refresh_keys_retry() {
let (esi_client, mut mock_server) = setup().await;
let jwt_key_cache = &esi_client.inner.jwt_key_cache;
let mock_500 = get_jwk_internal_server_error_response(&mut mock_server, 1);
let mock_200 = get_jwk_success_response(&mut mock_server, 1);
let result = refresh_jwt_keys(
&esi_client.inner.reqwest_client,
&esi_client.inner.jwt_key_cache,
esi_client.inner.jwt_key_cache.config.refresh_max_retries,
)
.await;
mock_500.assert();
mock_200.assert();
assert!(result.is_ok());
let cache = jwt_key_cache.cache.read().await;
assert!(*&cache.is_some())
}
}