tokio-interval-set 0.1.1

Run a closure repeatedly on a fixed time interval using tokio
Documentation
  • Coverage
  • 100%
    4 out of 4 items documented3 out of 4 items with examples
  • Size
  • Source code size: 22.63 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 353.08 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 11s Average build duration of successful builds.
  • all releases: 9s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • ordinaryday-my

tokio-interval-set

Crates.io Docs.rs License: MIT

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:

[dependencies]
tokio-interval-set = "0.1.0"
tokio = { version = "1", features = ["time", "sync", "rt", "macros"] }

Then use it in your code:

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.

Parameter Type Description
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:

Method Description
clear(self) 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()

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)

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.