Je pourrais utiliser un pseudo-code, ou mieux, Python. J'essaie d'implémenter une file d'attente de limitation de débit pour un Python IRC bot), et cela fonctionne partiellement, mais si quelqu'un déclenche moins de messages que la limite (par exemple , la limite de débit est de 5 messages toutes les 8 secondes, et la personne ne déclenche que 4), et le prochain déclencheur dure plus de 8 secondes (par exemple, 16 secondes plus tard), le bot envoie le message, mais la file d'attente est pleine et attend 8 secondes, même si ce n’est pas nécessaire puisque la période de 8 secondes s’est écoulée.
Ici, le algorithme le plus simple , si vous souhaitez simplement supprimer les messages quand ils arrivent trop rapidement (au lieu de les mettre en file d'attente, ce qui est logique car la file d'attente peut devenir arbitrairement grande):
rate = 5.0; // unit: messages
per = 8.0; // unit: seconds
allowance = rate; // unit: messages
last_check = now(); // floating-point, e.g. usec accuracy. Unit: seconds
when (message_received):
current = now();
time_passed = current - last_check;
last_check = current;
allowance += time_passed * (rate / per);
if (allowance > rate):
allowance = rate; // throttle
if (allowance < 1.0):
discard_message();
else:
forward_message();
allowance -= 1.0;
Il n'y a pas d'infrastructures de données, de minuteries, etc. dans cette solution et cela fonctionne proprement :) Pour voir cela, la "tolérance" augmente à une vitesse maximale de 5/8 unités par seconde, c'est-à-dire au maximum cinq unités toutes les huit secondes. Chaque message transféré déduit une unité, vous ne pouvez donc pas envoyer plus de cinq messages toutes les huit secondes.
Notez que rate
doit être un entier, c’est-à-dire sans partie décimale non nulle, sinon l’algorithme ne fonctionnera pas correctement (le taux réel ne sera pas rate/per
). Par exemple. rate=0.5; per=1.0;
_ ne fonctionne pas car allowance
ne deviendra jamais 1.0. Mais rate=1.0; per=2.0;
fonctionne bien.
Utilisez ce décorateur @RateLimited (ratepersec) avant votre fonction qui met en file d'attente.
En gros, cela vérifie si 1/rate secondes se sont écoulés depuis la dernière fois et si ce n’est pas le cas, attend le reste du temps, sinon il n’attendra pas. Cela vous limite effectivement à taux/sec. Le décorateur peut être appliqué à toute fonction de votre choix.
Dans votre cas, si vous souhaitez un maximum de 5 messages par 8 secondes, utilisez @RateLimited (0.625) avant votre fonction sendToQueue.
import time
def RateLimited(maxPerSecond):
minInterval = 1.0 / float(maxPerSecond)
def decorate(func):
lastTimeCalled = [0.0]
def rateLimitedFunction(*args,**kargs):
elapsed = time.clock() - lastTimeCalled[0]
leftToWait = minInterval - elapsed
if leftToWait>0:
time.sleep(leftToWait)
ret = func(*args,**kargs)
lastTimeCalled[0] = time.clock()
return ret
return rateLimitedFunction
return decorate
@RateLimited(2) # 2 per second at most
def PrintNumber(num):
print num
if __== "__main__":
print "This should print 1,2,3... at about 2 per second."
for i in range(1,100):
PrintNumber(i)
Un seau à jetons est assez simple à mettre en œuvre.
Commencez avec un seau avec 5 jetons.
Toutes les 5/8 secondes: si le compartiment contient moins de 5 jetons, ajoutez-en un.
Chaque fois que vous souhaitez envoyer un message: Si le compartiment contient ≥ 1 jeton, enlevez un jeton et envoyez le message. Sinon, attendez/déposez le message/peu importe.
(Évidemment, dans le code réel, vous utiliseriez un compteur entier au lieu de vrais jetons et vous pouvez optimiser toutes les étapes tous les 5/8s en stockant des horodatages)
En relisant la question, si la limite de débit est entièrement réinitialisée toutes les 8 secondes, voici une modification:
Commencez avec un horodatage, last_send
, jadis (à l’époque, par exemple). De plus, commencez avec le même seau de 5 jetons.
Frappez la règle toutes les 5/8 secondes.
Chaque fois que vous envoyez un message: vérifiez d'abord si last_send
≥ 8 secondes auparavant. Si c'est le cas, remplissez le seau (réglez-le sur 5 jetons). Deuxièmement, s'il y a des jetons dans le compartiment, envoyez le message (sinon, déposez/attendez/etc.). Troisièmement, définissez last_send
jusqu'à maintenant.
Cela devrait fonctionner pour ce scénario.
J'ai en fait écrit un bot IRC) en utilisant une stratégie comme celle-ci (la première approche). Il est en Perl, pas en Python, mais voici du code pour illustrer:
La première partie traite ici de l’ajout de jetons au seau. Vous pouvez voir l'optimisation de l'ajout de jetons en fonction du temps (2e à la dernière ligne), puis la dernière ligne fixe le contenu du compartiment au maximum (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 est une structure de données qui est transmise. Cela fait partie d'une méthode qui s'exécute régulièrement (il calcule quand la prochaine fois qu'il aura quelque chose à faire et dort si longtemps ou jusqu'à ce qu'il reçoive du trafic réseau). La partie suivante de la méthode gère l’envoi. C'est assez compliqué, car les messages ont des priorités qui leur sont associées.
# 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] = [];
C'est la première file d'attente, qui est exécutée quoi qu'il arrive. Même si notre connexion est mise à mort pour cause d'inondation. Utilisé pour des tâches extrêmement importantes, telles que la réponse à la commande PING du serveur. Ensuite, le reste des files d'attente:
# 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}});
}
}
}
Enfin, le statut du compartiment est sauvegardé dans la structure de données $ conn (en fait, un peu plus tard dans la méthode; il calcule d’abord le temps qu’il aura plus de travail)
# Save status.
$conn->{fujiko_limit_bucket} = $bucket;
$conn->{fujiko_limit_lasttx} = $start_time;
Comme vous pouvez le constater, le code de manipulation du godet est très petit - environ quatre lignes. Le reste du code est la gestion de la file d'attente prioritaire. Le bot a des files d’attente prioritaires afin que, par exemple, une personne qui discute avec lui ne puisse l’empêcher de s’acquitter de ses tâches importantes de kick/ban.
pour bloquer le traitement jusqu'à ce que le message puisse être envoyé, mettant ainsi en file d'attente d'autres messages, la belle solution d'antti peut également être modifiée comme suit:
rate = 5.0; // unit: messages
per = 8.0; // unit: seconds
allowance = rate; // unit: messages
last_check = now(); // floating-point, e.g. usec accuracy. Unit: seconds
when (message_received):
current = now();
time_passed = current - last_check;
last_check = current;
allowance += time_passed * (rate / per);
if (allowance > rate):
allowance = rate; // throttle
if (allowance < 1.0):
time.sleep( (1-allowance) * (per/rate))
forward_message();
allowance = 0.0;
else:
forward_message();
allowance -= 1.0;
il attend juste que suffisamment d’allocation soit disponible pour envoyer le message. pour ne pas commencer avec deux fois le taux, l’allocation peut également être initialisée avec 0.
Une solution consiste à attacher un horodatage à chaque élément de la file d'attente et à le supprimer au bout de 8 secondes. Vous pouvez effectuer cette vérification à chaque fois que la file d'attente est ajoutée à.
Cela ne fonctionne que si vous limitez la taille de la file d'attente à 5 et supprimez tous les ajouts tant qu'elle est pleine.
Gardez l'heure à laquelle les cinq dernières lignes ont été envoyées. Tenez les messages en file d'attente jusqu'à ce que le cinquième message le plus récent (s'il existe) soit au moins 8 secondes dans le passé (avec last_five comme tableau de fois):
now = time.time()
if len(last_five) == 0 or (now - last_five[-1]) >= 8.0:
last_five.insert(0, now)
send_message(msg)
if len(last_five) > 5:
last_five.pop()
Juste une python implémentation d'un code de réponse acceptée.
import time
class Object(object):
pass
def get_throttler(rate, per):
scope = Object()
scope.allowance = rate
scope.last_check = time.time()
def throttler(fn):
current = time.time()
time_passed = current - scope.last_check;
scope.last_check = current;
scope.allowance = scope.allowance + time_passed * (rate / per)
if (scope.allowance > rate):
scope.allowance = rate
if (scope.allowance < 1):
pass
else:
fn()
scope.allowance = scope.allowance - 1
return throttler
Si quelqu'un est toujours intéressé, j'utilise cette classe appelable simple avec un stockage de valeur de clé LRU temporisé pour limiter le débit de requête par IP. Utilise un deque, mais peut être réécrit pour être utilisé avec une liste.
from collections import deque
import time
class RateLimiter:
def __init__(self, maxRate=5, timeUnit=1):
self.timeUnit = timeUnit
self.deque = deque(maxlen=maxRate)
def __call__(self):
if self.deque.maxlen == len(self.deque):
cTime = time.time()
if cTime - self.deque[0] > self.timeUnit:
self.deque.append(cTime)
return False
else:
return True
self.deque.append(time.time())
return False
r = RateLimiter()
for i in range(0,100):
time.sleep(0.1)
print(i, "block" if r() else "pass")
J'avais besoin d'une variation de Scala. C'est ici:
case class Limiter[-A, +B](callsPerSecond: (Double, Double), f: A ⇒ B) extends (A ⇒ B) {
import Thread.sleep
private def now = System.currentTimeMillis / 1000.0
private val (calls, sec) = callsPerSecond
private var allowance = 1.0
private var last = now
def apply(a: A): B = {
synchronized {
val t = now
val delta_t = t - last
last = t
allowance += delta_t * (calls / sec)
if (allowance > calls)
allowance = calls
if (allowance < 1d) {
sleep(((1 - allowance) * (sec / calls) * 1000d).toLong)
}
allowance -= 1
}
f(a)
}
}
Voici comment cela peut être utilisé:
val f = Limiter((5d, 8d), {
_: Unit ⇒
println(System.currentTimeMillis)
})
while(true){f(())}
Que dis-tu de ça:
long check_time = System.currentTimeMillis();
int msgs_sent_count = 0;
private boolean isRateLimited(int msgs_per_sec) {
if (System.currentTimeMillis() - check_time > 1000) {
check_time = System.currentTimeMillis();
msgs_sent_count = 0;
}
if (msgs_sent_count > (msgs_per_sec - 1)) {
return true;
} else {
msgs_sent_count++;
}
return false;
}