RateLimiter saveAllowance при Too Many Requests

Всё что касается построения API
Ответить
Аватара пользователя
Seagull
Сообщения: 31
Зарегистрирован: 2019.01.24, 20:00

RateLimiter saveAllowance при Too Many Requests

Сообщение Seagull »

В методе RateLimiter::checkRateLimit, при условии исчерпавшихся запросов, происходит вызов RateLimitInterface::saveAllowance c $current timestamp'ом.

Код: Выделить всё

    public function checkRateLimit($user, $request, $response, $action)
    {
        list($limit, $window) = $user->getRateLimit($request, $action);
        list($allowance, $timestamp) = $user->loadAllowance($request, $action);
        $current = time();
        $allowance += (int) (($current - $timestamp) * $limit / $window);
        if ($allowance > $limit) {
            $allowance = $limit;
        }
        if ($allowance < 1) {
            $user->saveAllowance($request, $action, 0, $current); // <=======================
            $this->addRateLimitHeaders($response, $limit, 0, $window);
            throw new TooManyRequestsHttpException($this->errorMessage);
        }
        $user->saveAllowance($request, $action, $allowance - 1, $current);
        $this->addRateLimitHeaders($response, $limit, $allowance - 1, (int) (($limit - $allowance + 1) * $window / $limit));
    }
В таком варианте, если выставить лимиты [10, 600] (10 запросов на 10 минут), и отправлять запрос к API каждые 59 секунд, то после первых 10и запросов ВСЕ последующие (а не только 11й, 13й, ...) будут с 429 кодом из-за перезаписи timestamp.
Верно ли такое поведение или лучше оставлять timestamp последнего удачного запроса?
Аватара пользователя
Seagull
Сообщения: 31
Зарегистрирован: 2019.01.24, 20:00

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение Seagull »

Получил ответ.
Но если правильно понял концепцию leaky bucket, то все равно алгоритм должен быть другим.(?)

Пример:

Код: Выделить всё

public function getRateLimit($request, $action)
{
    return [1, 60];
}
Изображение
  1. Когда трафик не превышает лимиты (как сейчас/как должно быть)
  2. Избыточный трафик. Если запросы идут чаще, чем полноценное восполнение одного $allowance, то восполнение не произойдет вовсе. (как сейчас)
  3. Избыточный трафик. $allowance восполняется с течением времени. При этом фильтруя избыточный трафик (как должно быть?)
Решение?:
  • Сохранять timestamp последнего удачного запроса

    Код: Выделить всё

        public function checkRateLimit($user, $request, $response, $action)
        {
            //...
            list($allowance, $timestamp) = $user->loadAllowance($request, $action);
            //...
            $allowance += (int) (($current - $timestamp) * $limit / $window);
            //...
            if ($allowance < 1) {
                $user->saveAllowance($request, $action, 0, $timestamp); // <================
                $this->addRateLimitHeaders($response, $limit, 0, $window);
                throw new TooManyRequestsHttpException($this->errorMessage);
            }
            //...
        }
    
  • Сохранять $allowance с дробным значением

    Код: Выделить всё

        public function checkRateLimit($user, $request, $response, $action)
        {
            //...
            $current = time();
            
            $allowance += (float) (($current - $timestamp) * $limit / $window);// ? round(, 2); <===============
            //...
            if ($allowance < 1) {
                $user->saveAllowance($request, $action, max(0, $allowance), $current); // <================
                $this->addRateLimitHeaders($response, $limit, 0, $window);
                throw new TooManyRequestsHttpException($this->errorMessage);
            }
            //...
        }
    
Последний раз редактировалось Seagull 2019.04.04, 15:49, всего редактировалось 2 раза.
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение samdark »

$allowance и восполняется, просто лимит считается для окна, окно двигается.
Аватара пользователя
Seagull
Сообщения: 31
Зарегистрирован: 2019.01.24, 20:00

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение Seagull »

samdark писал(а): 2019.04.03, 18:49 $allowance и восполняется, просто лимит считается для окна, окно двигается.
$allowance обрезается из-за int'а ($allowance = 0.8 => 0)

По итогу получается второй вариант с иллюстрации, когда RateLimit перестает вообще пропускать запросы?!
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение samdark »

Да, RateLimit перестаёт пропускать запросы пока в окне есть количество запросов, большее чем allowance.
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение samdark »

Это позволяет избежать пиков в количестве запросов. Если бы это считалось фиксированным окном, на его правой границе и левой границе следующего возникал бы пик из $allowance запросов.
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение samdark »

В принципе, да. Если allowance наращивать при этом дробными значениями, то распределение будет более плавным при небольших значениях allowance.
Аватара пользователя
Seagull
Сообщения: 31
Зарегистрирован: 2019.01.24, 20:00

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение Seagull »

samdark писал(а): 2019.04.04, 00:25 Это позволяет избежать пиков в количестве запросов. Если бы это считалось фиксированным окном, на его правой границе и левой границе следующего возникал бы пик из $allowance запросов.
Про это речи и не шло. Окно должно сдвигаться. И в моем исполнении, с обновлением timestamp'a, только удачных запросов, пиков на стыке также не будет.
samdark писал(а): 2019.04.04, 00:28 В принципе, да. Если allowance наращивать при этом дробными значениями, то распределение будет более плавным при небольших значениях allowance.
По большой части в этом и проблема, если клиент неправильно настроит интервалы запросов к API, то он в конечном счете перестанет вообще получать ответы отличные от 429.
Рабочий пример с лимитом в 50 запросов на 24 часа

С учетом того, что замена int на float разрушит уже существующие реализации, то изначально я подумал о сдвиге окна только во время удачного запроса (когда $allowance > 1). Конечный результат эквивалентен с дробным значением $allowance (и он даже точнее будет).
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение samdark »

А нельзя просто окно и allowance сделать меньше?
Аватара пользователя
Seagull
Сообщения: 31
Зарегистрирован: 2019.01.24, 20:00

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение Seagull »

Вопрос состоял в том, что allowance при избыточном трафике не будет восполняться, т.к. хранится в int'е и все значения 0 <= allowance <= 1 обрезаются до 0.
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение samdark »

Да, этот момент можно переделать. Оформите в pull request?
Аватара пользователя
Seagull
Сообщения: 31
Зарегистрирован: 2019.01.24, 20:00

Re: RateLimiter saveAllowance при Too Many Requests

Сообщение Seagull »

samdark писал(а): 2019.04.04, 14:25 Да, этот момент можно переделать. Оформите в pull request?
Попробую (пока еще не приходилось :( ).

Выходит, что решение будет таким:
Seagull писал(а): 2019.04.03, 01:09
  • Сохранять timestamp последнего удачного запроса

    Код: Выделить всё

        public function checkRateLimit($user, $request, $response, $action)
        {
            //...
            list($allowance, $timestamp) = $user->loadAllowance($request, $action);
            //...
            $allowance += (int) (($current - $timestamp) * $limit / $window);
            //...
            if ($allowance < 1) {
                $user->saveAllowance($request, $action, 0, $timestamp); // <================
                $this->addRateLimitHeaders($response, $limit, 0, $window);
                throw new TooManyRequestsHttpException($this->errorMessage);
            }
            //...
        }
    
Ответить