Class: ThrottledObject::Lock
- Inherits:
-
Object
- Object
- ThrottledObject::Lock
- Defined in:
- lib/throttled_object/lock.rb
Defined Under Namespace
Classes: Error, Unavailable, WaitForLock
Constant Summary collapse
- KEY_PREFIX =
"throttled_object:key:"
Instance Attribute Summary collapse
-
#amount ⇒ Object
readonly
Returns the value of attribute amount.
-
#identifier ⇒ Object
readonly
Returns the value of attribute identifier.
-
#period ⇒ Object
readonly
Returns the value of attribute period.
-
#redis ⇒ Object
readonly
Returns the value of attribute redis.
Instance Method Summary collapse
-
#initialize(options = {}) ⇒ Lock
constructor
A new instance of Lock.
-
#lock(max_time = nil) ⇒ Object
The general locking algorithm is pretty simple.
- #lock!(*args) ⇒ Object
- #synchronize(*args, &blk) ⇒ Object
- #synchronize!(*args, &blk) ⇒ Object
- #wait_for_lock(*args) ⇒ Object
Constructor Details
#initialize(options = {}) ⇒ Lock
Returns a new instance of Lock.
25 26 27 28 29 30 31 32 33 34 |
# File 'lib/throttled_object/lock.rb', line 25 def initialize( = {}) @identifier = [:identifier] @amount = [:amount] @redis = [:redis] || Redis.current _period = [:period] raise ArgumentError.new("You must provide an :identifier as a string") unless identifier.is_a?(String) raise ArgumentError.new("You must provide a valid amount of > hits per period") unless amount.is_a?(Numeric) && amount > 0 raise ArgumentError.new("You must provide a valid period of > 0 seconds") unless _period.is_a?(Numeric) && _period > 0 @period = (_period.to_f * 1000).ceil end |
Instance Attribute Details
#amount ⇒ Object (readonly)
Returns the value of attribute amount.
23 24 25 |
# File 'lib/throttled_object/lock.rb', line 23 def amount @amount end |
#identifier ⇒ Object (readonly)
Returns the value of attribute identifier.
23 24 25 |
# File 'lib/throttled_object/lock.rb', line 23 def identifier @identifier end |
#period ⇒ Object (readonly)
Returns the value of attribute period.
23 24 25 |
# File 'lib/throttled_object/lock.rb', line 23 def period @period end |
#redis ⇒ Object (readonly)
Returns the value of attribute redis.
23 24 25 |
# File 'lib/throttled_object/lock.rb', line 23 def redis @redis end |
Instance Method Details
#lock(max_time = nil) ⇒ Object
The general locking algorithm is pretty simple. It takes into account two things:
-
That we may want to block until it’s available (the default)
-
Occassionally, we need to abort after a short period.
So, the lock method operates in two methods. The first, and default, we will basically loop and attempt to aggressively obtain the lock. We loop until we’ve obtained a lock - To obtain the lock, we increment the current periods counter and check if it’s <= the max count. If it is, we have a lock. If not, we sleep until the lock should be ‘fresh’ again.
If we’re the first one to obtain a lock, we update some book keeping data.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/throttled_object/lock.rb', line 47 def lock(max_time = nil) raise 'lock must be called with a block' unless block_given? started_at = current_period has_lock = false until has_lock now = current_period if max_time && (now - started_at) >= max_time raise Unavailable.new("Unable to obtain a lock after #{now - started_at}ms") end lockable_time = rounded_period now current_key = KEY_PREFIX + lockable_time.to_s count = redis.incr current_key if count <= amount has_lock = true # Now we have a lock, we need to actually set the expiration of # the key. Note, we only ever set this on the first set to avoid # setting @amount times... if count == 1 # Expire after 3 periods. This means we only # ever keep a small number in memory. expires_after = ((period * 3).to_f / 1000).ceil redis.expire current_key, expires_after redis.setex "#{current_key}:obtained_at", now, expires_after end else obtained_at = [redis.get("#{current_key}:obtained_at").to_i, lockable_time].max next_period = (lockable_time + period) wait_for = (next_period - current_period).to_f / 1000 yield wait_for end end end |
#lock!(*args) ⇒ Object
84 85 86 87 88 |
# File 'lib/throttled_object/lock.rb', line 84 def lock!(*args) lock(*args) do |time| raise WaitForLock.new(time, "Lock unavailable, please wait #{time} seconds and attempt again.") end end |
#synchronize(*args, &blk) ⇒ Object
90 91 92 93 |
# File 'lib/throttled_object/lock.rb', line 90 def synchronize(*args, &blk) wait_for_lock *args yield if block_given? end |
#synchronize!(*args, &blk) ⇒ Object
95 96 97 98 |
# File 'lib/throttled_object/lock.rb', line 95 def synchronize!(*args, &blk) lock! *args yield if block_given? end |
#wait_for_lock(*args) ⇒ Object
80 81 82 |
# File 'lib/throttled_object/lock.rb', line 80 def wait_for_lock(*args) lock(*args) { |time| sleep time } end |