Aquí el algoritmo más simple , si solo desea soltar los mensajes cuando llegan demasiado rápido (en lugar de ponerlos en cola, lo que tiene sentido porque la cola podría ser arbitrariamente 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;
No hay estructuras de datos, temporizadores, etc. en esta solución y funciona de manera limpia :) Para ver esto, la 'asignación' crece a una velocidad de 5/8 unidades por segundo como máximo, es decir, como máximo cinco unidades por ocho segundos. Cada mensaje que se reenvía deduce una unidad, por lo que no puede enviar más de cinco mensajes por cada ocho segundos.
Tenga en cuenta que rate
debe ser un número entero, es decir, sin una parte decimal distinta de cero, o el algoritmo no funcionará correctamente (la tasa real no será rate/per
). Por ejemplo rate=0.5; per=1.0;
, no funciona porque allowance
nunca crecerá a 1.0. Pero rate=1.0; per=2.0;
funciona bien.
allowance
. El tamaño del cubo esrate
. Laallowance += …
línea es una optimización de agregar un token a cada velocidad ÷ por segundo.Use este decorador @RateLimited (ratepersec) antes de su función que pone en cola.
Básicamente, esto verifica si han transcurrido 1 / tasa de segundos desde la última vez y si no, espera el resto del tiempo, de lo contrario no espera. Esto efectivamente lo limita a la tasa / seg. El decorador se puede aplicar a cualquier función que desee con velocidad limitada.
En su caso, si desea un máximo de 5 mensajes por 8 segundos, use @RateLimited (0.625) antes de su función sendToQueue.
fuente
time.clock()
no tiene suficiente resolución en mi sistema, así quetime.time()
time.clock()
, que mide el tiempo transcurrido de la CPU. El tiempo de CPU puede correr mucho más rápido o mucho más lento que el tiempo "real". En sutime.time()
lugar, desea usar , que mide el tiempo de la pared (tiempo "real").Un Token Bucket es bastante simple de implementar.
Comience con un cubo con 5 fichas.
Cada 5/8 segundos: si el cubo tiene menos de 5 tokens, agrega uno.
Cada vez que desee enviar un mensaje: si el depósito tiene ≥1 ficha, saque una ficha y envíe el mensaje. De lo contrario, espere / suelte el mensaje / lo que sea.
(obviamente, en el código real, usaría un contador entero en lugar de tokens reales y puede optimizar cada paso de 5/8 almacenando marcas de tiempo)
Leyendo la pregunta nuevamente, si el límite de velocidad se restablece completamente cada 8 segundos, entonces aquí hay una modificación:
Comience con una marca de tiempo,
last_send
hace mucho tiempo (por ejemplo, en la época). Además, comience con el mismo cubo de 5 fichas.Aplica la regla cada 5/8 segundos.
Cada vez que envía un mensaje: Primero, verifique si hace
last_send
≥ 8 segundos. Si es así, llene el cubo (configúrelo en 5 fichas). En segundo lugar, si hay tokens en el depósito, envíe el mensaje (de lo contrario, soltar / esperar / etc.). Tercero, listolast_send
para ahora.Eso debería funcionar para ese escenario.
De hecho, he escrito un bot IRC usando una estrategia como esta (el primer enfoque). Está en Perl, no en Python, pero aquí hay un código para ilustrar:
La primera parte aquí maneja agregar tokens al cubo. Puede ver la optimización de agregar tokens en función del tiempo (de la segunda a la última línea) y luego la última línea sujeta el contenido del depósito al máximo (MESSAGE_BURST)
$ conn es una estructura de datos que se pasa. Esto está dentro de un método que se ejecuta de manera rutinaria (calcula cuándo será la próxima vez que tenga algo que hacer y duerme tanto tiempo o hasta que obtenga tráfico de red). La siguiente parte del método maneja el envío. Es bastante complicado, porque los mensajes tienen prioridades asociadas con ellos.
Esa es la primera cola, que se ejecuta sin importar qué. Incluso si se mata nuestra conexión por inundación. Se usa para cosas extremadamente importantes, como responder al PING del servidor. A continuación, el resto de las colas:
Finalmente, el estado del depósito se guarda nuevamente en la estructura de datos $ conn (en realidad un poco más adelante en el método; primero calcula qué tan pronto tendrá más trabajo)
Como puede ver, el código real de manejo de la cubeta es muy pequeño, aproximadamente cuatro líneas. El resto del código es manejo prioritario de colas. El bot tiene colas prioritarias para que, por ejemplo, alguien que chatea con él no pueda evitar que realice sus importantes tareas de patada / prohibición.
fuente
para bloquear el procesamiento hasta que se pueda enviar el mensaje, haciendo cola más mensajes, la hermosa solución de antti también se puede modificar de esta manera:
solo espera hasta que haya suficiente margen para enviar el mensaje. para no comenzar con dos veces la tasa, la asignación también puede inicializarse con 0.
fuente
(1-allowance) * (per/rate)
, debes agregar esa misma cantidadlast_check
.Mantenga la hora en que se enviaron las últimas cinco líneas. Mantenga los mensajes en cola hasta que el quinto mensaje más reciente (si existe) haya pasado al menos 8 segundos (con last_five como un conjunto de veces):
fuente
Una solución es adjuntar una marca de tiempo a cada elemento de la cola y descartar el elemento después de que hayan pasado 8 segundos. Puede realizar esta verificación cada vez que se agrega la cola.
Esto solo funciona si limita el tamaño de la cola a 5 y descarta cualquier adición mientras la cola está llena.
fuente
Si alguien todavía está interesado, utilizo esta simple clase invocable junto con un almacenamiento de valor de clave LRU cronometrado para limitar la tasa de solicitud por IP. Utiliza una deque, pero puede reescribirse para usarse con una lista en su lugar.
fuente
Solo una implementación en Python de un código de respuesta aceptada.
fuente
Qué tal esto:
fuente
Necesitaba una variación en Scala. Aquí está:
Así es como se puede usar:
fuente