use std::collections::VecDeque;
use std::num::NonZeroU64;
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use tokio::sync::watch;
use crate::boot::Timestamp;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sequenced<T> {
pub seq: NonZeroU64,
pub at: Timestamp,
pub item: T,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page<T> {
pub items: Vec<Sequenced<T>>,
pub next_cursor: u64,
pub truncated: Option<u64>,
}
#[derive(Debug)]
pub struct Ring<T> {
state: Mutex<VecDeque<Sequenced<T>>>,
cap: usize,
last: watch::Sender<u64>,
}
impl<T: Clone> Ring<T> {
#[must_use]
pub fn new(cap: usize) -> Self {
Self {
state: Mutex::new(VecDeque::with_capacity(cap.min(1024))),
cap: cap.max(1),
last: watch::channel(0).0,
}
}
pub fn push(&self, item: T) -> NonZeroU64 {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let seq = NonZeroU64::new(*self.last.borrow() + 1).unwrap_or(NonZeroU64::MIN);
if state.len() == self.cap {
state.pop_front();
}
state.push_back(Sequenced {
seq,
at: Timestamp::now(),
item,
});
self.last.send_replace(seq.get());
seq
}
#[must_use]
pub fn last_seq(&self) -> u64 {
*self.last.borrow()
}
#[must_use]
pub fn page(&self, after: u64, limit: usize, keep: impl Fn(&T) -> bool) -> Page<T> {
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let last = *self.last.borrow();
let oldest = state.front().map_or(last, |s| s.seq.get() - 1);
let truncated = (after < oldest && after < last).then_some(oldest);
let mut items = Vec::new();
let mut scanned = after;
for entry in state.iter().filter(|s| s.seq.get() > after) {
if items.len() == limit {
break;
}
scanned = entry.seq.get();
if keep(&entry.item) {
items.push(entry.clone());
}
}
Page {
items,
next_cursor: scanned,
truncated,
}
}
pub async fn wait_page(
&self,
after: u64,
limit: usize,
wait: Duration,
keep: impl Fn(&T) -> bool,
) -> Page<T> {
let page = self.page(after, limit, &keep);
if !page.items.is_empty() || wait.is_zero() {
return page;
}
let mut rx = self.last.subscribe();
let deadline = tokio::time::Instant::now() + wait;
let mut cursor = page.next_cursor;
loop {
let arrived =
tokio::time::timeout_at(deadline, rx.wait_for(|last| *last > cursor)).await;
let page = self.page(cursor, limit, &keep);
if !page.items.is_empty() || arrived.is_err() {
return page;
}
cursor = page.next_cursor;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_page_resumes_from_its_cursor_and_says_when_it_missed_items() {
let ring = Ring::new(3);
for n in 1..=5 {
ring.push(n);
}
let all = ring.page(0, 10, |_| true);
assert_eq!(
all.items.iter().map(|s| s.item).collect::<Vec<_>>(),
[3, 4, 5]
);
assert_eq!(all.truncated, Some(2), "items 1 and 2 were lost");
assert_eq!(all.next_cursor, 5);
let tail = ring.page(3, 10, |_| true);
assert_eq!(
tail.items.iter().map(|s| s.item).collect::<Vec<_>>(),
[4, 5]
);
assert_eq!(tail.truncated, None);
let odd = ring.page(0, 10, |n| n % 2 == 1);
assert_eq!(odd.items.iter().map(|s| s.item).collect::<Vec<_>>(), [3, 5]);
assert_eq!(
odd.next_cursor, 5,
"the cursor passes what the filter dropped"
);
assert!(ring.page(5, 10, |_| true).items.is_empty());
}
#[tokio::test(start_paused = true)]
async fn a_reader_waits_for_the_next_item_or_its_budget() {
let ring = std::sync::Arc::new(Ring::new(8));
let empty = ring
.wait_page(0, 10, Duration::from_millis(50), |_| true)
.await;
assert!(empty.items.is_empty());
let writer = std::sync::Arc::clone(&ring);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
writer.push("even");
writer.push("wanted");
});
let page = ring
.wait_page(0, 10, Duration::from_secs(5), |s: &&str| *s == "wanted")
.await;
assert_eq!(
page.items.iter().map(|s| s.item).collect::<Vec<_>>(),
["wanted"]
);
}
}