use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use dashmap::DashMap;
use tokio::sync::Semaphore;
#[cfg(feature = "telemetry")]
use crate::i18n::messages::{
MSG_LOG_OFFLOAD_LIFECYCLE_EVENT, MSG_LOG_OFFLOAD_TIMEOUT_POLICY_EXCEEDED, t,
};
#[cfg(feature = "metrics")]
fn record_counter(name: &str, value: u64) {
crate::infra::metrics::unified::GLOBAL_UNIFIED_METRICS.increment_counter(name, value);
}
#[cfg(not(feature = "metrics"))]
fn record_counter(_name: &str, _value: u64) {}
#[cfg(feature = "metrics")]
fn set_active_gauge(value: usize) {
crate::infra::metrics::unified::GLOBAL_UNIFIED_METRICS
.set_gauge("oxcache_offload_active", value as f64);
}
#[cfg(not(feature = "metrics"))]
fn set_active_gauge(_value: usize) {}
#[cfg(feature = "telemetry")]
#[inline]
fn telemetry_event(key: &str, event: &str) {
tracing::debug!(
target: "oxcache::offload",
key,
event,
"{}",
t(MSG_LOG_OFFLOAD_LIFECYCLE_EVENT, &[])
);
}
#[cfg(not(feature = "telemetry"))]
#[inline]
fn telemetry_event(_key: &str, _event: &str) {}
#[cfg(feature = "telemetry")]
#[inline]
fn telemetry_timeout(key: &str, elapsed: Duration, policy: &str) {
tracing::warn!(
target: "oxcache::offload",
key,
elapsed = ?elapsed,
policy,
"{}",
t(MSG_LOG_OFFLOAD_TIMEOUT_POLICY_EXCEEDED, &[])
);
}
#[cfg(not(feature = "telemetry"))]
#[inline]
fn telemetry_timeout(_key: &str, _elapsed: Duration, _policy: &str) {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeoutPolicy {
None,
Cancel(Duration),
Warn(Duration),
}
impl Default for TimeoutPolicy {
fn default() -> Self {
TimeoutPolicy::Warn(Duration::from_secs(30))
}
}
struct InFlightGuard {
in_flight: Arc<DashMap<Arc<str>, ()>>,
key: Arc<str>,
completed: Arc<AtomicU64>,
_permit: tokio::sync::OwnedSemaphorePermit,
}
impl InFlightGuard {
fn release(&mut self) {
if self.in_flight.remove(&self.key).is_some() {
set_active_gauge(self.in_flight.len());
self.completed.fetch_add(1, Ordering::Relaxed);
}
}
}
impl Drop for InFlightGuard {
fn drop(&mut self) {
self.release();
}
}
#[derive(Clone)]
pub struct OffloadManager {
in_flight: Arc<DashMap<Arc<str>, ()>>,
permits: Arc<Semaphore>,
max_concurrent_tasks: usize,
timeout_policy: TimeoutPolicy,
completed: Arc<AtomicU64>,
}
impl OffloadManager {
pub fn new(max_concurrent_tasks: usize) -> Self {
Self::with_policy(max_concurrent_tasks, TimeoutPolicy::default())
}
pub fn with_policy(max_concurrent_tasks: usize, timeout_policy: TimeoutPolicy) -> Self {
let max_concurrent_tasks = max_concurrent_tasks.max(1);
Self {
in_flight: Arc::new(DashMap::new()),
permits: Arc::new(Semaphore::new(max_concurrent_tasks)),
max_concurrent_tasks,
timeout_policy,
completed: Arc::new(AtomicU64::new(0)),
}
}
pub fn max_concurrent_tasks(&self) -> usize {
self.max_concurrent_tasks
}
pub fn timeout_policy(&self) -> TimeoutPolicy {
self.timeout_policy
}
pub fn spawn<F>(&self, key: impl Into<Arc<str>>, task: F) -> bool
where
F: std::future::Future<Output = ()> + Send + 'static,
{
let key: Arc<str> = key.into();
if self.in_flight.contains_key(&key) {
record_counter("oxcache_offload_deduplicated_total", 1);
telemetry_event(&key, "deduplicated");
return false;
}
let permit = match self.permits.clone().try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
telemetry_event(&key, "rejected_no_permit");
return false;
}
};
self.in_flight.insert(key.clone(), ());
set_active_gauge(self.in_flight.len());
record_counter("oxcache_offload_spawned_total", 1);
telemetry_event(&key, "spawned");
let guard = InFlightGuard {
in_flight: self.in_flight.clone(),
key: key.clone(),
completed: self.completed.clone(),
_permit: permit,
};
let policy = self.timeout_policy;
tokio::spawn(async move {
match policy {
TimeoutPolicy::None => task.await,
TimeoutPolicy::Cancel(limit) => {
if tokio::time::timeout(limit, task).await.is_err() {
record_counter("oxcache_offload_timeout_total", 1);
telemetry_timeout(&key, limit, "cancel");
}
}
TimeoutPolicy::Warn(limit) => {
let start = Instant::now();
task.await;
let elapsed = start.elapsed();
if elapsed > limit {
telemetry_timeout(&key, elapsed, "warn");
}
}
}
drop(guard);
});
true
}
pub fn is_in_flight(&self, key: &str) -> bool {
self.in_flight.contains_key(key)
}
pub fn in_flight_count(&self) -> usize {
self.in_flight.len()
}
pub fn cancel_all(&self) -> usize {
let removed = self.in_flight.len();
self.in_flight.clear();
set_active_gauge(self.in_flight.len());
removed
}
pub async fn wait_all(&self, timeout: Duration) -> usize {
let baseline = self.completed.load(Ordering::Relaxed);
let start = Instant::now();
while !self.in_flight.is_empty() {
if start.elapsed() >= timeout {
break;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
(self.completed.load(Ordering::Relaxed) - baseline) as usize
}
}
#[cfg(test)]
mod tests;