| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- //! Process-wide liveness cache for HA / RouterOS entity states.
- //!
- //! The dashboard's fleet probe runs every 30 s. Without a cache, every tick
- //! issues one HA `GET /api/states/...` per node/extra and — worse — a full
- //! RouterOS API login per switch. A dead switch must not be hammered, and a
- //! live one must not be asked to authenticate every 30 s. The cache keeps a
- //! state for a TTL (HA: 120 s, RouterOS: 300 s); a power action invalidates it
- //! so the next probe sees the real result immediately.
- use std::collections::HashMap;
- use std::sync::{LazyLock, Mutex};
- use std::time::{Duration, Instant};
- #[derive(Clone, Copy)]
- struct Entry {
- value: Option<bool>,
- checked: Instant,
- ttl: Duration,
- }
- static CACHE: LazyLock<Mutex<HashMap<String, Entry>>> =
- LazyLock::new(|| Mutex::new(HashMap::new()));
- fn lock_cache() -> std::sync::MutexGuard<'static, HashMap<String, Entry>> {
- CACHE
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- }
- /// Fresh cached state (`true`/`false`) or `None` when absent/expired.
- pub fn fresh(key: &str) -> Option<Option<bool>> {
- let mut cache = lock_cache();
- match cache.get(key) {
- Some(e) if e.checked.elapsed() < e.ttl => Some(e.value),
- _ => {
- cache.remove(key);
- None
- }
- }
- }
- /// Store a state for `ttl`.
- pub fn store(key: &str, value: Option<bool>, ttl: Duration) {
- let mut cache = lock_cache();
- cache.insert(
- key.to_string(),
- Entry {
- value,
- checked: Instant::now(),
- ttl,
- },
- );
- // Keep the map bounded by dropping expired entries on write.
- cache.retain(|_, e| e.checked.elapsed() < e.ttl);
- }
- /// Drop one cached entity (e.g. after a HA/ RouterOS toggle).
- pub fn invalidate(key: &str) {
- lock_cache().remove(key);
- }
- /// Drop every entity (after a power flow; the dashboard's next tick re-probes).
- pub fn invalidate_all() {
- lock_cache().clear();
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn fresh_store_and_expire() {
- store("ha:switch.x", Some(true), Duration::from_secs(2));
- assert_eq!(fresh("ha:switch.x"), Some(Some(true)));
- // A zero TTL means "do not trust this entry".
- store("ha:switch.y", Some(false), Duration::ZERO);
- assert_eq!(fresh("ha:switch.y"), None);
- }
- #[test]
- fn invalidation_forces_reprobe() {
- store(
- "mikrotik:sw:10.0.0.1",
- Some(false),
- Duration::from_secs(300),
- );
- assert_eq!(fresh("mikrotik:sw:10.0.0.1"), Some(Some(false)));
- invalidate("mikrotik:sw:10.0.0.1");
- assert_eq!(fresh("mikrotik:sw:10.0.0.1"), None);
- store("ha:switch.z", Some(true), Duration::from_secs(120));
- invalidate_all();
- assert_eq!(fresh("ha:switch.z"), None);
- }
- }
|