use parking_lot::RwLock;
use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::proxy::cache::CacheStore;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct UpstreamVersion {
pub version: Option<String>,
pub corpus_hash: Option<String>,
pub doc_count: Option<u64>,
pub last_modified: Option<u64>,
}
impl UpstreamVersion {
pub fn new(version: String) -> Self {
Self {
version: Some(version),
..Default::default()
}
}
pub fn full(
version: Option<String>,
corpus_hash: Option<String>,
doc_count: Option<u64>,
last_modified: Option<u64>,
) -> Self {
Self {
version,
corpus_hash,
doc_count,
last_modified,
}
}
pub fn differs_from(&self, other: &UpstreamVersion) -> bool {
if let (Some(a), Some(b)) = (&self.version, &other.version) {
if a != b {
return true;
}
}
if let (Some(a), Some(b)) = (&self.corpus_hash, &other.corpus_hash) {
if a != b {
return true;
}
}
if let (Some(a), Some(b)) = (self.doc_count, other.doc_count) {
let diff = if a > b {
a.saturating_sub(b)
} else {
b.saturating_sub(a)
};
let max_val = a.max(b) as f64;
let threshold = (max_val * 0.01) as u64; if diff > threshold {
return true;
}
}
if let (Some(a), Some(b)) = (self.last_modified, other.last_modified) {
if a != b {
return true;
}
}
false
}
pub fn is_empty(&self) -> bool {
self.version.is_none()
&& self.corpus_hash.is_none()
&& self.doc_count.is_none()
&& self.last_modified.is_none()
}
}
#[derive(Debug, Clone)]
pub struct VersionCheckConfig {
pub version_endpoint: String,
pub poll_interval: Duration,
pub timeout: Duration,
pub invalidate_on_change: bool,
}
impl Default for VersionCheckConfig {
fn default() -> Self {
Self {
version_endpoint: String::new(),
poll_interval: Duration::from_secs(300), timeout: Duration::from_secs(10),
invalidate_on_change: true,
}
}
}
pub struct VersionCheckWorker {
cache: Arc<CacheStore>,
config: VersionCheckConfig,
current_version: Arc<RwLock<UpstreamVersion>>,
client: reqwest::Client,
changes_detected: std::sync::atomic::AtomicU64,
cancel: CancellationToken,
}
impl VersionCheckWorker {
pub fn new(
cache: Arc<CacheStore>,
config: VersionCheckConfig,
cancel: CancellationToken,
) -> Self {
let client = super::super::socket_tuning::create_tuned_client_default(config.timeout)
.build()
.unwrap_or_default();
Self {
cache,
config,
current_version: Arc::new(RwLock::new(UpstreamVersion::default())),
client,
changes_detected: std::sync::atomic::AtomicU64::new(0),
cancel,
}
}
pub fn current_version(&self) -> UpstreamVersion {
self.current_version.read().clone()
}
pub fn changes_detected(&self) -> u64 {
self.changes_detected
.load(std::sync::atomic::Ordering::Relaxed)
}
pub async fn run(&self) {
if self.config.version_endpoint.is_empty() {
return;
}
let mut interval = tokio::time::interval(self.config.poll_interval);
interval.tick().await;
loop {
tokio::select! {
_ = self.cancel.cancelled() => {
break;
}
_ = interval.tick() => {
self.check_version().await;
}
}
}
}
async fn check_version(&self) {
match self.fetch_version().await {
Ok(new_version) => {
let current = self.current_version.read().clone();
if !current.is_empty() && new_version.differs_from(¤t) {
self.changes_detected
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if self.config.invalidate_on_change {
self.cache.clear();
}
}
*self.current_version.write() = new_version;
}
Err(_) => {
}
}
}
async fn fetch_version(&self) -> Result<UpstreamVersion, ()> {
let response = self
.client
.get(&self.config.version_endpoint)
.send()
.await
.map_err(|_| ())?;
if !response.status().is_success() {
return Err(());
}
let body = tokio::time::timeout(self.config.timeout, response.text())
.await
.map_err(|_| ())? .map_err(|_| ())?;
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
let version = json
.get("version")
.and_then(|v| v.as_str())
.map(String::from);
let corpus_hash = json
.get("corpus_hash")
.or_else(|| json.get("hash"))
.and_then(|v| v.as_str())
.map(String::from);
let doc_count = json
.get("doc_count")
.or_else(|| json.get("count"))
.and_then(|v| v.as_u64());
let last_modified = json
.get("last_modified")
.or_else(|| json.get("timestamp"))
.and_then(|v| v.as_u64());
return Ok(UpstreamVersion::full(
version,
corpus_hash,
doc_count,
last_modified,
));
}
let version = body.trim().to_string();
if !version.is_empty() {
return Ok(UpstreamVersion::new(version));
}
Err(())
}
}
#[cfg(test)]
#[path = "tests/version_tests.rs"]
mod tests;