use log::warn;
use serde::{Deserialize, Serialize};
use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use tokio::{
sync::{Mutex, RwLock},
time::interval,
};
use crate::core::{
api_resp::{ApiResponseTrait, RawResponse, ResponseFormat},
app_ticket_manager::AppTicketManager,
cache::QuickCache,
config::Config,
constants::{
AppType, APP_ACCESS_TOKEN_INTERNAL_URL_PATH, APP_ACCESS_TOKEN_KEY_PREFIX,
APP_ACCESS_TOKEN_URL_PATH, EXPIRY_DELTA, TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH,
TENANT_ACCESS_TOKEN_URL_PATH,
},
error::LarkAPIError,
SDKResult,
};
#[derive(Debug, Clone)]
pub struct PreheatingConfig {
pub check_interval_seconds: u64,
pub preheat_threshold_seconds: u64,
pub enable_tenant_preheating: bool,
pub max_concurrent_preheat: usize,
}
impl Default for PreheatingConfig {
fn default() -> Self {
Self {
check_interval_seconds: 1800, preheat_threshold_seconds: 900, enable_tenant_preheating: true,
max_concurrent_preheat: 3,
}
}
}
#[derive(Debug, Default)]
pub struct TokenMetrics {
pub app_cache_hits: AtomicU64,
pub app_cache_misses: AtomicU64,
pub tenant_cache_hits: AtomicU64,
pub tenant_cache_misses: AtomicU64,
pub refresh_success: AtomicU64,
pub refresh_failures: AtomicU64,
pub read_lock_acquisitions: AtomicU64,
pub write_lock_acquisitions: AtomicU64,
}
impl TokenMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn app_cache_hit_rate(&self) -> f64 {
let hits = self.app_cache_hits.load(Ordering::Relaxed) as f64;
let misses = self.app_cache_misses.load(Ordering::Relaxed) as f64;
let total = hits + misses;
if total > 0.0 {
hits / total
} else {
0.0
}
}
pub fn tenant_cache_hit_rate(&self) -> f64 {
let hits = self.tenant_cache_hits.load(Ordering::Relaxed) as f64;
let misses = self.tenant_cache_misses.load(Ordering::Relaxed) as f64;
let total = hits + misses;
if total > 0.0 {
hits / total
} else {
0.0
}
}
pub fn refresh_success_rate(&self) -> f64 {
let success = self.refresh_success.load(Ordering::Relaxed) as f64;
let failures = self.refresh_failures.load(Ordering::Relaxed) as f64;
let total = success + failures;
if total > 0.0 {
success / total
} else {
0.0
}
}
pub fn performance_report(&self) -> String {
format!(
"TokenManager Performance Metrics:\n\
- App Cache Hit Rate: {:.2}%\n\
- Tenant Cache Hit Rate: {:.2}%\n\
- Refresh Success Rate: {:.2}%\n\
- Total Read Locks: {}\n\
- Total Write Locks: {}\n\
- App Cache: {} hits, {} misses\n\
- Tenant Cache: {} hits, {} misses\n\
- Refreshes: {} success, {} failures",
self.app_cache_hit_rate() * 100.0,
self.tenant_cache_hit_rate() * 100.0,
self.refresh_success_rate() * 100.0,
self.read_lock_acquisitions.load(Ordering::Relaxed),
self.write_lock_acquisitions.load(Ordering::Relaxed),
self.app_cache_hits.load(Ordering::Relaxed),
self.app_cache_misses.load(Ordering::Relaxed),
self.tenant_cache_hits.load(Ordering::Relaxed),
self.tenant_cache_misses.load(Ordering::Relaxed),
self.refresh_success.load(Ordering::Relaxed),
self.refresh_failures.load(Ordering::Relaxed)
)
}
}
#[derive(Debug)]
pub struct TokenManager {
cache: Arc<RwLock<QuickCache<String>>>,
metrics: Arc<TokenMetrics>,
preheating_handle: Option<tokio::task::JoinHandle<()>>,
}
impl Default for TokenManager {
fn default() -> Self {
Self::new()
}
}
impl TokenManager {
pub fn new() -> Self {
Self {
cache: Arc::new(RwLock::new(QuickCache::new())),
metrics: Arc::new(TokenMetrics::new()),
preheating_handle: None,
}
}
pub fn metrics(&self) -> &Arc<TokenMetrics> {
&self.metrics
}
pub fn get_cache(&self) -> Arc<RwLock<QuickCache<String>>> {
self.cache.clone()
}
pub fn get_metrics(&self) -> Arc<TokenMetrics> {
self.metrics.clone()
}
pub fn log_performance_metrics(&self) {
log::info!("{}", self.metrics.performance_report());
}
pub fn start_background_preheating(
&mut self,
config: Config,
app_ticket_manager: Arc<Mutex<AppTicketManager>>,
) {
self.start_background_preheating_with_config(
config,
app_ticket_manager,
PreheatingConfig::default(),
)
}
pub fn start_background_preheating_with_config(
&mut self,
config: Config,
app_ticket_manager: Arc<Mutex<AppTicketManager>>,
preheat_config: PreheatingConfig,
) {
if self.preheating_handle.is_some() {
log::info!("🔄 停止现有预热任务,启动新配置的预热任务");
self.stop_background_preheating();
}
let cache = self.cache.clone();
let metrics = self.metrics.clone();
let handle = tokio::spawn(async move {
let mut interval = interval(Duration::from_secs(preheat_config.check_interval_seconds));
log::info!(
"🔄 Token后台预热机制已启动,检查间隔: {}分钟,预热阈值: {}分钟",
preheat_config.check_interval_seconds / 60,
preheat_config.preheat_threshold_seconds / 60
);
loop {
interval.tick().await;
if let Err(e) = Self::preheat_tokens_if_needed_with_config(
&cache,
&metrics,
&config,
&app_ticket_manager,
&preheat_config,
)
.await
{
log::warn!("⚠️ Token预热过程中发生错误: {e:?}");
}
}
});
self.preheating_handle = Some(handle);
log::info!("✅ Token后台预热任务已启动并注册到TokenManager");
}
#[allow(dead_code)]
async fn preheat_tokens_if_needed(
cache: &Arc<RwLock<QuickCache<String>>>,
metrics: &Arc<TokenMetrics>,
config: &Config,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<()> {
Self::preheat_tokens_if_needed_with_config(
cache,
metrics,
config,
app_ticket_manager,
&PreheatingConfig::default(),
)
.await
}
async fn preheat_tokens_if_needed_with_config(
cache: &Arc<RwLock<QuickCache<String>>>,
metrics: &Arc<TokenMetrics>,
config: &Config,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
preheat_config: &PreheatingConfig,
) -> SDKResult<()> {
log::debug!("🔍 检查需要预热的token...");
let mut preheated_count = 0;
let app_key = app_access_token_key(&config.app_id);
if Self::should_preheat_token_with_threshold(
cache,
&app_key,
preheat_config.preheat_threshold_seconds,
)
.await
{
log::info!("🔄 开始预热 app access token");
if let Err(e) = Self::preheat_app_token(cache, config, app_ticket_manager).await {
log::warn!("❌ App token预热失败: {e:?}");
metrics.refresh_failures.fetch_add(1, Ordering::Relaxed);
} else {
log::info!("✅ App token预热成功");
metrics.refresh_success.fetch_add(1, Ordering::Relaxed);
preheated_count += 1;
}
}
if preheat_config.enable_tenant_preheating {
let tenant_keys = Self::get_cached_tenant_keys(cache, &config.app_id).await;
for tenant_key in tenant_keys
.into_iter()
.take(preheat_config.max_concurrent_preheat)
{
let tenant_cache_key = tenant_access_token_key(&config.app_id, &tenant_key);
if Self::should_preheat_token_with_threshold(
cache,
&tenant_cache_key,
preheat_config.preheat_threshold_seconds,
)
.await
{
log::info!("🔄 开始预热 tenant access token: {tenant_key}");
if let Err(e) =
Self::preheat_tenant_token(cache, config, &tenant_key, app_ticket_manager)
.await
{
log::warn!("❌ Tenant token预热失败 ({tenant_key}): {e:?}");
metrics.refresh_failures.fetch_add(1, Ordering::Relaxed);
} else {
log::info!("✅ Tenant token预热成功: {tenant_key}");
metrics.refresh_success.fetch_add(1, Ordering::Relaxed);
preheated_count += 1;
}
}
}
}
if preheated_count > 0 {
log::info!("🎯 本轮预热完成,共刷新了 {preheated_count} 个token");
} else {
log::debug!("✨ 所有token状态良好,无需预热");
}
Ok(())
}
#[allow(dead_code)]
async fn should_preheat_token(cache: &Arc<RwLock<QuickCache<String>>>, key: &str) -> bool {
Self::should_preheat_token_with_threshold(cache, key, 900).await
}
async fn should_preheat_token_with_threshold(
cache: &Arc<RwLock<QuickCache<String>>>,
key: &str,
threshold_seconds: u64,
) -> bool {
let cache_read = cache.read().await;
if cache_read.get(key).is_none_or(|token| token.is_empty()) {
log::debug!("🔍 Token {key} 不存在,需要预热");
return true;
}
if let Some(expiry_info) = cache_read.get_with_expiry(key) {
let remaining_seconds = expiry_info.expiry_seconds();
if remaining_seconds < threshold_seconds {
log::debug!(
"🔍 Token {key} 将在{remaining_seconds}秒后过期,阈值{threshold_seconds}秒,需要预热"
);
return true;
}
}
false
}
async fn get_cached_tenant_keys(
_cache: &Arc<RwLock<QuickCache<String>>>,
_app_id: &str,
) -> Vec<String> {
vec![]
}
async fn preheat_app_token(
cache: &Arc<RwLock<QuickCache<String>>>,
config: &Config,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<String> {
let temp_manager = TokenManager {
cache: cache.clone(),
metrics: Arc::new(TokenMetrics::new()), preheating_handle: None,
};
match config.app_type {
AppType::SelfBuild => {
temp_manager
.get_custom_app_access_token_then_cache(config)
.await
}
_ => {
temp_manager
.get_marketplace_app_access_token_then_cache(config, "", app_ticket_manager)
.await
}
}
}
async fn preheat_tenant_token(
cache: &Arc<RwLock<QuickCache<String>>>,
config: &Config,
tenant_key: &str,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<String> {
let temp_manager = TokenManager {
cache: cache.clone(),
metrics: Arc::new(TokenMetrics::new()), preheating_handle: None,
};
if config.app_type == AppType::SelfBuild {
temp_manager
.get_custom_tenant_access_token_then_cache(config, tenant_key)
.await
} else {
temp_manager
.get_marketplace_tenant_access_token_then_cache(
config,
tenant_key,
"",
app_ticket_manager,
)
.await
}
}
pub fn stop_background_preheating(&mut self) {
if let Some(handle) = self.preheating_handle.take() {
handle.abort();
log::info!("🛑 Token后台预热机制已停止");
}
}
pub fn is_preheating_active(&self) -> bool {
self.preheating_handle.is_some()
}
pub async fn get_app_access_token(
&self,
config: &Config,
app_ticket: &str,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<String> {
let start_time = Instant::now();
let key = app_access_token_key(&config.app_id);
{
self.metrics
.read_lock_acquisitions
.fetch_add(1, Ordering::Relaxed);
let cache = self.cache.read().await;
if let Some(token) = cache.get(&key) {
if !token.is_empty() {
self.metrics.app_cache_hits.fetch_add(1, Ordering::Relaxed);
log::debug!("App token cache hit in {:?}", start_time.elapsed());
return Ok(token);
}
}
}
self.metrics
.app_cache_misses
.fetch_add(1, Ordering::Relaxed);
self.metrics
.write_lock_acquisitions
.fetch_add(1, Ordering::Relaxed);
let cache = self.cache.write().await;
if let Some(token) = cache.get(&key) {
if !token.is_empty() {
self.metrics
.app_cache_misses
.fetch_sub(1, Ordering::Relaxed);
self.metrics.app_cache_hits.fetch_add(1, Ordering::Relaxed);
log::debug!("App token double-check hit in {:?}", start_time.elapsed());
return Ok(token);
}
}
drop(cache); log::debug!("App token cache miss, refreshing token");
let app_type = config.app_type;
let result = if app_type == AppType::SelfBuild {
self.get_custom_app_access_token_then_cache(config).await
} else {
self.get_marketplace_app_access_token_then_cache(config, app_ticket, app_ticket_manager)
.await
};
match &result {
Ok(_) => {
self.metrics.refresh_success.fetch_add(1, Ordering::Relaxed);
log::debug!("App token refresh succeeded in {:?}", start_time.elapsed());
}
Err(e) => {
self.metrics
.refresh_failures
.fetch_add(1, Ordering::Relaxed);
log::warn!(
"App token refresh failed in {:?}: {:?}",
start_time.elapsed(),
e
);
}
}
result
}
async fn get_custom_app_access_token_then_cache(&self, config: &Config) -> SDKResult<String> {
let url = format!("{}{}", config.base_url, APP_ACCESS_TOKEN_INTERNAL_URL_PATH);
let body = SelfBuiltAppAccessTokenReq {
app_id: config.app_id.clone(),
app_secret: config.app_secret.clone(),
};
let response = config.http_client.post(&url).json(&body).send().await?;
let resp: AppAccessTokenResp = response.json().await?;
self.handle_app_access_token_response(resp, &config.app_id)
.await
}
async fn handle_app_access_token_response(
&self,
resp: AppAccessTokenResp,
app_id: &str,
) -> SDKResult<String> {
if resp.raw_response.code == 0 {
let expire = resp.expire - EXPIRY_DELTA;
{
let mut cache = self.cache.write().await;
cache.set(
&app_access_token_key(app_id),
resp.app_access_token.clone(),
expire,
);
}
Ok(resp.app_access_token)
} else {
warn!("app access token response error: {:#?}", resp.raw_response);
Err(LarkAPIError::illegal_param(resp.raw_response.msg.clone()))
}
}
async fn get_marketplace_app_access_token_then_cache(
&self,
config: &Config,
app_ticket: &str,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<String> {
let mut app_ticket = app_ticket.to_string();
if app_ticket.is_empty() {
match app_ticket_manager.lock().await.get(config).await {
None => return Err(LarkAPIError::illegal_param("App ticket is empty")),
Some(ticket) => {
app_ticket = ticket;
}
}
}
let url = format!("{}{}", config.base_url, APP_ACCESS_TOKEN_URL_PATH);
let body = MarketplaceAppAccessTokenReq {
app_id: config.app_id.clone(),
app_secret: config.app_secret.clone(),
app_ticket,
};
let response = config.http_client.post(&url).json(&body).send().await?;
let resp: AppAccessTokenResp = response.json().await?;
self.handle_app_access_token_response(resp, &config.app_id)
.await
}
pub async fn get_tenant_access_token(
&self,
config: &Config,
tenant_key: &str,
app_ticket: &str,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<String> {
let start_time = Instant::now();
let key = tenant_access_token_key(&config.app_id, tenant_key);
{
self.metrics
.read_lock_acquisitions
.fetch_add(1, Ordering::Relaxed);
let cache = self.cache.read().await;
if let Some(token) = cache.get(&key) {
if !token.is_empty() {
self.metrics
.tenant_cache_hits
.fetch_add(1, Ordering::Relaxed);
log::debug!("Tenant token cache hit in {:?}", start_time.elapsed());
return Ok(token);
}
}
}
self.metrics
.tenant_cache_misses
.fetch_add(1, Ordering::Relaxed);
self.metrics
.write_lock_acquisitions
.fetch_add(1, Ordering::Relaxed);
let cache = self.cache.write().await;
if let Some(token) = cache.get(&key) {
if !token.is_empty() {
self.metrics
.tenant_cache_misses
.fetch_sub(1, Ordering::Relaxed);
self.metrics
.tenant_cache_hits
.fetch_add(1, Ordering::Relaxed);
log::debug!(
"Tenant token double-check hit in {:?}",
start_time.elapsed()
);
return Ok(token);
}
}
drop(cache); log::debug!("Tenant token cache miss, refreshing token");
let result = if config.app_type == AppType::SelfBuild {
self.get_custom_tenant_access_token_then_cache(config, tenant_key)
.await
} else {
self.get_marketplace_tenant_access_token_then_cache(
config,
tenant_key,
app_ticket,
app_ticket_manager,
)
.await
};
match &result {
Ok(_) => {
self.metrics.refresh_success.fetch_add(1, Ordering::Relaxed);
log::debug!(
"Tenant token refresh succeeded in {:?}",
start_time.elapsed()
);
}
Err(e) => {
self.metrics
.refresh_failures
.fetch_add(1, Ordering::Relaxed);
log::warn!(
"Tenant token refresh failed in {:?}: {:?}",
start_time.elapsed(),
e
);
}
}
result
}
async fn get_custom_tenant_access_token_then_cache(
&self,
config: &Config,
tenant_key: &str,
) -> SDKResult<String> {
let url = format!(
"{}{}",
config.base_url, TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH
);
let body = SelfBuiltTenantAccessTokenReq {
app_id: config.app_id.clone(),
app_secret: config.app_secret.clone(),
};
let response = config.http_client.post(&url).json(&body).send().await?;
let resp: TenantAccessTokenResp = response.json().await?;
self.handle_tenant_access_token_response(resp, &config.app_id, tenant_key)
.await
}
async fn handle_tenant_access_token_response(
&self,
resp: TenantAccessTokenResp,
app_id: &str,
tenant_key: &str,
) -> SDKResult<String> {
if resp.raw_response.code == 0 {
let expire = resp.expire - EXPIRY_DELTA;
{
let mut cache = self.cache.write().await;
cache.set(
&tenant_access_token_key(app_id, tenant_key),
resp.tenant_access_token.clone(),
expire,
);
}
Ok(resp.tenant_access_token)
} else {
warn!(
"tenant access token response error: {:#?}",
resp.raw_response
);
Err(LarkAPIError::illegal_param(resp.raw_response.msg.clone()))
}
}
async fn get_marketplace_tenant_access_token_then_cache(
&self,
config: &Config,
tenant_key: &str,
app_ticket: &str,
app_ticket_manager: &Arc<Mutex<AppTicketManager>>,
) -> SDKResult<String> {
let app_access_token = self
.get_marketplace_app_access_token_then_cache(config, app_ticket, app_ticket_manager)
.await?;
let url = format!("{}{}", config.base_url, TENANT_ACCESS_TOKEN_URL_PATH);
let body = MarketplaceTenantAccessTokenReq {
app_access_token,
tenant_key: tenant_key.to_string(),
};
let response = config
.http_client
.post(&url)
.json(&body)
.header(
"Authorization",
&format!("Bearer {}", &body.app_access_token),
)
.send()
.await?;
let resp: TenantAccessTokenResp = response.json().await?;
self.handle_tenant_access_token_response(resp, &config.app_id, tenant_key)
.await
}
}
fn app_access_token_key(app_id: &str) -> String {
format!("{APP_ACCESS_TOKEN_KEY_PREFIX}-{app_id}")
}
fn tenant_access_token_key(app_id: &str, tenant_key: &str) -> String {
format!("{APP_ACCESS_TOKEN_KEY_PREFIX}-{app_id}-{tenant_key}")
}
#[derive(Debug, Serialize, Deserialize)]
struct SelfBuiltAppAccessTokenReq {
app_id: String,
app_secret: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct SelfBuiltTenantAccessTokenReq {
app_id: String,
app_secret: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct AppAccessTokenResp {
#[serde(flatten)]
raw_response: RawResponse,
expire: i32,
app_access_token: String,
}
impl ApiResponseTrait for AppAccessTokenResp {
fn data_format() -> ResponseFormat {
ResponseFormat::Flatten
}
}
#[derive(Serialize, Deserialize)]
struct MarketplaceAppAccessTokenReq {
app_id: String,
app_secret: String,
app_ticket: String,
}
#[derive(Serialize, Deserialize)]
struct MarketplaceTenantAccessTokenReq {
app_access_token: String,
tenant_key: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct TenantAccessTokenResp {
#[serde(flatten)]
raw_response: RawResponse,
expire: i32,
tenant_access_token: String,
}
impl ApiResponseTrait for TenantAccessTokenResp {
fn data_format() -> ResponseFormat {
ResponseFormat::Flatten
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{config::Config, constants::AppType};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::Mutex;
#[test]
fn test_token_manager_creation() {
let manager = TokenManager::new();
let _cache = &manager.cache;
}
#[test]
fn test_app_access_token_key_generation() {
let app_id = "test_app_id";
let key = app_access_token_key(app_id);
assert_eq!(key, format!("{APP_ACCESS_TOKEN_KEY_PREFIX}-{app_id}"));
}
#[test]
fn test_tenant_access_token_key_generation() {
let app_id = "test_app_id";
let tenant_key = "test_tenant";
let key = tenant_access_token_key(app_id, tenant_key);
assert_eq!(
key,
format!("{APP_ACCESS_TOKEN_KEY_PREFIX}-{app_id}-{tenant_key}")
);
}
#[tokio::test]
async fn test_cache_miss_returns_empty_string() {
let manager = TokenManager::new();
let key = "non_existent_key";
let cache = manager.cache.read().await;
let result = cache.get(key).unwrap_or_default();
assert_eq!(result, String::new());
}
#[tokio::test]
async fn test_get_app_access_token_cache_miss_does_not_error() {
let manager = TokenManager::new();
let config = Config {
app_id: "test_app".to_string(),
app_secret: "test_secret".to_string(),
app_type: AppType::SelfBuild,
base_url: "https://open.feishu.cn".to_string(),
http_client: reqwest::Client::new(),
enable_token_cache: true,
req_timeout: Some(Duration::from_secs(30)),
header: HashMap::new(),
token_manager: Arc::new(Mutex::new(TokenManager::new())),
app_ticket_manager: Arc::new(Mutex::new(
crate::core::app_ticket_manager::AppTicketManager::new(),
)),
};
let app_ticket_manager = Arc::new(Mutex::new(
crate::core::app_ticket_manager::AppTicketManager::new(),
));
let result = manager
.get_app_access_token(&config, "", &app_ticket_manager)
.await;
if let Err(error) = result {
let error_msg = format!("{error:?}");
assert!(
!error_msg.contains("cache error"),
"应该不再出现'cache error',而是实际的API调用错误: {error_msg}"
);
}
}
#[test]
fn test_token_metrics_creation() {
let metrics = TokenMetrics::new();
assert_eq!(metrics.app_cache_hit_rate(), 0.0);
assert_eq!(metrics.tenant_cache_hit_rate(), 0.0);
assert_eq!(metrics.refresh_success_rate(), 0.0);
}
#[test]
fn test_token_metrics_cache_hit_rate_calculation() {
let metrics = TokenMetrics::new();
metrics.app_cache_hits.store(8, Ordering::Relaxed);
metrics.app_cache_misses.store(2, Ordering::Relaxed);
assert_eq!(metrics.app_cache_hit_rate(), 0.8);
metrics.tenant_cache_hits.store(9, Ordering::Relaxed);
metrics.tenant_cache_misses.store(1, Ordering::Relaxed);
assert_eq!(metrics.tenant_cache_hit_rate(), 0.9); }
#[test]
fn test_token_metrics_refresh_success_rate() {
let metrics = TokenMetrics::new();
metrics.refresh_success.store(19, Ordering::Relaxed);
metrics.refresh_failures.store(1, Ordering::Relaxed);
assert_eq!(metrics.refresh_success_rate(), 0.95); }
#[test]
fn test_token_metrics_performance_report() {
let metrics = TokenMetrics::new();
metrics.app_cache_hits.store(80, Ordering::Relaxed);
metrics.app_cache_misses.store(20, Ordering::Relaxed);
metrics.refresh_success.store(95, Ordering::Relaxed);
metrics.refresh_failures.store(5, Ordering::Relaxed);
let report = metrics.performance_report();
assert!(report.contains("80.00%")); assert!(report.contains("95.00%")); assert!(report.contains("80 hits, 20 misses")); }
#[tokio::test]
async fn test_token_manager_metrics_integration() {
let manager = TokenManager::new();
let metrics = manager.metrics();
assert_eq!(metrics.read_lock_acquisitions.load(Ordering::Relaxed), 0);
manager.log_performance_metrics(); }
#[tokio::test]
async fn test_preheating_config_default_values() {
let config = PreheatingConfig::default();
assert_eq!(config.check_interval_seconds, 1800); assert_eq!(config.preheat_threshold_seconds, 900); assert!(config.enable_tenant_preheating);
assert_eq!(config.max_concurrent_preheat, 3);
}
#[tokio::test]
async fn test_should_preheat_token_with_custom_threshold() {
let manager = TokenManager::new();
let key = "test_token_key";
assert!(TokenManager::should_preheat_token_with_threshold(&manager.cache, key, 600).await);
{
let mut cache = manager.cache.write().await;
cache.set(key, "test_token_value".to_string(), 3600); }
assert!(!TokenManager::should_preheat_token_with_threshold(&manager.cache, key, 600).await);
assert!(TokenManager::should_preheat_token_with_threshold(&manager.cache, key, 3700).await);
}
#[tokio::test]
async fn test_get_cached_tenant_keys() {
let manager = TokenManager::new();
let tenant_keys = TokenManager::get_cached_tenant_keys(&manager.cache, "test_app").await;
assert!(tenant_keys.is_empty());
}
#[test]
fn test_cache_entry_expiry_calculations() {
use crate::core::cache::CacheEntry;
use std::time::Duration;
use tokio::time::Instant;
let now = Instant::now();
let expires_in_10_mins = now + Duration::from_secs(600);
let entry = CacheEntry {
value: "test_value".to_string(),
expires_at: expires_in_10_mins,
current_time: now,
};
assert_eq!(entry.expiry_seconds(), 600);
assert!(entry.expires_within(700)); assert!(!entry.expires_within(500)); }
}