use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::time::Instant;
use crate::tui::point::{BrowseRead, ReqId};
pub(crate) struct RateCap {
interval: Duration,
state: Mutex<Slots>,
}
#[derive(Default)]
struct Slots {
next: Option<Instant>,
recent: VecDeque<Instant>,
}
const METER_WINDOW: Duration = Duration::from_secs(crate::tui::watched::METER_WINDOW_SECS);
impl RateCap {
pub(crate) fn new(per_second: u32) -> Self {
let interval = match per_second {
0 => Duration::ZERO,
n => Duration::from_secs(1) / n,
};
Self {
interval,
state: Mutex::new(Slots::default()),
}
}
fn reserve(&self, now: Instant) -> Instant {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let slot = state.next.map_or(now, |next| next.max(now));
state.next = Some(slot + self.interval);
while state
.recent
.front()
.is_some_and(|at| now.saturating_duration_since(*at) >= METER_WINDOW)
{
state.recent.pop_front();
}
state.recent.push_back(slot);
slot
}
pub(crate) async fn acquire(&self) {
let slot = self.reserve(Instant::now());
if !self.interval.is_zero() {
tokio::time::sleep_until(slot).await;
}
}
pub(crate) async fn wait_free(&self) {
loop {
let next = self
.state
.lock()
.unwrap_or_else(PoisonError::into_inner)
.next;
match next {
Some(at) if at > Instant::now() => tokio::time::sleep_until(at).await,
_ => return,
}
}
}
pub(crate) fn recent(&self) -> usize {
let now = Instant::now();
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state
.recent
.iter()
.filter(|at| **at <= now && now.saturating_duration_since(**at) < METER_WINDOW)
.count()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Job {
pub(crate) req: ReqId,
pub(crate) read: BrowseRead,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Task {
Read(Job),
Names,
}
#[derive(Debug)]
pub(crate) struct Started {
pub(crate) device: u32,
pub(crate) task: Task,
pub(crate) cancel: Arc<AtomicBool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Device,
Object,
Names,
}
#[derive(Default)]
struct Slot {
running: Option<(Kind, Arc<AtomicBool>)>,
device: Option<Job>,
object: Option<Job>,
}
#[derive(Default)]
pub(crate) struct Scheduler {
slots: HashMap<u32, Slot>,
}
impl Scheduler {
pub(crate) fn submit(&mut self, device: u32, job: Job) -> Option<Started> {
let slot = self.slots.entry(device).or_default();
let reads_device = job.read == BrowseRead::Device;
let Some((running, cancel)) = &slot.running else {
return Some(Self::start(slot, device, Task::Read(job)));
};
if reads_device || *running != Kind::Device {
cancel.store(true, Ordering::Relaxed);
}
if reads_device {
slot.object = None;
slot.device = Some(job);
} else {
slot.object = Some(job);
}
None
}
pub(crate) fn submit_names(&mut self, device: u32) -> Option<Started> {
let slot = self.slots.entry(device).or_default();
if slot.running.is_some() {
return None;
}
Some(Self::start(slot, device, Task::Names))
}
pub(crate) fn finished(&mut self, device: u32, names: bool) -> Option<Started> {
let slot = self.slots.get_mut(&device)?;
slot.running = None;
match slot.device.take().or_else(|| slot.object.take()) {
Some(job) => Some(Self::start(slot, device, Task::Read(job))),
None if names => Some(Self::start(slot, device, Task::Names)),
None => {
self.slots.remove(&device);
None
}
}
}
pub(crate) fn stop(&mut self, device: u32) {
if let Some(slot) = self.slots.get_mut(&device) {
slot.device = None;
slot.object = None;
if let Some((_, cancel)) = &slot.running {
cancel.store(true, Ordering::Relaxed);
}
}
}
fn start(slot: &mut Slot, device: u32, task: Task) -> Started {
let cancel = Arc::new(AtomicBool::new(false));
let kind = match &task {
Task::Read(job) if job.read == BrowseRead::Device => Kind::Device,
Task::Read(_) => Kind::Object,
Task::Names => Kind::Names,
};
slot.running = Some((kind, Arc::clone(&cancel)));
Started {
device,
task,
cancel,
}
}
}
#[cfg(test)]
mod tests {
use bacnet_types::enums::ObjectType;
use bacnet_types::primitives::ObjectIdentifier;
use super::*;
fn object(req: ReqId, instance: u32) -> Job {
Job {
req,
read: BrowseRead::Object(
ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance).unwrap(),
),
}
}
fn device(req: ReqId) -> Job {
Job {
req,
read: BrowseRead::Device,
}
}
#[test]
fn one_job_per_device_and_the_newest_waiting_wins() {
let mut scheduler = Scheduler::default();
let first = scheduler.submit(7, device(1)).expect("idle device starts");
assert!(scheduler.submit(7, object(2, 1)).is_none());
assert!(scheduler.submit(7, object(3, 2)).is_none());
assert!(
!first.cancel.load(Ordering::Relaxed),
"an object read doesn't stop the device read"
);
assert!(scheduler.submit(8, object(4, 1)).is_some());
let next = scheduler
.finished(7, false)
.expect("the waiting object read");
assert_eq!(next.task, Task::Read(object(3, 2)));
assert!(scheduler.finished(7, false).is_none());
assert!(scheduler.submit(7, object(5, 3)).is_some(), "idle again");
}
#[test]
fn a_newer_read_stops_the_running_one_it_replaces() {
let mut scheduler = Scheduler::default();
let running = scheduler.submit(7, object(1, 1)).unwrap();
assert!(scheduler.submit(7, object(2, 2)).is_none());
assert!(running.cancel.load(Ordering::Relaxed));
assert!(scheduler.submit(7, device(3)).is_none());
assert_eq!(
scheduler.finished(7, false).unwrap().task,
Task::Read(device(3))
);
let running = scheduler.finished(7, false);
assert!(running.is_none(), "the object read was dropped");
}
#[test]
fn stop_drops_waiting_jobs_and_stops_the_running_one() {
let mut scheduler = Scheduler::default();
let running = scheduler.submit(7, device(1)).unwrap();
scheduler.submit(7, object(2, 1));
scheduler.stop(7);
assert!(running.cancel.load(Ordering::Relaxed));
assert!(scheduler.finished(7, false).is_none());
}
#[test]
fn name_reads_wait_for_and_give_way_to_every_other_read() {
let mut scheduler = Scheduler::default();
let running = scheduler.submit(7, object(1, 1)).unwrap();
assert!(scheduler.submit_names(7).is_none(), "the device is busy");
assert!(!running.cancel.load(Ordering::Relaxed));
let names = scheduler.finished(7, true).expect("names next");
assert_eq!(names.task, Task::Names);
assert!(
scheduler.submit_names(7).is_none(),
"the running one takes them"
);
assert!(scheduler.submit(7, object(2, 2)).is_none());
assert!(
names.cancel.load(Ordering::Relaxed),
"an object read stops it"
);
let next = scheduler.finished(7, true).unwrap();
assert_eq!(next.task, Task::Read(object(2, 2)));
assert_eq!(scheduler.finished(7, true).unwrap().task, Task::Names);
assert!(scheduler.finished(7, false).is_none(), "nothing left");
assert_eq!(scheduler.submit_names(8).unwrap().task, Task::Names);
}
#[tokio::test(start_paused = true)]
async fn the_rate_cap_spaces_requests_evenly() {
let cap = RateCap::new(4);
let start = Instant::now();
let mut slots = Vec::new();
for _ in 0..3 {
cap.acquire().await;
slots.push(Instant::now() - start);
}
assert_eq!(
slots,
[
Duration::ZERO,
Duration::from_millis(250),
Duration::from_millis(500)
]
);
tokio::time::sleep(Duration::from_secs(1)).await;
let before = Instant::now();
cap.acquire().await;
assert_eq!(Instant::now(), before);
let none = RateCap::new(0);
let before = Instant::now();
for _ in 0..10 {
none.acquire().await;
}
assert_eq!(Instant::now(), before);
}
#[tokio::test(start_paused = true)]
async fn waiting_for_a_free_slot_takes_none() {
let cap = RateCap::new(4);
let start = Instant::now();
cap.acquire().await;
cap.wait_free().await;
assert_eq!(Instant::now() - start, Duration::from_millis(250));
cap.wait_free().await;
cap.acquire().await;
assert_eq!(Instant::now() - start, Duration::from_millis(250));
assert_eq!(cap.recent(), 2);
}
}