use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use lru::LruCache;
use tokio::sync::RwLock;
use rmqtt::{context::ServerContext, types::NodeId};
use rmqtt_storage::DefaultStorageDB as StorageDb;
use crate::types::{CacheEntry, EntryFlags};
pub(crate) static UNPERSISTED_COUNT: AtomicI64 = AtomicI64::new(0);
pub(crate) type HistoryCache = Arc<RwLock<LruCache<u64, CacheEntry>>>;
#[derive(Clone)]
pub(crate) struct HistoryCaches {
pub stats: HistoryCache,
pub metrics: HistoryCache,
}
impl HistoryCaches {
pub fn new(stats: HistoryCache, metrics: HistoryCache) -> Self {
Self { stats, metrics }
}
}
pub(crate) const STATS_PREFIX: &str = "stats_hist";
pub(crate) const METRICS_PREFIX: &str = "metrics_hist";
pub(crate) fn new_cache(capacity: usize) -> anyhow::Result<HistoryCache> {
let non_zero = std::num::NonZeroUsize::new(capacity)
.ok_or_else(|| anyhow::anyhow!("[http-api] LRU cache capacity must be > 0, got {capacity}"))?;
Ok(Arc::new(RwLock::new(LruCache::new(non_zero))))
}
pub fn start_flusher(
scx: ServerContext,
storage_db: StorageDb,
stats_cache: HistoryCache,
metrics_cache: HistoryCache,
interval: Duration,
retention: Duration,
) -> tokio::task::JoinHandle<()> {
let node_id = scx.node.id();
let exec = scx.get_exec(("HISTORY_FLUSHER_EXEC", 10, 10_000));
tokio::spawn(async move {
let mut timer = tokio::time::interval(interval);
timer.tick().await;
loop {
timer.tick().await;
let ts = rounded_timestamp_ms(interval);
let stats = scx.stats.clone(&scx).await;
let stats_value = stats.to_json(&scx).await;
if let Ok(stats_json) = serde_json::to_string(&stats_value) {
let storage_key = make_key(STATS_PREFIX, node_id, ts);
let entry = CacheEntry::new(stats_json);
stats_cache.write().await.put(ts, entry);
let db = storage_db.clone();
let cache = stats_cache.clone();
exec.spawn(async move {
flush_one(&db, &cache, &storage_key, ts, retention).await;
});
}
let metrics = scx.metrics.clone();
let metrics_value = metrics.to_json();
if let Ok(metrics_json) = serde_json::to_string(&metrics_value) {
let storage_key = make_key(METRICS_PREFIX, node_id, ts);
let entry = CacheEntry::new(metrics_json);
metrics_cache.write().await.put(ts, entry);
let db = storage_db.clone();
let cache = metrics_cache.clone();
exec.spawn(async move {
flush_one(&db, &cache, &storage_key, ts, retention).await;
});
}
}
})
}
async fn flush_one(
storage_db: &StorageDb,
cache: &HistoryCache,
storage_key: &str,
ts: u64,
retention: Duration,
) {
let ok = {
let key = storage_key.as_bytes();
storage_db
.insert(key, &cache.read().await.peek(&ts).map(|e| e.json.clone()).unwrap_or_default())
.await
.is_ok()
&& storage_db.expire(key, retention.as_millis() as i64).await.is_ok()
};
if ok {
if let Some(entry) = cache.write().await.get_mut(&ts) {
entry.flags = EntryFlags::new(EntryFlags::NONE);
}
} else {
log::error!("[http-api] flush {} error", storage_key);
if let Some(entry) = cache.write().await.get_mut(&ts) {
entry.flags = EntryFlags::new(EntryFlags::FAILED);
}
UNPERSISTED_COUNT.fetch_add(1, Ordering::Release);
}
}
pub fn start_recovery_loop(
stats_cache: HistoryCache,
metrics_cache: HistoryCache,
storage_db: StorageDb,
retention: Duration,
node_id: NodeId,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if UNPERSISTED_COUNT.load(Ordering::Acquire) == 0 {
continue;
}
recover_one_cache(&stats_cache, &storage_db, STATS_PREFIX, node_id, retention).await;
recover_one_cache(&metrics_cache, &storage_db, METRICS_PREFIX, node_id, retention).await;
}
})
}
async fn recover_one_cache(
cache: &HistoryCache,
storage_db: &StorageDb,
prefix: &str,
node_id: NodeId,
retention: Duration,
) {
let failed: Vec<(u64, String)> = {
let guard = cache.read().await;
guard
.iter()
.filter(|(_, e)| e.flags.needs_recovery())
.take(200)
.map(|(ts, e)| (*ts, e.json.clone()))
.collect()
};
if failed.is_empty() {
return;
}
for (ts, json) in &failed {
let storage_key = make_key(prefix, node_id, *ts);
let ok = storage_db.insert(storage_key.as_bytes(), json).await.is_ok()
&& storage_db.expire(storage_key.as_bytes(), retention.as_millis() as i64).await.is_ok();
if ok {
if let Some(entry) = cache.write().await.get_mut(ts) {
entry.flags = EntryFlags::new(EntryFlags::NONE);
}
let _ = UNPERSISTED_COUNT.fetch_update(Ordering::Release, Ordering::Acquire, |v| {
if v > 0 {
Some(v - 1)
} else {
None
}
});
} else {
log::error!("[http-api] recovery flush {} error", storage_key);
break;
}
}
}
pub fn start_warmup(
stats_cache: HistoryCache,
metrics_cache: HistoryCache,
storage_db: StorageDb,
node_id: NodeId,
retention: Duration,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let stats_count = load_into_cache(&stats_cache, &storage_db, STATS_PREFIX, node_id, retention).await;
let metrics_count =
load_into_cache(&metrics_cache, &storage_db, METRICS_PREFIX, node_id, retention).await;
log::info!("[http-api] history cache warmup complete, stats={stats_count}, metrics={metrics_count}");
})
}
async fn load_into_cache(
cache: &HistoryCache,
storage_db: &StorageDb,
prefix: &str,
node_id: NodeId,
retention: Duration,
) -> usize {
let pattern = format!("{}:{}:*", prefix, node_id);
let retention_ms = retention.as_millis() as u64;
let now_ms =
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis()
as u64;
let cutoff = now_ms.saturating_sub(retention_ms);
let keys_sorted: Vec<(u64, Vec<u8>)> = {
let mut iter_storage_db = storage_db.clone();
let mut pairs = Vec::new();
if let Ok(mut iter) = iter_storage_db.scan(pattern.as_bytes()).await {
while let Some(key) = iter.next().await {
if let Ok(key_bytes) = key {
let key_str = String::from_utf8_lossy(&key_bytes);
if let Some(ts) = key_str.rsplit(':').next().and_then(|s| s.parse::<u64>().ok()) {
pairs.push((ts, key_bytes));
}
}
}
}
pairs.sort_by_key(|(ts, _)| *ts);
pairs
};
let mut count = 0usize;
for (ts, key_bytes) in &keys_sorted {
if *ts < cutoff {
log::debug!(
"[http-api] warmup discarding expired entry ts={ts} (< cutoff={cutoff}), removing from storage"
);
let _ = storage_db.remove(key_bytes).await;
continue;
}
if let Ok(Some(json)) = storage_db.get::<_, String>(key_bytes).await {
let entry = CacheEntry { json, flags: EntryFlags::new(EntryFlags::NONE) };
cache.write().await.put(*ts, entry);
count += 1;
}
}
count
}
#[inline]
pub(crate) fn make_key(prefix: &str, node_id: NodeId, ts: u64) -> String {
format!("{prefix}:{node_id}:{ts}")
}
#[inline]
pub(crate) fn rounded_timestamp_ms(interval: Duration) -> u64 {
let interval_ms = interval.as_millis() as u64;
let ms =
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis()
as u64;
ms / interval_ms * interval_ms
}