use std::time::Duration;
use tokio::sync::broadcast;
use crate::microgrid::caching_sender::CachingSender;
use crate::{Bounds, quantity::Quantity};
pub(crate) struct PoolBoundsTracker<S, Q: Quantity, F> {
pool_status_rx: broadcast::Receiver<S>,
pool_bounds_tx: CachingSender<Vec<Bounds<Q>>>,
compute: F,
label: String,
}
impl<S, Q, F> PoolBoundsTracker<S, Q, F>
where
S: Clone,
Q: Quantity,
F: Fn(&S) -> Vec<Bounds<Q>>,
{
pub(crate) fn new(
pool_status_rx: broadcast::Receiver<S>,
pool_bounds_tx: CachingSender<Vec<Bounds<Q>>>,
compute: F,
label: String,
) -> Self {
Self {
pool_status_rx,
pool_bounds_tx,
compute,
label,
}
}
pub(crate) async fn run(mut self) {
let mut interval = tokio::time::interval(Duration::from_millis(200));
let reason = loop {
tokio::select! {
recv = self.pool_status_rx.recv() => {
match recv {
Ok(snapshot) => {
let bounds = (self.compute)(&snapshot);
if !self.pool_bounds_tx.publish_if_changed(&bounds) {
break "no receivers";
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(
"{} bounds tracker lagged by {n} pool status updates.",
self.label,
);
}
Err(broadcast::error::RecvError::Closed) => {
break "pool status channel closed";
}
}
}
_ = interval.tick() => {
if self.pool_bounds_tx.receiver_count() == 0 {
break "no receivers";
}
}
}
};
tracing::debug!("{} bounds tracker shutting down: {reason}.", self.label);
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tokio::sync::broadcast;
use super::PoolBoundsTracker;
use crate::metric::AcPowerActive;
use crate::microgrid::caching_sender::CachingSender;
use crate::microgrid::pool_bounds::compute_pv_pool_bounds as compute_pool_bounds;
use crate::microgrid::telemetry_tracker::pv_pool_telemetry_tracker::PvPoolSnapshot;
#[tokio::test]
async fn exits_when_snapshot_channel_closes() {
let (snapshot_tx, snapshot_rx) = broadcast::channel::<PvPoolSnapshot>(16);
let bounds_tx = CachingSender::new();
let _bounds_rx = bounds_tx.subscribe_with_current();
let handle = tokio::spawn(
PoolBoundsTracker::new(
snapshot_rx,
bounds_tx,
compute_pool_bounds::<AcPowerActive>,
"test".to_string(),
)
.run(),
);
tokio::task::yield_now().await;
drop(snapshot_tx);
tokio::time::timeout(Duration::from_secs(1), handle)
.await
.expect("bounds tracker should exit when the snapshot channel closes")
.expect("bounds tracker task panicked");
}
#[tokio::test(start_paused = true)]
async fn exits_when_all_bounds_consumers_drop() {
let (_snapshot_tx, snapshot_rx) = broadcast::channel::<PvPoolSnapshot>(16);
let bounds_tx = CachingSender::new();
let bounds_rx = bounds_tx.subscribe_with_current();
let handle = tokio::spawn(
PoolBoundsTracker::new(
snapshot_rx,
bounds_tx,
compute_pool_bounds::<AcPowerActive>,
"test".to_string(),
)
.run(),
);
tokio::task::yield_now().await;
drop(bounds_rx);
tokio::time::advance(Duration::from_millis(200)).await;
tokio::time::timeout(Duration::from_secs(1), handle)
.await
.expect("bounds tracker should exit once its consumers drop")
.expect("bounds tracker task panicked");
}
}