# tokio-interval-set
[](https://crates.io/crates/tokio-interval-set)
[](https://docs.rs/tokio-interval-set)
[](LICENSE)
A lightweight Rust utility for running a closure repeatedly on a fixed time
interval, backed by [`tokio`]'s time and task system.
`tokio-interval-set` provides a simple, safe, and ergonomic API for launching
periodic background tasks â think JavaScript's `setInterval`, but for async
Rust with cancellation handled via a dedicated handle.
---
## Features
- **âąïļ Fixed-interval execution** â Run any async closure on a repeating
[`tokio::time::Interval`].
- **ð Graceful cancellation** â Call [`clear()`] on the returned handle to
synchronously stop the loop after the current tick completes.
- **ð Drop-safe** â Dropping the handle (without calling `clear()`) also
stops the task on the next tick boundary; nothing leaks.
- **ðĻ Panic detection** â If the spawned task panics, calling `clear()` will
panic with a descriptive message so you know something went wrong.
- **ðŠķ Minimal** â One function, one struct, zero dependencies beyond
[`tokio`].
---
## Quick Start
Add the crate to your `Cargo.toml`:
```toml
[dependencies]
tokio-interval-set = "0.1.0"
tokio = { version = "1", features = ["time", "sync", "rt", "macros"] }
```
Then use it in your code:
```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::interval;
use tokio_interval_set::set_interval;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
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(100)), move || {
let count = Arc::clone(&count_cloned);
async move {
let mut c = count.lock().await;
*c += 1;
}
});
tokio::time::sleep(Duration::from_millis(350)).await;
handle.clear(); // stop the interval
let final_count = *count.lock().await;
assert!(final_count >= 3, "expected at least 3 ticks, got {}", final_count);
});
```
---
## API Overview
### `set_interval(interval, func) -> IntervalHandle`
Spawns a background task that calls `func()` every time the given
[`tokio::time::Interval`] ticks.
| `interval` | [`tokio::time::Interval`] | Controls the tick timing. |
| `func` | `Fn() -> Fut + Send + Sync + 'static` | Closure returning a future. |
### `IntervalHandle`
The handle returned by `set_interval`. It exposes one method:
| [`clear(self)`][`clear()`] | Synchronously stops the interval loop. Panics if the task has panicked. |
If the handle is **dropped** without calling `clear()`, the interval task is
cancelled on the next tick boundary.
---
## Examples
### Stopping via `clear()`
```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::interval;
use tokio_interval_set::set_interval;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let count = Arc::new(Mutex::new(0u64));
let count_clone = Arc::clone(&count);
let handle = set_interval(
interval(Duration::from_secs(1)),
move || {
let count = Arc::clone(&count_clone);
async move {
let mut c = count.lock().await;
*c += 1;
}
},
);
tokio::time::sleep(Duration::from_secs(3)).await;
handle.clear(); // â
clean shutdown
assert_eq!(*count.lock().await, 3);
});
```
### Dropping the handle (automatic cancellation)
```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::interval;
use tokio_interval_set::set_interval;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let values = Arc::new(Mutex::new(Vec::new()));
let values_clone = Arc::clone(&values);
let handle = set_interval(
interval(Duration::from_millis(100)),
move || {
let values = Arc::clone(&values_clone);
async move {
let mut v = values.lock().await;
v.push(v.len());
}
},
);
tokio::time::sleep(Duration::from_millis(250)).await;
drop(handle); // cancels on the next tick
// ... or just let `handle` go out of scope
});
```
---
## How It Works
1. [`set_interval`] creates a [`tokio::sync::oneshot`] channel and spawns a
background task via [`tokio::spawn`].
2. The task loops, using [`tokio::select!`] to wait on both the [`Interval`]
tick and a shutdown signal from the oneshot receiver.
3. On each tick, the closure is called and awaited.
4. When [`clear()`] is invoked (or the handle is dropped), the oneshot sender
is consumed / dropped, the receiver wakes up, and the loop exits.
This approach ensures that:
- The interval task runs **concurrently** with your other async work.
- The current tick's closure completes before the loop exits.
- No busy-waiting or manual thread management.
---
## Safety & Correctness
- **No `unsafe` code** â the crate is written entirely in safe Rust.
- **Cancellation is clean** â the shutdown signal is sent via a oneshot
channel; the loop exits on the next iteration.
- **Panic propagation** â if the closure panics, the spawned task panics and
the oneshot receiver is dropped. Calling `clear()` on a panicked task
produces a clear panic message (`"Interval was panic"`), so the error is
never silently swallowed.
---
## License
This project is licensed under the [MIT License](LICENSE).
[`tokio`]: https://tokio.rs
[`tokio::time::Interval`]: https://docs.rs/tokio/latest/tokio/time/struct.Interval.html
[`tokio::spawn`]: https://docs.rs/tokio/latest/tokio/fn.spawn.html
[`tokio::select!`]: https://docs.rs/tokio/latest/tokio/macro.select.html
[`tokio::sync::oneshot`]: https://docs.rs/tokio/latest/tokio/sync/oneshot/index.html
[`set_interval`]: https://docs.rs/tokio-interval-set/latest/tokio_interval_set/fn.set_interval.html
[`IntervalHandle`]: https://docs.rs/tokio-interval-set/latest/tokio_interval_set/struct.IntervalHandle.html
[`clear()`]: https://docs.rs/tokio-interval-set/latest/tokio_interval_set/struct.IntervalHandle.html#method.clear