You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ratelimit_check.lua 2.8KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. -- This Lua script is a rate limiter for Redis using the token bucket algorithm.
  2. -- The script checks if a message should be rate-limited and updates the bucket status accordingly.
  3. -- Input keys:
  4. -- KEYS[1]: A prefix for the Redis keys, e.g., RL_<triplet>_<seconds>
  5. -- KEYS[2]: The current time in milliseconds
  6. -- KEYS[3]: The bucket leak rate (messages per millisecond)
  7. -- KEYS[4]: The maximum allowed burst
  8. -- KEYS[5]: The expiration time for a bucket
  9. -- KEYS[6]: The number of recipients for the message
  10. -- Redis keys used:
  11. -- l: Last hit (time in milliseconds)
  12. -- b: Current burst (number of tokens in the bucket)
  13. -- p: Pending messages (number of messages in processing)
  14. -- dr: Current dynamic rate multiplier (*10000)
  15. -- db: Current dynamic burst multiplier (*10000)
  16. -- Returns:
  17. -- An array containing:
  18. -- 1. if the message should be rate-limited or 0 if not
  19. -- 2. The current burst value after processing the message
  20. -- 3. The dynamic rate multiplier
  21. -- 4. The dynamic burst multiplier
  22. -- 5. The number of tokens leaked during processing
  23. local last = redis.call('HGET', KEYS[1], 'l')
  24. local now = tonumber(KEYS[2])
  25. local nrcpt = tonumber(KEYS[6])
  26. local leak_rate = tonumber(KEYS[3])
  27. local max_burst = tonumber(KEYS[4])
  28. local prefix = KEYS[1]
  29. local dynr, dynb, leaked = 0, 0, 0
  30. if not last then
  31. -- New bucket
  32. redis.call('HMSET', prefix, 'l', tostring(now), 'b', '0', 'dr', '10000', 'db', '10000', 'p', tostring(nrcpt))
  33. redis.call('EXPIRE', prefix, KEYS[5])
  34. return { 0, '0', '1', '1', '0' }
  35. end
  36. last = tonumber(last)
  37. local burst, pending = unpack(redis.call('HMGET', prefix, 'b', 'p'))
  38. burst, pending = tonumber(burst or '0'), tonumber(pending or '0')
  39. -- Sanity to avoid races
  40. if burst < 0 then
  41. burst = 0
  42. end
  43. if pending < 0 then
  44. pending = 0
  45. end
  46. pending = pending + nrcpt -- this message
  47. -- Perform leak
  48. if burst + pending > 0 then
  49. -- If we have any time passed
  50. if burst > 0 and last < now then
  51. dynr = tonumber(redis.call('HGET', prefix, 'dr')) / 10000.0
  52. if dynr == 0 then
  53. dynr = 0.0001
  54. end
  55. leak_rate = leak_rate * dynr
  56. leaked = ((now - last) * leak_rate)
  57. if leaked > burst then
  58. leaked = burst
  59. end
  60. burst = burst - leaked
  61. redis.call('HINCRBYFLOAT', prefix, 'b', -(leaked))
  62. redis.call('HSET', prefix, 'l', tostring(now))
  63. end
  64. dynb = tonumber(redis.call('HGET', prefix, 'db')) / 10000.0
  65. if dynb == 0 then
  66. dynb = 0.0001
  67. end
  68. burst = burst + pending
  69. if burst > 0 and burst > max_burst * dynb then
  70. return { 1, tostring(burst - pending), tostring(dynr), tostring(dynb), tostring(leaked) }
  71. end
  72. -- Increase pending if we allow ratelimit
  73. redis.call('HINCRBY', prefix, 'p', nrcpt)
  74. else
  75. burst = 0
  76. redis.call('HMSET', prefix, 'b', '0', 'p', tostring(nrcpt))
  77. end
  78. return { 0, tostring(burst), tostring(dynr), tostring(dynb), tostring(leaked) }