entity_cache.rs 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. //! Process-wide liveness cache for HA / RouterOS entity states.
  2. //!
  3. //! The dashboard's fleet probe runs every 30 s. Without a cache, every tick
  4. //! issues one HA `GET /api/states/...` per node/extra and — worse — a full
  5. //! RouterOS API login per switch. A dead switch must not be hammered, and a
  6. //! live one must not be asked to authenticate every 30 s. The cache keeps a
  7. //! state for a TTL (HA: 120 s, RouterOS: 300 s); a power action invalidates it
  8. //! so the next probe sees the real result immediately.
  9. use std::collections::HashMap;
  10. use std::sync::{LazyLock, Mutex};
  11. use std::time::{Duration, Instant};
  12. #[derive(Clone, Copy)]
  13. struct Entry {
  14. value: Option<bool>,
  15. checked: Instant,
  16. ttl: Duration,
  17. }
  18. static CACHE: LazyLock<Mutex<HashMap<String, Entry>>> =
  19. LazyLock::new(|| Mutex::new(HashMap::new()));
  20. fn lock_cache() -> std::sync::MutexGuard<'static, HashMap<String, Entry>> {
  21. CACHE
  22. .lock()
  23. .unwrap_or_else(std::sync::PoisonError::into_inner)
  24. }
  25. /// Fresh cached state (`true`/`false`) or `None` when absent/expired.
  26. pub fn fresh(key: &str) -> Option<Option<bool>> {
  27. let mut cache = lock_cache();
  28. match cache.get(key) {
  29. Some(e) if e.checked.elapsed() < e.ttl => Some(e.value),
  30. _ => {
  31. cache.remove(key);
  32. None
  33. }
  34. }
  35. }
  36. /// Store a state for `ttl`.
  37. pub fn store(key: &str, value: Option<bool>, ttl: Duration) {
  38. let mut cache = lock_cache();
  39. cache.insert(
  40. key.to_string(),
  41. Entry {
  42. value,
  43. checked: Instant::now(),
  44. ttl,
  45. },
  46. );
  47. // Keep the map bounded by dropping expired entries on write.
  48. cache.retain(|_, e| e.checked.elapsed() < e.ttl);
  49. }
  50. /// Drop one cached entity (e.g. after a HA/ RouterOS toggle).
  51. pub fn invalidate(key: &str) {
  52. lock_cache().remove(key);
  53. }
  54. /// Drop every entity (after a power flow; the dashboard's next tick re-probes).
  55. pub fn invalidate_all() {
  56. lock_cache().clear();
  57. }
  58. #[cfg(test)]
  59. mod tests {
  60. use super::*;
  61. #[test]
  62. fn fresh_store_and_expire() {
  63. store("ha:switch.x", Some(true), Duration::from_secs(2));
  64. assert_eq!(fresh("ha:switch.x"), Some(Some(true)));
  65. // A zero TTL means "do not trust this entry".
  66. store("ha:switch.y", Some(false), Duration::ZERO);
  67. assert_eq!(fresh("ha:switch.y"), None);
  68. }
  69. #[test]
  70. fn invalidation_forces_reprobe() {
  71. store(
  72. "mikrotik:sw:10.0.0.1",
  73. Some(false),
  74. Duration::from_secs(300),
  75. );
  76. assert_eq!(fresh("mikrotik:sw:10.0.0.1"), Some(Some(false)));
  77. invalidate("mikrotik:sw:10.0.0.1");
  78. assert_eq!(fresh("mikrotik:sw:10.0.0.1"), None);
  79. store("ha:switch.z", Some(true), Duration::from_secs(120));
  80. invalidate_all();
  81. assert_eq!(fresh("ha:switch.z"), None);
  82. }
  83. }