use super::types::GeographicBeacon;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::{debug, info};
pub struct BeaconJanitor {
nearby_beacons: Arc<RwLock<HashMap<String, GeographicBeacon>>>,
ttl: Duration,
cleanup_interval: Duration,
running: Arc<RwLock<bool>>,
}
impl BeaconJanitor {
pub fn new(
nearby_beacons: Arc<RwLock<HashMap<String, GeographicBeacon>>>,
ttl: Duration,
cleanup_interval: Duration,
) -> Self {
Self {
nearby_beacons,
ttl,
cleanup_interval,
running: Arc::new(RwLock::new(false)),
}
}
pub async fn start(&self) {
let mut running = self.running.write().await;
if *running {
debug!("Beacon janitor already running");
return;
}
*running = true;
drop(running);
info!(
"Starting beacon janitor with TTL {:?} and cleanup interval {:?}",
self.ttl, self.cleanup_interval
);
let nearby_beacons = self.nearby_beacons.clone();
let ttl_secs = self.ttl.as_secs();
let cleanup_interval = self.cleanup_interval;
let running_clone = self.running.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(cleanup_interval);
while *running_clone.read().await {
interval.tick().await;
let mut beacons = nearby_beacons.write().await;
let initial_count = beacons.len();
beacons.retain(|_, beacon| !beacon.is_expired(ttl_secs));
let removed_count = initial_count - beacons.len();
if removed_count > 0 {
debug!("Removed {} expired beacons", removed_count);
}
}
info!("Beacon janitor stopped");
});
}
pub async fn stop(&self) {
let mut running = self.running.write().await;
*running = false;
info!("Stopped beacon janitor");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::beacon::types::{GeoPosition, HierarchyLevel};
#[tokio::test]
async fn test_janitor_lifecycle() {
let beacons = Arc::new(RwLock::new(HashMap::new()));
let janitor =
BeaconJanitor::new(beacons, Duration::from_secs(30), Duration::from_millis(100));
janitor.start().await;
assert!(*janitor.running.read().await);
tokio::time::sleep(Duration::from_millis(50)).await;
janitor.stop().await;
assert!(!*janitor.running.read().await);
}
#[tokio::test]
async fn test_janitor_cleanup() {
let beacons = Arc::new(RwLock::new(HashMap::new()));
let fresh_beacon = GeographicBeacon::new(
"fresh".to_string(),
GeoPosition::new(37.0, -122.0),
HierarchyLevel::Platform,
);
beacons
.write()
.await
.insert("fresh".to_string(), fresh_beacon);
let mut expired_beacon = GeographicBeacon::new(
"expired".to_string(),
GeoPosition::new(37.0, -122.0),
HierarchyLevel::Platform,
);
expired_beacon.timestamp = 0; beacons
.write()
.await
.insert("expired".to_string(), expired_beacon);
assert_eq!(beacons.read().await.len(), 2);
let janitor = BeaconJanitor::new(
beacons.clone(),
Duration::from_secs(5),
Duration::from_millis(50),
);
janitor.start().await;
tokio::time::sleep(Duration::from_millis(200)).await;
janitor.stop().await;
assert_eq!(beacons.read().await.len(), 1);
assert!(beacons.read().await.contains_key("fresh"));
assert!(!beacons.read().await.contains_key("expired"));
}
#[tokio::test]
async fn test_janitor_double_start() {
let beacons = Arc::new(RwLock::new(HashMap::new()));
let janitor =
BeaconJanitor::new(beacons, Duration::from_secs(30), Duration::from_millis(100));
janitor.start().await;
assert!(*janitor.running.read().await);
janitor.start().await;
assert!(*janitor.running.read().await);
janitor.stop().await;
}
#[tokio::test]
async fn test_janitor_stop_before_start() {
let beacons = Arc::new(RwLock::new(HashMap::new()));
let janitor =
BeaconJanitor::new(beacons, Duration::from_secs(30), Duration::from_millis(100));
janitor.stop().await;
assert!(!*janitor.running.read().await);
}
#[tokio::test]
async fn test_janitor_construction() {
let beacons: Arc<RwLock<HashMap<String, GeographicBeacon>>> =
Arc::new(RwLock::new(HashMap::new()));
let janitor = BeaconJanitor::new(beacons, Duration::from_secs(60), Duration::from_secs(10));
assert!(!*janitor.running.read().await);
}
#[tokio::test]
async fn test_janitor_empty_map_cleanup() {
let beacons = Arc::new(RwLock::new(HashMap::new()));
let janitor = BeaconJanitor::new(
beacons.clone(),
Duration::from_secs(5),
Duration::from_millis(50),
);
janitor.start().await;
tokio::time::sleep(Duration::from_millis(150)).await;
janitor.stop().await;
assert_eq!(beacons.read().await.len(), 0);
}
}