· 2 мин чтения

Токен-бакет на целых числах: что проверяют property-тесты

Как rate-gate считает токен-бакет без float, зачем милли-токены и какие три свойства доказывают property-тесты proptest.

Проект: rate-gate

Ограничитель частоты выглядит простым, пока не доходит до округлений. Если хранить токены в float, ошибка накапливается, а два экземпляра алгоритма на одних данных могут разойтись. В rate-gate (Rust, axum) токены лежат в милли-токенах: целое число, где 1000 равно одному токену.

Что считается

Бакет хранит число милли-токенов и момент последнего пополнения. При проверке пополнение равно elapsed * refill, ограничивается ёмкостью, затем списывается cost (он может быть больше 1). Если токенов не хватает, сервис отвечает allowed: false и считает retry_after_ms, то есть сколько ждать до нужного числа токенов, и reset_ms, когда бакет наполнится.

Время передаётся в чистые функции параметром, поэтому в тестах его можно «прокручивать» без сна.

Три свойства

Property-тесты (proptest) проверяют не примеры, а законы:

  • за любой период бакет выдаёт не больше capacity + refill * elapsed;
  • залп без хода времени не превышает ёмкость;
  • оценка скользящего окна никогда не превышает лимит.

В README нет истории о найденной ошибке, и я её не выдумываю: смысл свойств в том, что они ловят класс ошибок округления и переполнения, а не одну конкретную.

Что это не гарантирует

  • Состояние в памяти одного экземпляра: реплики его не делят.
  • Скользящее окно приближённое: взвешенная сумма текущего и предыдущего окна, оценка округляется вверх, то есть консервативно.
  • Снапшот раз в 30 секунд не транзакционный: при аварии лимиты станут чуть мягче.