Class: SimpleThrottle
- Inherits:
-
Object
- Object
- SimpleThrottle
- Defined in:
- lib/simple_throttle.rb
Overview
Create a simple throttle that can be used to limit the number of request for a resouce per time period. These objects are thread safe.
Constant Summary collapse
- LUA_SCRIPT =
Server side Lua script that maintains the throttle in redis. The throttle is stored as a list of timestamps in milliseconds. When the script is invoked it will scan the oldest entries removing any that should be expired from the list. If the list is below the specified limit then the current entry will be added. The list is marked to expire with the oldest entry so there's no need to cleanup the lists.
<<~LUA redis.replicate_commands() local list_key = KEYS[1] local limit = tonumber(ARGV[1]) local ttl = tonumber(ARGV[2]) local pause_to_recover = tonumber(ARGV[3]) local amount = tonumber(ARGV[4]) local cleanup = tonumber(ARGV[5]) -- Use the Redis server clock so timestamps are consistent and monotonic -- across all clients regardless of individual machine clock skew. local time = redis.call('time') local now = (tonumber(time[1]) * 1000) + math.floor(tonumber(time[2]) / 1000) local size = redis.call('llen', list_key) if size >= limit or (cleanup > 0 and size > 0) then local expired = now - ttl while size > 0 do local t = redis.call('lpop', list_key) if tonumber(t) > expired then redis.call('lpush', list_key, t) break end size = size - 1 end end if pause_to_recover > 0 then limit = limit + 1 end if size + amount > limit then amount = (limit - size) + 1 end if size < limit then for i = 1, amount do redis.call('rpush', list_key, now) end redis.call('pexpire', list_key, ttl) end return size + amount LUA
Instance Attribute Summary collapse
-
#limit ⇒ Object
readonly
Returns the value of attribute limit.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#ttl ⇒ Object
readonly
Returns the value of attribute ttl.
Class Method Summary collapse
-
.[](name) ⇒ SimpleThrottle
Returns a globally defined throttle with the specfied name.
-
.add(name, ttl:, limit:, pause_to_recover: false, redis: nil) ⇒ void
Add a global throttle that can be referenced later with the [] method.
-
.redis ⇒ Redis
Return the Redis instance where the throttles are stored.
-
.set_redis(client = nil, &block) ⇒ void
Set the Redis instance to use for maintaining the throttle.
Instance Method Summary collapse
-
#allowed! ⇒ Boolean
Returns true if the limit for the throttle has not been reached yet.
-
#increment!(amount = 1) ⇒ Integer
Increment the throttle by the specified and return the current size.
-
#initialize(name, ttl:, limit:, pause_to_recover: false, redis: nil) ⇒ SimpleThrottle
constructor
Create a new throttle.
-
#peek ⇒ Integer
Peek at the current number for throttled calls being tracked.
-
#reset! ⇒ void
Reset a throttle back to zero.
-
#wait_time ⇒ Float
Returns when the next resource call should be allowed.
Constructor Details
#initialize(name, ttl:, limit:, pause_to_recover: false, redis: nil) ⇒ SimpleThrottle
Create a new throttle.
156 157 158 159 160 161 162 |
# File 'lib/simple_throttle.rb', line 156 def initialize(name, ttl:, limit:, pause_to_recover: false, redis: nil) @name = name.to_s.dup.freeze @limit = limit.to_i @ttl = ttl.to_f @pause_to_recover = !!pause_to_recover @redis = redis end |
Instance Attribute Details
#limit ⇒ Object (readonly)
Returns the value of attribute limit.
145 146 147 |
# File 'lib/simple_throttle.rb', line 145 def limit @limit end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
145 146 147 |
# File 'lib/simple_throttle.rb', line 145 def name @name end |
#ttl ⇒ Object (readonly)
Returns the value of attribute ttl.
145 146 147 |
# File 'lib/simple_throttle.rb', line 145 def ttl @ttl end |
Class Method Details
.[](name) ⇒ SimpleThrottle
Returns a globally defined throttle with the specfied name.
91 92 93 94 95 |
# File 'lib/simple_throttle.rb', line 91 def [](name) # Assign then read for thread safety. throttles = @throttles throttles[name.to_s] if throttles end |
.add(name, ttl:, limit:, pause_to_recover: false, redis: nil) ⇒ void
This method returns an undefined value.
Add a global throttle that can be referenced later with the [] method. This can be used to configure global throttles that you want to setup once and then use in multiple places.
77 78 79 80 81 82 83 84 85 |
# File 'lib/simple_throttle.rb', line 77 def add(name, ttl:, limit:, pause_to_recover: false, redis: nil) @lock.synchronize do # Copy-on-write so that lock-free readers in `[]` always see a # fully-populated, immutable hash and never a partially rehashed one. throttles = @throttles.dup throttles[name.to_s] = new(name, limit: limit, ttl: ttl, pause_to_recover: pause_to_recover, redis: redis) @throttles = throttles.freeze end end |
.redis ⇒ Redis
Return the Redis instance where the throttles are stored.
113 114 115 116 117 118 119 120 121 |
# File 'lib/simple_throttle.rb', line 113 def redis @lock.synchronize { @redis_client ||= Redis.new } unless @redis_client client = @redis_client if client.is_a?(Proc) client.call else client end end |
.set_redis(client = nil, &block) ⇒ void
This method returns an undefined value.
Set the Redis instance to use for maintaining the throttle. This can either be set with a hard coded value or by the value yielded by a block. If the block form is used it will be invoked at runtime to get the instance. Use this method if your Redis instance isn't constant (for example if you're in a forking environment and re-initialize connections on fork)
106 107 108 |
# File 'lib/simple_throttle.rb', line 106 def set_redis(client = nil, &block) @redis_client = (client || block) # rubocop:disable Style/RedundantParentheses end |
Instance Method Details
#allowed! ⇒ Boolean
Returns true if the limit for the throttle has not been reached yet. This method will also track the throttled resource as having been invoked on each call.
168 169 170 171 |
# File 'lib/simple_throttle.rb', line 168 def allowed! size = add_request(1, false) size <= limit end |
#increment!(amount = 1) ⇒ Integer
Increment the throttle by the specified and return the current size. Because how the throttle is implemented in Redis, the return value will always max out at the throttle limit + 1 or, if the pause to recover option is set, limit + 2.
179 180 181 182 183 184 |
# File 'lib/simple_throttle.rb', line 179 def increment!(amount = 1) amount = amount.to_i raise ArgumentError, "amount must be a positive integer" if amount < 1 add_request(amount, true) end |
#peek ⇒ Integer
Peek at the current number for throttled calls being tracked.
196 197 198 199 200 |
# File 'lib/simple_throttle.rb', line 196 def peek , now = = ((now - ttl) * 1000).ceil .count { |t| t > } end |
#reset! ⇒ void
This method returns an undefined value.
Reset a throttle back to zero.
189 190 191 |
# File 'lib/simple_throttle.rb', line 189 def reset! redis_client.del(redis_key) end |
#wait_time ⇒ Float
Returns when the next resource call should be allowed. Note that this doesn't guarantee that calling allow! will return true if the wait time is zero since other processes or threads can claim the resource.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/simple_throttle.rb', line 207 def wait_time , now = = ((now - ttl) * 1000).ceil if .count { |t| t > } < limit 0.0 else # The entry that frees up a slot is the limit-th newest (index -limit), # not the head of the list, since the list can legitimately hold more # than `limit` entries (increment! and pause_to_recover both add extras). oldest = [-limit] return 0.0 if oldest.nil? first = oldest.to_f / 1000.0 wait = ttl - (now - first) wait.clamp(0.0, ttl) end end |