Class: Restrainer
- Inherits:
-
Object
- Object
- Restrainer
- Defined in:
- lib/restrainer.rb
Overview
Redis backed throttling mechanism to ensure that only a limited number of processes can be executed at any one time.
Usage:
Restrainer.new(:foo, 10).throttle do
# Do something
end
If more than the specified number of processes as identified by the name argument is currently running, then the throttle block will raise an error.
Defined Under Namespace
Classes: ThrottledError
Constant Summary collapse
- ADD_PROCESS_SCRIPT =
<<-LUA redis.replicate_commands() -- Parse arguments local sorted_set = KEYS[1] local process_id = ARGV[1] local limit = tonumber(ARGV[2]) local ttl = tonumber(ARGV[3]) -- Use the Redis server clock so all clients share a consistent view of time. local time = redis.call('time') local now = tonumber(time[1]) + (tonumber(time[2]) / 1000000) -- Get count of current processes. If more than the max, check if any of the processes have timed out -- and try again. local process_count = redis.call('zcard', sorted_set) if process_count >= limit then local max_score = now - ttl local expired_keys = redis.call('zremrangebyscore', sorted_set, '-inf', max_score) if expired_keys > 0 then process_count = redis.call('zcard', sorted_set) end end -- Success so add to the list and set a global expiration so the list cleans up after itself. if process_count < limit then redis.call('zadd', sorted_set, now, process_id) redis.call('pexpire', sorted_set, math.ceil(ttl * 1000)) end -- Return the number of processes running before the process was added. return process_count LUA
- ADD_PROCESS_SCRIPT_SHA1 =
SHA1 digest of the script used to call it with EVALSHA. The script itself is only loaded to the server if it isn't already cached there.
Digest::SHA1.hexdigest(ADD_PROCESS_SCRIPT).freeze
Instance Attribute Summary collapse
-
#limit ⇒ Object
readonly
Returns the value of attribute limit.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Class Method Summary collapse
-
.redis(&block) ⇒ Redis
Either configure the redis instance using a block or yield the instance.
-
.redis=(conn) ⇒ void
Set the redis instance to a specific instance.
Instance Method Summary collapse
-
#clear! ⇒ void
Clear all locks.
-
#current ⇒ Integer
Get the number of processes currently being executed for this restrainer.
-
#initialize(name, limit:, timeout: 60, redis: nil) ⇒ Restrainer
constructor
Create a new restrainer.
-
#lock!(process_id = nil, limit: nil) ⇒ String?
Obtain a lock on one the allowed processes.
-
#release!(process_id) ⇒ Boolean
Release one of the allowed processes.
-
#throttle(limit: nil) ⇒ Object
Wrap a block with this method to throttle concurrent execution.
Constructor Details
#initialize(name, limit:, timeout: 60, redis: nil) ⇒ Restrainer
Create a new restrainer. The name is used to identify the Restrainer and group processes together. You can create any number of Restrainers with different names.
The required limit parameter specifies the maximum number of processes that will be allowed to execute the throttle block at any point in time.
The timeout parameter is used for cleaning up internal data structures so that jobs aren't orphaned if their process is killed. Processes will automatically be removed from the running jobs list after the specified number of seconds. Note that the Restrainer will not handle timing out any code itself. This value is just used to insure the integrity of internal data structures.
109 110 111 112 113 114 115 |
# File 'lib/restrainer.rb', line 109 def initialize(name, limit:, timeout: 60, redis: nil) @name = name @limit = limit @timeout = timeout @key = "#{self.class.name}.#{name}" @redis = redis end |
Instance Attribute Details
#limit ⇒ Object (readonly)
Returns the value of attribute limit.
18 19 20 |
# File 'lib/restrainer.rb', line 18 def limit @limit end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
18 19 20 |
# File 'lib/restrainer.rb', line 18 def name @name end |
Class Method Details
.redis(&block) ⇒ Redis
Either configure the redis instance using a block or yield the instance. Configuring with a block allows you to use things like connection pools etc. without hard coding a single instance.
73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/restrainer.rb', line 73 def redis(&block) if block @redis = block else @redis ||= begin client = Redis.new lambda { client } end @redis.call end end |
.redis=(conn) ⇒ void
This method returns an undefined value.
Set the redis instance to a specific instance. It is usually preferable to use the block form for configurating the instance so that it can be evaluated at runtime.
93 94 95 |
# File 'lib/restrainer.rb', line 93 def redis=(conn) @redis = lambda { conn } end |
Instance Method Details
#clear! ⇒ void
This method returns an undefined value.
Clear all locks.
183 184 185 |
# File 'lib/restrainer.rb', line 183 def clear! redis.del(key) end |
#current ⇒ Integer
Get the number of processes currently being executed for this restrainer.
176 177 178 |
# File 'lib/restrainer.rb', line 176 def current redis.zcard(key).to_i end |
#lock!(process_id = nil, limit: nil) ⇒ String?
Obtain a lock on one the allowed processes. The method returns a process identifier that must be passed to the release! to release the lock. You can pass in a unique identifier if you already have one.
150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/restrainer.rb', line 150 def lock!(process_id = nil, limit: nil) process_id ||= SecureRandom.uuid limit ||= self.limit # limit of less zero is no limit; limit of zero is allow none return nil if limit < 0 raise ThrottledError.new("#{self.class}: #{@name} is not allowing any processing") if limit == 0 add_process!(redis, process_id, limit) process_id end |
#release!(process_id) ⇒ Boolean
Release one of the allowed processes. You must pass in a process id returned by the lock method.
167 168 169 170 171 |
# File 'lib/restrainer.rb', line 167 def release!(process_id) return false if process_id.nil? remove_process!(redis, process_id) end |
#throttle(limit: nil) ⇒ Object
Wrap a block with this method to throttle concurrent execution. If more than the alotted number of processes (as identified by the name) are currently executing, then a Restrainer::ThrottledError will be raised.
125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/restrainer.rb', line 125 def throttle(limit: nil) limit ||= self.limit # limit of less zero is no limit; limit of zero is allow none return yield if limit < 0 process_id = lock!(limit: limit) begin yield ensure release!(process_id) end end |