TokioMiddleTechnical

Какова возможность Tokio приостанавливать и продвигать время в тестах (tokio::time::pause())?

tokio::time::pause() замораживает внутренние часы Tokio; tokio::time::advance(d).await сдвигает время без реального ожидания, делая тесты с таймерами мгновенными и детерминированными.

Управление временем в тестах Tokio

Tokio предоставляет возможность «заморозить» внутренние часы и вручную продвигать время вперёд без реального ожидания. Это делает тесты с таймаутами, rate-limiter'ами и периодическими задачами мгновенными и детерминированными.

Ключевые функции

  • tokio::time::pause() — останавливает автоматическое продвижение времени.
  • tokio::time::advance(Duration) — сдвигает внутренние часы вперёд на указанный интервал и будит все таймеры, которые должны были сработать.
  • tokio::time::resume() — возобновляет нормальное течение времени.

Функции работают только в рантайме, собранном с feature test-util. Атрибут #[tokio::test] автоматически подключает этот feature.

Пример: тестирование таймаута

use tokio::time::{self, Duration, Instant};

#[tokio::test]
async fn test_timeout_fires() {
    time::pause();

    let start = Instant::now();
    let result = tokio::time::timeout(
        Duration::from_secs(30),
        async {
            time::sleep(Duration::from_secs(60)).await;
            42_u32
        },
    );

    // Продвигаем время на 31 секунду — таймаут должен сработать
    time::advance(Duration::from_secs(31)).await;

    assert!(result.await.is_err(), "timeout should have fired");
    // Реально прошли миллисекунды, а не 31 секунда
    assert!(start.elapsed() < Duration::from_secs(1));
}

Пример: периодический tick

use tokio::time::{self, Duration, interval};

#[tokio::test]
async fn test_interval_ticks_three_times() {
    time::pause();

    let mut ticker = interval(Duration::from_secs(10));
    let mut count = 0_u32;

    // Первый тик — немедленный
    ticker.tick().await;
    count += 1;

    for _ in 0..2 {
        time::advance(Duration::from_secs(10)).await;
        ticker.tick().await;
        count += 1;
    }

    assert_eq!(count, 3);
}

Пример: rate-limiter тест

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use tokio::time::{self, Duration};

async fn rate_limited_action(counter: Arc<AtomicU32>) {
    counter.fetch_add(1, Ordering::SeqCst);
    time::sleep(Duration::from_millis(100)).await;
}

#[tokio::test]
async fn test_only_one_per_100ms() {
    time::pause();
    let counter = Arc::new(AtomicU32::new(0));
    let c = counter.clone();

    tokio::spawn(async move {
        for _ in 0..5 {
            rate_limited_action(c.clone()).await;
        }
    });

    // Даём выполниться первому вызову
    time::advance(Duration::from_millis(50)).await;
    assert_eq!(counter.load(Ordering::SeqCst), 1);

    // Ждём ещё — запускается второй
    time::advance(Duration::from_millis(100)).await;
    assert_eq!(counter.load(Ordering::SeqCst), 2);
}

Cargo.toml — нужен feature

[dev-dependencies]
tokio = { version = "1", features = ["full", "test-util"] }

Подводные камни

  • Требуется flavor current_thread: pause() работает только в однопоточном рантайме (current_thread, который и является дефолтом для #[tokio::test]). В multi_thread вызов паникует.
  • advance() — async: нужно .await, иначе задачи не получат шанс выполниться и таймеры не сработают.
  • Сторонние таймеры не управляются: std::thread::sleep и std::time::Instant используют системные часы, не подвластные pause().
  • Забытый resume(): если тест завершился аварийно без resume(), последующие тесты в том же потоке могут получить замороженное время — изолируйте тесты или используйте tokio::time::resume() в деструкторе.
  • auto-advance при отсутствии задач: когда все задачи заблокированы на таймерах, рантайм автоматически прыгает к ближайшему дедлайну — это может удивить, если вы ожидаете явного контроля.
  • Не работает без test-util: в production-бинарниках pause()/advance() не скомпилируются без feature-флага.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics

Какова возможность Tokio приостанавливать и продвигать время в тестах (`tokio::time::pause()`)? | Talanto