use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::proxy::cache::CacheStore;
use crate::proxy::upstream::{GenericRestAdapter, HealthTracker, UpstreamStatus};
#[derive(Debug, Clone)]
pub struct RecoveryConfig {
pub initial_rps: f32,
pub max_rps: f32,
pub rps_increment: f32,
pub batch_size: usize,
pub batch_delay: Duration,
pub stability_period: Duration,
}
impl Default for RecoveryConfig {
fn default() -> Self {
Self {
initial_rps: 1.0,
max_rps: 50.0,
rps_increment: 5.0,
batch_size: 5,
batch_delay: Duration::from_millis(200),
stability_period: Duration::from_secs(30),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryState {
Stable,
Recovering,
Paused,
Offline,
}
pub struct RecoveryTracker {
state: AtomicU32,
current_rps: std::sync::atomic::AtomicU32,
is_recovering: AtomicBool,
successful_batches: AtomicU32,
required_batches: u32,
}
impl RecoveryTracker {
pub fn new(config: &RecoveryConfig) -> Self {
let batch_interval_secs = config.batch_delay.as_secs_f32();
let required = if batch_interval_secs > 0.0 {
(config.stability_period.as_secs_f32() / batch_interval_secs) as u32
} else {
10
};
Self {
state: AtomicU32::new(RecoveryState::Stable as u32),
current_rps: AtomicU32::new((config.initial_rps * 100.0) as u32), is_recovering: AtomicBool::new(false),
successful_batches: AtomicU32::new(0),
required_batches: required.max(1),
}
}
pub fn state(&self) -> RecoveryState {
match self.state.load(Ordering::SeqCst) {
0 => RecoveryState::Stable,
1 => RecoveryState::Recovering,
2 => RecoveryState::Paused,
_ => RecoveryState::Offline,
}
}
fn set_state(&self, state: RecoveryState) {
self.state.store(state as u32, Ordering::SeqCst);
}
pub fn current_rps(&self) -> f32 {
self.current_rps.load(Ordering::SeqCst) as f32 / 100.0
}
fn set_rps(&self, rps: f32) {
self.current_rps
.store((rps * 100.0) as u32, Ordering::SeqCst);
}
pub fn is_recovering(&self) -> bool {
self.is_recovering.load(Ordering::SeqCst)
}
pub fn start_recovery(&self, initial_rps: f32) {
self.is_recovering.store(true, Ordering::SeqCst);
self.set_state(RecoveryState::Recovering);
self.set_rps(initial_rps);
self.successful_batches.store(0, Ordering::SeqCst);
}
pub fn record_success(&self, rps_increment: f32, max_rps: f32) {
let prev = self.successful_batches.fetch_add(1, Ordering::SeqCst);
let batches = prev.saturating_add(1);
let current_rps = self.current_rps();
let new_rps = (current_rps + rps_increment).min(max_rps);
self.set_rps(new_rps);
if batches >= self.required_batches {
self.complete_recovery();
}
}
pub fn record_failure(&self) {
self.successful_batches.store(0, Ordering::SeqCst);
self.set_state(RecoveryState::Paused);
}
pub fn complete_recovery(&self) {
self.is_recovering.store(false, Ordering::SeqCst);
self.set_state(RecoveryState::Stable);
}
pub fn go_offline(&self) {
self.is_recovering.store(false, Ordering::SeqCst);
self.set_state(RecoveryState::Offline);
}
}
pub struct RecoveryWorker {
cache: Arc<CacheStore>,
upstream: Arc<GenericRestAdapter>,
health: Arc<HealthTracker>,
tracker: Arc<RecoveryTracker>,
config: RecoveryConfig,
query_map: Arc<dashmap::DashMap<[u8; 32], String>>,
cancel: CancellationToken,
}
impl RecoveryWorker {
pub fn new(
cache: Arc<CacheStore>,
upstream: Arc<GenericRestAdapter>,
health: Arc<HealthTracker>,
query_map: Arc<dashmap::DashMap<[u8; 32], String>>,
config: RecoveryConfig,
cancel: CancellationToken,
) -> Self {
let tracker = Arc::new(RecoveryTracker::new(&config));
Self {
cache,
upstream,
health,
tracker,
config,
query_map,
cancel,
}
}
pub fn with_defaults(
cache: Arc<CacheStore>,
upstream: Arc<GenericRestAdapter>,
health: Arc<HealthTracker>,
query_map: Arc<dashmap::DashMap<[u8; 32], String>>,
cancel: CancellationToken,
) -> Self {
Self::new(
cache,
upstream,
health,
query_map,
RecoveryConfig::default(),
cancel,
)
}
pub fn tracker(&self) -> Arc<RecoveryTracker> {
self.tracker.clone()
}
pub async fn run(&self) {
let mut was_offline = false;
loop {
tokio::select! {
_ = self.cancel.cancelled() => {
break;
}
_ = tokio::time::sleep(Duration::from_millis(100)) => {
let status = self.health.status();
match status {
UpstreamStatus::Offline => {
was_offline = true;
self.tracker.go_offline();
}
UpstreamStatus::Online | UpstreamStatus::Degraded => {
if was_offline && !self.tracker.is_recovering() {
was_offline = false;
self.tracker.start_recovery(self.config.initial_rps);
}
if self.tracker.is_recovering() {
self.process_recovery_batch().await;
}
}
}
}
}
}
}
async fn process_recovery_batch(&self) {
let all_stale = self.cache.get_stale_hashes();
let stale_hashes: Vec<_> = all_stale.into_iter().take(self.config.batch_size).collect();
if stale_hashes.is_empty() {
self.tracker.complete_recovery();
return;
}
let mut success_count: u32 = 0;
let mut failure_count: u32 = 0;
for hash in stale_hashes {
let query = match self.query_map.get(&hash) {
Some(q) => q.clone(),
None => continue, };
let delay = Duration::from_secs_f32(1.0 / self.tracker.current_rps());
tokio::time::sleep(delay).await;
match self.refresh_entry(&hash, &query).await {
Ok(_) => {
success_count = success_count.saturating_add(1);
self.health.record_success();
}
Err(_) => {
failure_count = failure_count.saturating_add(1);
self.health.record_failure();
}
}
}
if failure_count > success_count {
self.tracker.record_failure();
} else {
self.tracker
.record_success(self.config.rps_increment, self.config.max_rps);
}
tokio::time::sleep(self.config.batch_delay).await;
}
async fn refresh_entry(&self, hash: &[u8; 32], query: &str) -> Result<(), ()> {
let request = crate::proxy::types::QueryRequest {
query: query.to_string(),
top_k: Some(10), priority: None,
upstream_id: None,
upstream_type: None,
};
match self.upstream.query(&request).await {
Ok(response) => {
self.cache.insert_by_hash(
*hash,
response,
self.upstream.base_url().to_string(),
Some(query),
);
Ok(())
}
Err(_) => Err(()),
}
}
}
#[cfg(test)]
#[path = "tests/recovery_tests.rs"]
mod tests;