use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use dashmap::DashMap;
use tokio::sync::Notify;
use crate::message::Transaction;
use crate::simulation::TimeSource;
type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
#[derive(Clone)]
pub(crate) struct StreamProgressHandle {
last_progress_millis: Arc<AtomicU64>,
notify: Arc<Notify>,
now_millis: Arc<dyn Fn() -> u64 + Send + Sync>,
}
impl std::fmt::Debug for StreamProgressHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamProgressHandle")
.field(
"last_progress_millis",
&self.last_progress_millis.load(Ordering::Relaxed),
)
.finish_non_exhaustive()
}
}
impl StreamProgressHandle {
pub(crate) fn new<T: TimeSource>(time_source: T) -> Self {
let now_ts = time_source;
let now_millis: Arc<dyn Fn() -> u64 + Send + Sync> =
Arc::new(move || now_ts.now().as_millis() as u64);
let initial = now_millis();
Self {
last_progress_millis: Arc::new(AtomicU64::new(initial)),
notify: Arc::new(Notify::new()),
now_millis,
}
}
pub(crate) fn record(&self) {
let now = (self.now_millis)();
self.last_progress_millis.store(now, Ordering::Relaxed);
self.notify.notify_one();
}
pub(crate) fn since_last(&self) -> Duration {
let now = (self.now_millis)();
let last = self.last_progress_millis.load(Ordering::Relaxed);
Duration::from_millis(now.saturating_sub(last))
}
pub(crate) async fn notified(&self) {
self.notify.notified().await;
}
}
#[derive(Clone)]
pub(crate) struct StreamProgress {
handle: StreamProgressHandle,
sleep: Arc<dyn Fn(Duration) -> BoxFuture + Send + Sync>,
}
impl StreamProgress {
pub(crate) fn new<T: TimeSource>(time_source: T) -> Self {
let handle = StreamProgressHandle::new(time_source.clone());
let sleep_ts = time_source;
Self {
handle,
sleep: Arc::new(move |d| sleep_ts.sleep(d)),
}
}
pub(crate) fn handle(&self) -> StreamProgressHandle {
self.handle.clone()
}
pub(crate) fn sleep(&self, duration: Duration) -> BoxFuture {
(self.sleep)(duration)
}
}
pub(crate) struct StreamProgressRegistry {
handles: DashMap<Transaction, StreamProgressHandle>,
}
impl StreamProgressRegistry {
pub(crate) fn new() -> Self {
Self {
handles: DashMap::new(),
}
}
pub(crate) fn insert(&self, tx: Transaction, handle: StreamProgressHandle) {
self.handles.insert(tx, handle);
}
pub(crate) fn remove(&self, tx: &Transaction) {
self.handles.remove(tx);
}
pub(crate) fn get(&self, tx: &Transaction) -> Option<StreamProgressHandle> {
self.handles.get(tx).map(|h| h.clone())
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.handles.len()
}
}
impl Default for StreamProgressRegistry {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct StreamProgressGuard {
registry: Arc<StreamProgressRegistry>,
tx: Transaction,
}
impl StreamProgressGuard {
pub(crate) fn new(
registry: Arc<StreamProgressRegistry>,
tx: Transaction,
handle: StreamProgressHandle,
) -> Self {
registry.insert(tx, handle);
Self { registry, tx }
}
}
impl Drop for StreamProgressGuard {
fn drop(&mut self) {
self.registry.remove(&self.tx);
}
}
pub(crate) const STREAM_OP_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(30);
#[cfg(test)]
mod tests {
use super::*;
use crate::operations::put::PutMsg;
use crate::simulation::VirtualTime;
#[test]
fn record_advances_last_progress_and_since_last_tracks_it() {
let clock = VirtualTime::new();
let handle = StreamProgressHandle::new(clock.clone());
assert_eq!(handle.since_last(), Duration::ZERO);
clock.advance(Duration::from_secs(5));
assert_eq!(handle.since_last(), Duration::from_secs(5));
handle.record();
assert_eq!(handle.since_last(), Duration::ZERO);
clock.advance(Duration::from_millis(2_500));
assert_eq!(handle.since_last(), Duration::from_millis(2_500));
}
#[test]
fn writer_and_reader_share_one_clock_so_stall_is_detectable() {
let clock = VirtualTime::new();
clock.advance(Duration::from_secs(120));
let reader = StreamProgressHandle::new(clock.clone());
let writer = reader.clone();
writer.record();
assert_eq!(reader.since_last(), Duration::ZERO, "fresh record → 0");
clock.advance(STREAM_OP_INACTIVITY_TIMEOUT);
assert_eq!(
reader.since_last(),
STREAM_OP_INACTIVITY_TIMEOUT,
"after the writer's record + 30 s of silence, since_last must show \
the true elapsed time so the stall fires; a cross-epoch clock would \
saturate this to 0 and defeat the whole fix"
);
}
#[test]
fn since_last_saturates_when_progress_is_in_the_future() {
let clock = VirtualTime::new();
let handle = StreamProgressHandle::new(clock);
handle.record();
assert_eq!(handle.since_last(), Duration::ZERO);
}
#[tokio::test]
async fn notified_wakes_after_record() {
let handle = StreamProgressHandle::new(VirtualTime::new());
let observer = handle.clone();
handle.record();
tokio::time::timeout(Duration::from_secs(1), observer.notified())
.await
.expect("a record() before notified() must leave a permit");
}
#[test]
fn registry_insert_get_remove_roundtrip_and_is_leak_free() {
let registry = StreamProgressRegistry::new();
let tx = Transaction::new::<PutMsg>();
assert_eq!(registry.len(), 0);
assert!(registry.get(&tx).is_none());
registry.insert(tx, StreamProgressHandle::new(VirtualTime::new()));
assert_eq!(registry.len(), 1);
assert!(registry.get(&tx).is_some());
registry.remove(&tx);
assert_eq!(registry.len(), 0, "remove must leave the registry empty");
registry.remove(&tx);
assert_eq!(registry.len(), 0);
}
#[test]
fn guard_registers_on_new_and_removes_on_drop_including_early_drop() {
let registry = Arc::new(StreamProgressRegistry::new());
let tx = Transaction::new::<PutMsg>();
let handle = StreamProgressHandle::new(VirtualTime::new());
{
let _guard = StreamProgressGuard::new(registry.clone(), tx, handle.clone());
assert_eq!(registry.len(), 1, "guard must register on construction");
assert!(registry.get(&tx).is_some());
}
assert_eq!(registry.len(), 0, "guard must remove on Drop");
let guard = StreamProgressGuard::new(registry.clone(), tx, handle);
assert_eq!(registry.len(), 1);
drop(guard); assert_eq!(
registry.len(),
0,
"an early/cancellation drop of the guard must still remove the entry"
);
}
}