use std::{
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Instant,
};
use chrono::{DateTime, Timelike, Utc};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshJobStatus {
Idle,
Running,
Success,
Failed,
Stopped,
}
impl std::fmt::Display for RefreshJobStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Idle => write!(f, "idle"),
Self::Running => write!(f, "running"),
Self::Success => write!(f, "success"),
Self::Failed => write!(f, "failed"),
Self::Stopped => write!(f, "stopped"),
}
}
}
#[derive(Debug, Clone)]
pub struct RefreshConfig {
pub enabled: bool,
pub check_interval_hours: u32,
pub refresh_threshold_percent: u32,
pub quiet_hours_start: Option<u32>,
pub quiet_hours_end: Option<u32>,
}
impl RefreshConfig {
pub fn new() -> Self {
Self {
enabled: true,
check_interval_hours: 24,
refresh_threshold_percent: 80,
quiet_hours_start: None,
quiet_hours_end: None,
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_check_interval(mut self, hours: u32) -> Self {
self.check_interval_hours = hours.max(1);
self
}
pub fn with_refresh_threshold(mut self, percent: u32) -> Self {
self.refresh_threshold_percent = percent.min(99);
self
}
pub fn with_quiet_hours(mut self, start_hour: u32, end_hour: u32) -> Self {
if start_hour < 24 && end_hour < 24 {
self.quiet_hours_start = Some(start_hour);
self.quiet_hours_end = Some(end_hour);
}
self
}
}
impl Default for RefreshConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RefreshTrigger {
config: Arc<RefreshConfig>,
last_check: Arc<std::sync::Mutex<Option<DateTime<Utc>>>>,
last_refresh: Arc<std::sync::Mutex<Option<DateTime<Utc>>>>,
last_refresh_duration_ms: Arc<AtomicU64>,
total_refreshes: Arc<AtomicU64>,
failed_refreshes: Arc<AtomicU64>,
refresh_pending: Arc<AtomicBool>,
}
impl RefreshTrigger {
pub fn new(config: RefreshConfig) -> Self {
Self {
config: Arc::new(config),
last_check: Arc::new(std::sync::Mutex::new(None)),
last_refresh: Arc::new(std::sync::Mutex::new(None)),
last_refresh_duration_ms: Arc::new(AtomicU64::new(0)),
total_refreshes: Arc::new(AtomicU64::new(0)),
failed_refreshes: Arc::new(AtomicU64::new(0)),
refresh_pending: Arc::new(AtomicBool::new(false)),
}
}
pub fn should_trigger(&self, ttl_consumed_percent: u32) -> bool {
if !self.config.enabled {
return false;
}
if self.refresh_pending.load(Ordering::Relaxed) {
return false;
}
if ttl_consumed_percent < self.config.refresh_threshold_percent {
return false;
}
if let (Some(start), Some(end)) =
(self.config.quiet_hours_start, self.config.quiet_hours_end)
{
let now = Utc::now();
let hour = now.hour() as u32;
if start < end {
if hour >= start && hour < end {
return false;
}
} else {
if hour >= start || hour < end {
return false;
}
}
}
true
}
pub fn mark_pending(&self) {
self.refresh_pending.store(true, Ordering::Relaxed);
}
pub fn clear_pending(&self) {
self.refresh_pending.store(false, Ordering::Relaxed);
}
pub fn record_success(&self, duration_ms: u64) {
self.total_refreshes.fetch_add(1, Ordering::Relaxed);
self.last_refresh_duration_ms.store(duration_ms, Ordering::Relaxed);
if let Ok(mut last) = self.last_refresh.lock() {
*last = Some(Utc::now());
}
}
pub fn record_failure(&self) {
self.failed_refreshes.fetch_add(1, Ordering::Relaxed);
}
pub fn record_check(&self) {
if let Ok(mut last) = self.last_check.lock() {
*last = Some(Utc::now());
}
}
pub fn last_check_time(&self) -> Option<DateTime<Utc>> {
if let Ok(last) = self.last_check.lock() {
*last
} else {
None
}
}
pub fn last_refresh_time(&self) -> Option<DateTime<Utc>> {
if let Ok(last) = self.last_refresh.lock() {
*last
} else {
None
}
}
pub fn total_refreshes(&self) -> u64 {
self.total_refreshes.load(Ordering::Relaxed)
}
pub fn failed_refreshes(&self) -> u64 {
self.failed_refreshes.load(Ordering::Relaxed)
}
pub fn success_rate_percent(&self) -> u32 {
let total = self.total_refreshes();
if total == 0 {
100
} else {
let failed = self.failed_refreshes();
let successful = total - failed;
((successful as f64 / total as f64) * 100.0) as u32
}
}
pub fn is_pending(&self) -> bool {
self.refresh_pending.load(Ordering::Relaxed)
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
}
impl Default for RefreshTrigger {
fn default() -> Self {
Self::new(RefreshConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct RefreshJob {
status: Arc<std::sync::Mutex<RefreshJobStatus>>,
start_time: Arc<std::sync::Mutex<Option<Instant>>>,
last_error: Arc<std::sync::Mutex<Option<String>>>,
shutdown_requested: Arc<AtomicBool>,
}
impl RefreshJob {
pub fn new() -> Self {
Self {
status: Arc::new(std::sync::Mutex::new(RefreshJobStatus::Idle)),
start_time: Arc::new(std::sync::Mutex::new(None)),
last_error: Arc::new(std::sync::Mutex::new(None)),
shutdown_requested: Arc::new(AtomicBool::new(false)),
}
}
pub fn start(&self) -> Result<(), String> {
let mut status = self.status.lock().map_err(|e| format!("Failed to lock status: {}", e))?;
if *status != RefreshJobStatus::Idle {
return Err(format!("Job already running: {}", status));
}
*status = RefreshJobStatus::Running;
let mut start = self
.start_time
.lock()
.map_err(|e| format!("Failed to lock start time: {}", e))?;
*start = Some(Instant::now());
Ok(())
}
pub fn complete_success(&self) -> Result<(), String> {
let mut status = self.status.lock().map_err(|e| format!("Failed to lock status: {}", e))?;
*status = RefreshJobStatus::Success;
Ok(())
}
pub fn complete_failure(&self, error: impl Into<String>) -> Result<(), String> {
let mut status = self.status.lock().map_err(|e| format!("Failed to lock status: {}", e))?;
*status = RefreshJobStatus::Failed;
let mut last_error =
self.last_error.lock().map_err(|e| format!("Failed to lock error: {}", e))?;
*last_error = Some(error.into());
Ok(())
}
pub fn request_shutdown(&self) {
self.shutdown_requested.store(true, Ordering::Relaxed);
}
pub fn should_shutdown(&self) -> bool {
self.shutdown_requested.load(Ordering::Relaxed)
}
pub fn status(&self) -> Result<RefreshJobStatus, String> {
let status = self.status.lock().map_err(|e| format!("Failed to lock status: {}", e))?;
Ok(*status)
}
pub fn duration(&self) -> Result<Option<std::time::Duration>, String> {
let start = self
.start_time
.lock()
.map_err(|e| format!("Failed to lock start time: {}", e))?;
Ok(start.map(|s| s.elapsed()))
}
pub fn last_error(&self) -> Result<Option<String>, String> {
let error = self.last_error.lock().map_err(|e| format!("Failed to lock error: {}", e))?;
Ok(error.clone())
}
}
impl Default for RefreshJob {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RefreshManager {
trigger: Arc<RefreshTrigger>,
job: Arc<RefreshJob>,
}
impl RefreshManager {
pub fn new(config: RefreshConfig) -> Self {
Self {
trigger: Arc::new(RefreshTrigger::new(config)),
job: Arc::new(RefreshJob::new()),
}
}
pub fn check_and_trigger(&self, ttl_consumed_percent: u32) -> bool {
self.trigger.record_check();
if self.trigger.should_trigger(ttl_consumed_percent) {
self.trigger.mark_pending();
true
} else {
false
}
}
pub fn start_job(&self) -> Result<(), String> {
self.job.start()
}
pub fn complete_job_success(&self) -> Result<(), String> {
self.trigger.clear_pending();
self.job.complete_success()
}
pub fn complete_job_failure(&self, error: impl Into<String>) -> Result<(), String> {
self.job.complete_failure(error)
}
pub fn trigger(&self) -> Arc<RefreshTrigger> {
Arc::clone(&self.trigger)
}
pub fn job(&self) -> Arc<RefreshJob> {
Arc::clone(&self.job)
}
pub fn refresh_pending(&self) -> bool {
self.trigger.is_pending()
}
pub fn is_enabled(&self) -> bool {
self.trigger.is_enabled()
}
pub fn trigger_manual(&self) -> Result<(), String> {
if !self.trigger.is_pending() {
self.trigger.mark_pending();
Ok(())
} else {
Err("Refresh already pending".to_string())
}
}
pub fn request_shutdown(&self) {
self.job.request_shutdown();
}
pub fn time_since_last_check(&self) -> Option<std::time::Duration> {
self.trigger
.last_check_time()
.map(|last| (Utc::now() - last).to_std().unwrap_or_default())
}
pub fn time_since_last_refresh(&self) -> Option<std::time::Duration> {
self.trigger
.last_refresh_time()
.map(|last| (Utc::now() - last).to_std().unwrap_or_default())
}
pub fn job_running(&self) -> bool {
self.job.status().map(|s| s == RefreshJobStatus::Running).unwrap_or(false)
}
pub fn job_success_rate_percent(&self) -> u32 {
self.trigger.success_rate_percent()
}
pub fn health_status(&self) -> RefreshHealthStatus {
let job_status = self.job.status().unwrap_or(RefreshJobStatus::Failed);
if !self.is_enabled() {
RefreshHealthStatus::Disabled
} else if self.job_running() {
RefreshHealthStatus::Running
} else if self.refresh_pending() {
RefreshHealthStatus::Pending
} else if job_status == RefreshJobStatus::Failed && self.trigger.failed_refreshes() > 2 {
RefreshHealthStatus::Degraded
} else {
RefreshHealthStatus::Healthy
}
}
pub fn should_retry_refresh(&self) -> bool {
self.refresh_pending() && self.trigger.failed_refreshes() < 5
}
pub fn reset_for_retry(&self) {
self.trigger.clear_pending();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshHealthStatus {
Disabled,
Running,
Pending,
Degraded,
Healthy,
}
impl Default for RefreshManager {
fn default() -> Self {
Self::new(RefreshConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_refresh_config_default() {
let config = RefreshConfig::new();
assert!(config.enabled);
assert_eq!(config.check_interval_hours, 24);
assert_eq!(config.refresh_threshold_percent, 80);
}
#[test]
fn test_refresh_config_builder() {
let config = RefreshConfig::new()
.with_enabled(false)
.with_check_interval(12)
.with_refresh_threshold(75);
assert!(!config.enabled);
assert_eq!(config.check_interval_hours, 12);
assert_eq!(config.refresh_threshold_percent, 75);
}
#[test]
fn test_refresh_trigger_creation() {
let trigger = RefreshTrigger::new(RefreshConfig::default());
assert!(trigger.is_enabled());
assert!(!trigger.is_pending());
assert_eq!(trigger.total_refreshes(), 0);
}
#[test]
fn test_refresh_trigger_should_trigger() {
let trigger = RefreshTrigger::new(RefreshConfig::default());
assert!(!trigger.should_trigger(75)); assert!(trigger.should_trigger(80)); assert!(trigger.should_trigger(85)); }
#[test]
fn test_refresh_trigger_disabled() {
let config = RefreshConfig::new().with_enabled(false);
let trigger = RefreshTrigger::new(config);
assert!(!trigger.should_trigger(85)); }
#[test]
fn test_refresh_trigger_mark_pending() {
let trigger = RefreshTrigger::new(RefreshConfig::default());
trigger.mark_pending();
assert!(trigger.is_pending());
trigger.clear_pending();
assert!(!trigger.is_pending());
}
#[test]
fn test_refresh_trigger_single_trigger() {
let trigger = RefreshTrigger::new(RefreshConfig::default());
assert!(trigger.should_trigger(85));
trigger.mark_pending();
assert!(!trigger.should_trigger(85)); }
#[test]
fn test_refresh_trigger_record_success() {
let trigger = RefreshTrigger::new(RefreshConfig::default());
trigger.record_success(100);
assert_eq!(trigger.total_refreshes(), 1);
assert_eq!(trigger.failed_refreshes(), 0);
assert_eq!(trigger.success_rate_percent(), 100);
}
#[test]
fn test_refresh_trigger_record_failure() {
let trigger = RefreshTrigger::new(RefreshConfig::default());
trigger.record_success(100);
trigger.record_success(100);
trigger.record_failure();
assert_eq!(trigger.total_refreshes(), 2);
assert_eq!(trigger.failed_refreshes(), 1);
assert_eq!(trigger.success_rate_percent(), 50);
}
#[test]
fn test_refresh_job_creation() {
let job = RefreshJob::new();
assert_eq!(job.status().unwrap(), RefreshJobStatus::Idle);
assert!(!job.should_shutdown());
}
#[test]
fn test_refresh_job_lifecycle() {
let job = RefreshJob::new();
job.start().unwrap();
assert_eq!(job.status().unwrap(), RefreshJobStatus::Running);
job.complete_success().unwrap();
assert_eq!(job.status().unwrap(), RefreshJobStatus::Success);
}
#[test]
fn test_refresh_job_failure() {
let job = RefreshJob::new();
job.start().unwrap();
job.complete_failure("Vault unreachable").unwrap();
assert_eq!(job.status().unwrap(), RefreshJobStatus::Failed);
assert!(job.last_error().unwrap().is_some());
}
#[test]
fn test_refresh_job_shutdown() {
let job = RefreshJob::new();
assert!(!job.should_shutdown());
job.request_shutdown();
assert!(job.should_shutdown());
}
#[test]
fn test_refresh_manager_creation() {
let manager = RefreshManager::new(RefreshConfig::default());
assert!(manager.is_enabled());
assert!(!manager.refresh_pending());
}
#[test]
fn test_refresh_manager_check_and_trigger() {
let manager = RefreshManager::new(RefreshConfig::default());
assert!(!manager.check_and_trigger(75));
assert!(manager.check_and_trigger(80));
assert!(manager.refresh_pending());
}
#[test]
fn test_refresh_manager_job_lifecycle() {
let manager = RefreshManager::new(RefreshConfig::default());
assert!(manager.check_and_trigger(85));
manager.start_job().unwrap();
assert_eq!(manager.job().status().unwrap(), RefreshJobStatus::Running);
manager.complete_job_success().unwrap();
assert_eq!(manager.job().status().unwrap(), RefreshJobStatus::Success);
assert!(!manager.refresh_pending());
}
#[test]
fn test_refresh_manager_manual_trigger() {
let manager = RefreshManager::new(RefreshConfig::default());
manager.trigger_manual().unwrap();
assert!(manager.refresh_pending());
}
#[test]
fn test_refresh_manager_job_running() {
let manager = RefreshManager::new(RefreshConfig::default());
assert!(!manager.job_running());
manager.start_job().unwrap();
assert!(manager.job_running());
}
#[test]
fn test_refresh_manager_health_status_disabled() {
let config = RefreshConfig::default().with_enabled(false);
let manager = RefreshManager::new(config);
assert_eq!(manager.health_status(), RefreshHealthStatus::Disabled);
}
#[test]
fn test_refresh_manager_health_status_healthy() {
let manager = RefreshManager::new(RefreshConfig::default());
assert_eq!(manager.health_status(), RefreshHealthStatus::Healthy);
}
#[test]
fn test_refresh_manager_health_status_pending() {
let manager = RefreshManager::new(RefreshConfig::default());
manager.check_and_trigger(85);
assert_eq!(manager.health_status(), RefreshHealthStatus::Pending);
}
#[test]
fn test_refresh_manager_health_status_running() {
let manager = RefreshManager::new(RefreshConfig::default());
manager.check_and_trigger(85);
manager.start_job().unwrap();
assert_eq!(manager.health_status(), RefreshHealthStatus::Running);
}
#[test]
fn test_refresh_manager_should_retry() {
let manager = RefreshManager::new(RefreshConfig::default());
manager.check_and_trigger(85);
assert!(manager.should_retry_refresh());
for _ in 0..5 {
manager.trigger().record_failure();
}
assert!(!manager.should_retry_refresh());
}
#[test]
fn test_refresh_manager_reset_for_retry() {
let manager = RefreshManager::new(RefreshConfig::default());
manager.check_and_trigger(85);
assert!(manager.refresh_pending());
manager.reset_for_retry();
assert!(!manager.refresh_pending());
}
}