Class: SimpleThrottle

Inherits:
Object
  • Object
show all
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
  local list_key = KEYS[1]
  local limit = tonumber(ARGV[1])
  local ttl = tonumber(ARGV[2])
  local now = ARGV[3]
  local push = tonumber(ARGV[4])

  local size = redis.call('llen', list_key)
  if size >= limit then
    local expired = tonumber(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 push > 0 and size < limit then
    redis.call('rpush', list_key, now)
    redis.call('pexpire', list_key, ttl)
  end

  return size
LUA

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, ttl:, limit:, redis: nil) ⇒ SimpleThrottle

Create a new throttle.

Parameters:

  • name (String)

    unique name for the throttle

  • ttl (Numeric)

    number of seconds that the throttle will remain active

  • limit (Integer)

    number of allowed requests within the throttle ttl

  • redis (Redis, Proc) (defaults to: nil)

    Redis instance to use or a Proc that yields a Redos instance



124
125
126
127
128
129
130
# File 'lib/simple_throttle.rb', line 124

def initialize(name, ttl:, limit:, redis: nil)
  @name = name.to_s
  @name = name.dup.freeze unless name.frozen?
  @limit = limit.to_i
  @ttl = ttl.to_f
  @redis = redis
end

Instance Attribute Details

#limitObject (readonly)

Returns the value of attribute limit.



116
117
118
# File 'lib/simple_throttle.rb', line 116

def limit
  @limit
end

#nameObject (readonly)

Returns the value of attribute name.



116
117
118
# File 'lib/simple_throttle.rb', line 116

def name
  @name
end

#ttlObject (readonly)

Returns the value of attribute ttl.



116
117
118
# File 'lib/simple_throttle.rb', line 116

def ttl
  @ttl
end

Class Method Details

.[](name) ⇒ SimpleThrottle

Returns a globally defined throttle with the specfied name.

Parameters:

  • name (String, Symbol)

    name of the throttle

Returns:



64
65
66
67
68
# File 'lib/simple_throttle.rb', line 64

def [](name)
  if defined?(@throttles) && @throttles
    @throttles[name.to_s]
  end
end

.add(name, ttl:, limit:, 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.

Parameters:

  • name (String)

    unique name for the throttle

  • ttl (Numeric)

    number of seconds that the throttle will remain active

  • limit (Integer)

    number of allowed requests within the throttle ttl

  • redis (Redis, Proc) (defaults to: nil)

    Redis instance to use or a Proc that yields a Redos instance



53
54
55
56
57
58
# File 'lib/simple_throttle.rb', line 53

def add(name, ttl:, limit:, redis: nil)
  @lock.synchronize do
    @throttles ||= {}
    @throttles[name.to_s] = new(name, limit: limit, ttl: ttl, redis: redis)
  end
end

.redisRedis

Return the Redis instance where the throttles are stored.

Returns:

  • (Redis)


86
87
88
89
90
91
92
93
# File 'lib/simple_throttle.rb', line 86

def redis
  @redis_client ||= Redis.new
  if @redis_client.is_a?(Proc)
    @redis_client.call
  else
    @redis_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)

Parameters:

  • client (Redis, Proc) (defaults to: nil)

Yield Returns:

  • (Redis)


79
80
81
# File 'lib/simple_throttle.rb', line 79

def set_redis(client = nil, &block)
  @redis_client = (client || block)
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.

Returns:

  • (Boolean)


136
137
138
139
# File 'lib/simple_throttle.rb', line 136

def allowed!
  size = current_size(true)
  size < limit
end

#peekInteger

Peek at the current number for throttled calls being tracked.

Returns:

  • (Integer)


151
152
153
# File 'lib/simple_throttle.rb', line 151

def peek
  current_size(false)
end

#reset!void

This method returns an undefined value.

Reset a throttle back to zero.



144
145
146
# File 'lib/simple_throttle.rb', line 144

def reset!
  redis_client.del(redis_key)
end

#wait_timeFloat

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.

Returns:

  • (Float)


160
161
162
163
164
165
166
167
168
169
# File 'lib/simple_throttle.rb', line 160

def wait_time
  if peek < limit
    0.0
  else
    first = redis_client.lindex(redis_key, 0).to_f / 1000.0
    delta = Time.now.to_f - first
    delta = 0.0 if delta < 0
    ttl - delta
  end
end