Token Bucket довольно прост в реализации.
Начните с ведра с 5 жетонов.
Каждые 5/8 секунд: если в ведре меньше 5 жетонов, добавьте один.
Каждый раз, когда вы хотите отправить сообщение: если в корзине есть токен ≥1, выньте один токен и отправьте сообщение. В противном случае, подождите / отбросьте сообщение / что угодно.
(очевидно, в реальном коде вы использовали бы целочисленный счетчик вместо реальных токенов, и вы можете оптимизировать каждый шаг 5/8, сохраняя временные метки)
Повторное чтение вопроса, если ограничение скорости полностью сбрасывается каждые 8 секунд, то вот модификация:
Начните с отметки last_send
времени, давно, например, в эпоху. Кроме того, начните с того же ведра с 5 жетонами.
Ударьте правило каждые 5/8 секунд.
Каждый раз, когда вы отправляете сообщение: сначала проверьте, если last_send
≥ 8 секунд назад. Если так, заполните ведро (установите это к 5 жетонам). Во-вторых, если в корзине есть токены, отправьте сообщение (в противном случае отбросьте / подождите / и т.д.). В-третьих, установить last_send
сейчас.
Это должно работать для этого сценария.
Я на самом деле написал бот IRC, используя такую стратегию (первый подход). Это на Perl, а не на Python, но вот некоторый код для иллюстрации:
Первая часть здесь посвящена добавлению токенов в корзину. Вы можете увидеть оптимизацию добавления токенов на основе времени (от 2-й до последней строки), а затем последняя строка ограничивает содержимое сегмента до максимума (MESSAGE_BURST)
my $start_time = time;
...
# Bucket handling
my $bucket = $conn->{fujiko_limit_bucket};
my $lasttx = $conn->{fujiko_limit_lasttx};
$bucket += ($start_time-$lasttx)/MESSAGE_INTERVAL;
($bucket <= MESSAGE_BURST) or $bucket = MESSAGE_BURST;
$ conn - это структура данных, которая передается. Это внутри метода, который выполняется регулярно (он рассчитывает, когда в следующий раз ему будет что-то делать, и спит либо так долго, либо до тех пор, пока не получит сетевой трафик). Следующая часть метода обрабатывает отправку. Это довольно сложно, потому что сообщения имеют приоритеты, связанные с ними.
# Queue handling. Start with the ultimate queue.
my $queues = $conn->{fujiko_queues};
foreach my $entry (@{$queues->[PRIORITY_ULTIMATE]}) {
# Ultimate is special. We run ultimate no matter what. Even if
# it sends the bucket negative.
--$bucket;
$entry->{code}(@{$entry->{args}});
}
$queues->[PRIORITY_ULTIMATE] = [];
Это первая очередь, которая запускается несмотря ни на что. Даже если это приведет к тому, что наше соединение погибнет от наводнения. Используется для чрезвычайно важных вещей, например, для ответа на PING сервера. Далее остальные очереди:
# Continue to the other queues, in order of priority.
QRUN: for (my $pri = PRIORITY_HIGH; $pri >= PRIORITY_JUNK; --$pri) {
my $queue = $queues->[$pri];
while (scalar(@$queue)) {
if ($bucket < 1) {
# continue later.
$need_more_time = 1;
last QRUN;
} else {
--$bucket;
my $entry = shift @$queue;
$entry->{code}(@{$entry->{args}});
}
}
}
Наконец, статус корзины сохраняется обратно в структуру данных $ conn (на самом деле чуть позже в методе; сначала он вычисляет, как скоро у него будет больше работы)
# Save status.
$conn->{fujiko_limit_bucket} = $bucket;
$conn->{fujiko_limit_lasttx} = $start_time;
Как видите, реальный код обработки сегментов очень мал - около четырех строк. Остальная часть кода является приоритетной обработкой очереди. У бота есть приоритетные очереди, так что, например, кто-то, общаясь с ним, не может помешать ему выполнять свои важные обязанности по кик-бану.