Class: SimpleThrottle

Inherits:
Object
  • Object
show all
Defined in:
lib/simple_throttle.rb

Overview

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.

"local list_key = KEYS[1]\nlocal limit = tonumber(ARGV[1])\nlocal ttl = tonumber(ARGV[2])\nlocal now = ARGV[3]\nlocal push = tonumber(ARGV[4])\n\nlocal size = redis.call('llen', list_key)\nif size >= limit then\n  local expired = tonumber(now) - ttl\n  while size > 0 do\n    local t = redis.call('lpop', list_key)\n    if tonumber(t) > expired then\n      redis.call('lpush', list_key, t)\n      break\n    end\n    size = size - 1\n  end\nend\n\nif push > 0 and size < limit then\n  redis.call('rpush', list_key, now)\n  redis.call('pexpire', list_key, ttl)\nend\n\nreturn size\n"

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



101
102
103
104
105
106
107
# File 'lib/simple_throttle.rb', line 101

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.



94
95
96
# File 'lib/simple_throttle.rb', line 94

def limit
  @limit
end

#nameObject (readonly)

Returns the value of attribute name.



94
95
96
# File 'lib/simple_throttle.rb', line 94

def name
  @name
end

#ttlObject (readonly)

Returns the value of attribute ttl.



94
95
96
# File 'lib/simple_throttle.rb', line 94

def ttl
  @ttl
end

Class Method Details

.[](name) ⇒ Object

Returns a globally defined throttle with the specfied name.



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

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

.add(name, limit:, ttl:, redis: nil) ⇒ Object

Add a global throttle that can be referenced later with the [] method.



44
45
46
47
48
49
# File 'lib/simple_throttle.rb', line 44

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

.redisObject

Return the Redis instance where the throttles are stored.



68
69
70
71
72
73
74
75
# File 'lib/simple_throttle.rb', line 68

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) ⇒ Object

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)



63
64
65
# File 'lib/simple_throttle.rb', line 63

def set_redis(client = nil, &block)
  @redis_client = (client || block)
end

Instance Method Details

#allowed!Object

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.



111
112
113
114
# File 'lib/simple_throttle.rb', line 111

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

#peekObject

Peek at the current number for throttled calls being tracked.



122
123
124
# File 'lib/simple_throttle.rb', line 122

def peek
  current_size(false)
end

#reset!Object

Reset a throttle back to zero.



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

def reset!
  redis_client.del(redis_key)
end

#wait_timeObject

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.



129
130
131
132
133
134
135
136
137
138
# File 'lib/simple_throttle.rb', line 129

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
    delta
  end
end