use tokio::{select, sync::oneshot, time::Interval};
#[derive(Debug)]
pub struct IntervalHandle {
shutdown: oneshot::Sender<()>,
}
impl IntervalHandle {
pub fn clear(self) {
if self.shutdown.is_closed() {
panic!("Interval was panic");
}
self.shutdown.send(()).unwrap();
}
}
pub fn set_interval<F, Fut>(mut interval: Interval, func: F) -> IntervalHandle
where
F: Fn() -> Fut + Sync + Send + 'static,
Fut: Future<Output = ()> + Send,
{
let (tx, mut rx) = oneshot::channel();
tokio::spawn(async move {
loop {
select! {
_ = interval.tick() => {
func().await;
},
_ = &mut rx => break,
}
}
});
IntervalHandle { shutdown: tx }
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, time::Duration};
use tokio::{sync::Mutex, time::interval};
use super::*;
#[test]
fn tick_count() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
const INTERVAL_MS: u64 = 50;
const EXPECTED_TICKS: u64 = 5;
const SLEEP_MS: u64 = INTERVAL_MS * EXPECTED_TICKS + INTERVAL_MS / 2;
rt.block_on(async {
let count = Arc::new(Mutex::new(0u64));
let count_cloned = Arc::clone(&count);
let _handle = set_interval(
interval(Duration::from_millis(INTERVAL_MS)),
move || {
let count = Arc::clone(&count_cloned);
async move {
let mut c = count.lock().await;
*c += 1;
}
},
);
tokio::time::sleep(Duration::from_millis(SLEEP_MS)).await;
let final_count = *count.lock().await;
assert!(
final_count == EXPECTED_TICKS || final_count == EXPECTED_TICKS + 1,
"expected {EXPECTED_TICKS} or {} ticks, got {final_count}",
EXPECTED_TICKS + 1
);
});
}
#[test]
fn clear_stops_interval() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
const INTERVAL_MS: u64 = 50;
rt.block_on(async {
let count = Arc::new(Mutex::new(0u64));
let count_cloned = Arc::clone(&count);
let handle = set_interval(
interval(Duration::from_millis(INTERVAL_MS)),
move || {
let count = Arc::clone(&count_cloned);
async move {
let mut c = count.lock().await;
*c += 1;
}
},
);
tokio::time::sleep(Duration::from_millis(120)).await;
let after_two = *count.lock().await;
assert!(
after_two >= 2,
"expected at least 2 ticks before clear, got {after_two}"
);
handle.clear();
let count_after_clear = *count.lock().await;
tokio::time::sleep(Duration::from_millis(300)).await;
let final_count = *count.lock().await;
assert_eq!(
final_count, count_after_clear,
"count increased after clear(): was {count_after_clear}, now {final_count}"
);
});
}
#[test]
#[should_panic(expected = "Interval was panic")]
fn clear_panics_on_panicked_interval() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let handle = set_interval(interval(Duration::from_millis(10)), move || {
async move {
panic!("intentional panic in callback");
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
handle.clear();
});
}
#[test]
fn drop_handle_stops_interval() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
const INTERVAL_MS: u64 = 50;
rt.block_on(async {
let count = Arc::new(Mutex::new(0u64));
let count_cloned = Arc::clone(&count);
let handle = set_interval(
interval(Duration::from_millis(INTERVAL_MS)),
move || {
let count = Arc::clone(&count_cloned);
async move {
let mut c = count.lock().await;
*c += 1;
}
},
);
tokio::time::sleep(Duration::from_millis(120)).await;
assert!(*count.lock().await >= 2);
drop(handle);
let count_after_drop = *count.lock().await;
tokio::time::sleep(Duration::from_millis(300)).await;
let final_count = *count.lock().await;
assert_eq!(
final_count, count_after_drop,
"count increased after handle dropped: was {count_after_drop}, now {final_count}"
);
});
}
}