#[cfg(feature = "cache")]
use std::collections::HashMap;
#[cfg(feature = "cache")]
use std::time::{Duration, Instant};
#[cfg(feature = "cache")]
use parking_lot::RwLock;
#[cfg(feature = "cache")]
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
#[cfg(feature = "cache")]
use crate::metrics::{METRICS, MetricsCollector};
pub trait Cache: Send + Sync {
fn get(&self, key: &str) -> Option<Vec<u8>>;
fn set(&self, key: &str, value: Vec<u8>, ttl: Duration);
fn delete(&self, key: &str);
fn clear(&self);
fn stats(&self) -> CacheStats;
fn cleanup_expired(&self);
}
#[cfg(feature = "cache")]
#[derive(Debug)]
pub struct MemoryCache {
store: RwLock<HashMap<String, CacheEntry>>,
default_ttl: Duration,
max_entries: usize,
}
#[cfg(feature = "cache")]
#[derive(Debug, Clone)]
struct CacheEntry {
data: Vec<u8>,
expires_at: Instant,
created_at: Instant,
access_count: u32,
}
#[cfg(feature = "cache")]
impl MemoryCache {
pub fn new(default_ttl: Duration) -> Self {
Self {
store: RwLock::new(HashMap::new()),
default_ttl,
max_entries: 1000, }
}
pub fn with_capacity(capacity: usize, default_ttl: Duration) -> Self {
Self {
store: RwLock::new(HashMap::with_capacity(capacity)),
default_ttl,
max_entries: capacity,
}
}
pub fn default_ttl(&self) -> Duration {
self.default_ttl
}
fn is_at_capacity(&self) -> bool {
let store = self.store.read();
store.len() >= self.max_entries
}
fn evict_lru(&self) {
let mut store = self.store.write();
if store.is_empty() {
return;
}
let oldest_key = store
.iter()
.min_by_key(|(_, entry)| entry.created_at)
.map(|(key, _)| key.clone());
if let Some(key) = oldest_key {
store.remove(&key);
debug!(cache_key = %key, "Evicted LRU cache entry");
}
}
}
#[cfg(feature = "cache")]
impl Cache for MemoryCache {
fn get(&self, key: &str) -> Option<Vec<u8>> {
let mut store = self.store.write();
if let Some(entry) = store.get_mut(key) {
if entry.expires_at > Instant::now() {
entry.access_count += 1;
debug!(cache_key = key, "Cache hit");
METRICS.record_cache_hit(key);
Some(entry.data.clone())
} else {
store.remove(key);
debug!(cache_key = key, "Cache entry expired and removed");
METRICS.record_cache_miss(key);
None
}
} else {
debug!(cache_key = key, "Cache miss");
METRICS.record_cache_miss(key);
None
}
}
fn set(&self, key: &str, value: Vec<u8>, ttl: Duration) {
let now = Instant::now();
let expires_at = now + ttl;
if self.is_at_capacity() {
self.evict_lru();
}
let value_size = value.len();
let entry = CacheEntry {
data: value,
expires_at,
created_at: now,
access_count: 0,
};
let mut store = self.store.write();
store.insert(key.to_string(), entry);
debug!(
cache_key = key,
ttl_secs = ttl.as_secs(),
value_size = value_size,
"Cache entry stored"
);
}
fn delete(&self, key: &str) {
let mut store = self.store.write();
if store.remove(key).is_some() {
debug!(cache_key = key, "Cache entry deleted");
}
}
fn clear(&self) {
let mut store = self.store.write();
let count = store.len();
store.clear();
info!(entries_cleared = count, "Cache cleared");
}
fn stats(&self) -> CacheStats {
let store = self.store.read();
let now = Instant::now();
let total_entries = store.len();
let expired_entries = store
.values()
.filter(|entry| entry.expires_at <= now)
.count();
let total_size_bytes = store.values().map(|entry| entry.data.len()).sum();
CacheStats {
total_entries,
active_entries: total_entries - expired_entries,
expired_entries,
total_size_bytes,
max_capacity: self.max_entries,
}
}
fn cleanup_expired(&self) {
let now = Instant::now();
let mut store = self.store.write();
let original_count = store.len();
store.retain(|key, entry| {
let is_valid = entry.expires_at > now;
if !is_valid {
debug!(cache_key = key, "Removing expired cache entry");
}
is_valid
});
let removed_count = original_count - store.len();
if removed_count > 0 {
info!(
removed_entries = removed_count,
"Cleaned up expired cache entries"
);
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CacheStats {
pub total_entries: usize,
pub active_entries: usize,
pub expired_entries: usize,
pub total_size_bytes: usize,
pub max_capacity: usize,
}
impl CacheStats {
pub fn empty() -> Self {
Self {
total_entries: 0,
active_entries: 0,
expired_entries: 0,
total_size_bytes: 0,
max_capacity: 0,
}
}
pub fn utilization_percent(&self) -> f64 {
if self.max_capacity == 0 {
0.0
} else {
(self.active_entries as f64 / self.max_capacity as f64) * 100.0
}
}
pub fn avg_entry_size_bytes(&self) -> f64 {
if self.active_entries == 0 {
0.0
} else {
self.total_size_bytes as f64 / self.active_entries as f64
}
}
}
pub fn generate_cache_key(endpoint: &str, params: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let full_key = if params.is_empty() {
endpoint.to_string()
} else {
format!("{}?{}", endpoint, params)
};
let mut hasher = DefaultHasher::new();
full_key.hash(&mut hasher);
let endpoint_safe = endpoint.replace(['/', '?', '&'], "_");
format!("gouqi:{}:{:x}", endpoint_safe, hasher.finish())
}
pub fn jira_cache_key(operation: &str, resource_id: &str, params: &str) -> String {
let base_key = if resource_id.is_empty() {
operation.to_string()
} else {
format!("{}/{}", operation, resource_id)
};
generate_cache_key(&base_key, params)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeCacheConfig {
pub enabled: bool,
#[serde(with = "humantime_serde")]
pub default_ttl: Duration,
pub max_entries: usize,
pub strategies: HashMap<String, RuntimeCacheStrategy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeCacheStrategy {
#[serde(with = "humantime_serde")]
pub ttl: Duration,
pub cache_errors: bool,
pub use_etag: bool,
}
impl Default for RuntimeCacheConfig {
fn default() -> Self {
let mut strategies = HashMap::new();
strategies.insert(
"issues".to_string(),
RuntimeCacheStrategy {
ttl: Duration::from_secs(300), cache_errors: false,
use_etag: true,
},
);
strategies.insert(
"projects".to_string(),
RuntimeCacheStrategy {
ttl: Duration::from_secs(3600), cache_errors: false,
use_etag: true,
},
);
strategies.insert(
"users".to_string(),
RuntimeCacheStrategy {
ttl: Duration::from_secs(1800), cache_errors: false,
use_etag: false,
},
);
strategies.insert(
"search".to_string(),
RuntimeCacheStrategy {
ttl: Duration::from_secs(60), cache_errors: false,
use_etag: false,
},
);
Self {
enabled: true,
default_ttl: Duration::from_secs(300), max_entries: 1000,
strategies,
}
}
}
impl RuntimeCacheConfig {
pub fn strategy_for_endpoint(&self, endpoint: &str) -> RuntimeCacheStrategy {
for (pattern, strategy) in &self.strategies {
if endpoint.contains(pattern) {
return strategy.clone();
}
}
RuntimeCacheStrategy {
ttl: self.default_ttl,
cache_errors: false,
use_etag: true,
}
}
pub fn should_cache_endpoint(&self, endpoint: &str) -> bool {
self.enabled && !endpoint.contains("search") }
}
#[cfg(not(feature = "cache"))]
pub struct MemoryCache;
#[cfg(not(feature = "cache"))]
impl MemoryCache {
pub fn new(_default_ttl: Duration) -> Self {
Self
}
pub fn with_capacity(_capacity: usize, _default_ttl: Duration) -> Self {
Self
}
}
#[cfg(not(feature = "cache"))]
impl Cache for MemoryCache {
fn get(&self, _key: &str) -> Option<Vec<u8>> {
None
}
fn set(&self, _key: &str, _value: Vec<u8>, _ttl: Duration) {}
fn delete(&self, _key: &str) {}
fn clear(&self) {}
fn stats(&self) -> CacheStats {
CacheStats::empty()
}
fn cleanup_expired(&self) {}
}