Class: ThrottledObject::Lock

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Lock

Returns a new instance of Lock.

Raises:

  • (ArgumentError)


25
26
27
28
29
30
31
32
33
34
# File 'lib/throttled_object/lock.rb', line 25

def initialize(options = {})
  @identifier = options[:identifier]
  @amount     = options[:amount]
  @redis      = options[:redis] || Redis.current
  _period     = options[: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

#amountObject (readonly)

Returns the value of attribute amount.



23
24
25
# File 'lib/throttled_object/lock.rb', line 23

def amount
  @amount
end

#identifierObject (readonly)

Returns the value of attribute identifier.



23
24
25
# File 'lib/throttled_object/lock.rb', line 23

def identifier
  @identifier
end

#periodObject (readonly)

Returns the value of attribute period.



23
24
25
# File 'lib/throttled_object/lock.rb', line 23

def period
  @period
end

#redisObject (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:

  1. That we may want to block until it’s available (the default)

  2. 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