Class: RubyRollingRateLimiter

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_rolling_rate_limiter.rb,
lib/ruby_rolling_rate_limiter/errors.rb,
lib/ruby_rolling_rate_limiter/version.rb

Defined Under Namespace

Modules: Errors

Constant Summary collapse

VERSION =
"0.1.3"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(limiter_identifier, interval_in_seconds, max_calls_per_interval, min_distance_between_calls_in_seconds = 1, redis_connection = $redis) ⇒ RubyRollingRateLimiter

Returns a new instance of RubyRollingRateLimiter.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/ruby_rolling_rate_limiter.rb', line 11

def initialize(limiter_identifier, interval_in_seconds, max_calls_per_interval, min_distance_between_calls_in_seconds = 1, redis_connection = $redis)
  @limiter_identifier = limiter_identifier
  @interval_in_seconds = interval_in_seconds
  @max_calls_per_interval = max_calls_per_interval
  @min_distance_between_calls_in_seconds = min_distance_between_calls_in_seconds
  @redis_connection = redis_connection
  #Check to ensure args are good.
  validate_arguments

  # Check Redis is there
  check_redis_is_available
  # Setup the Lock Manager
  @lock_manager ||= Redlock::Client.new([redis_connection])

end

Instance Attribute Details

#current_errorObject (readonly)

Your code goes here…



9
10
11
# File 'lib/ruby_rolling_rate_limiter.rb', line 9

def current_error
  @current_error
end

Instance Method Details

#can_call_proceed?(call_size = 1) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/ruby_rolling_rate_limiter.rb', line 33

def can_call_proceed?(call_size = 1)
  if call_size > @max_calls_per_interval
    @current_error = {code: 0, result: false, error: "Call size too big. Max calls in rolling window is: #{@max_calls_per_interval}. Increase your max_calls_per_interval or decrease your call_size", retry_in: 0}
    return false
  end
  results = false
  now = DateTime.now.strftime('%s%6N').to_i # Time since EPOC in microseconds.
  interval = @interval_in_seconds * 1000 * 1000 # Inteval in microseconds

  key = "#{self.class.name}-#{@limiter_identifier}-#{@id}"
  
  clear_before = now - interval
  # Begin multi redis
  max_retry_counter = 0
  begin
    if max_retry_counter <= 100
      @lock_manager.lock("#{key}-lock", 10000) do |locked|
        if locked
          @redis_connection.multi
          @redis_connection.zremrangebyscore(key, 0, clear_before.to_s)
          @redis_connection.zrange(key, 0, -1)
          cur = @redis_connection.exec

          if (cur[1].count <= @max_calls_per_interval) && ((cur[1].count+call_size) <= @max_calls_per_interval) && ((@min_distance_between_calls_in_seconds * 1000 * 1000) && (now - cur[1].last.to_i) > (@min_distance_between_calls_in_seconds * 1000 * 1000))
            @redis_connection.multi
            @redis_connection.zrange(key, 0, -1)
            call_size.times do 
              @redis_connection.zadd(key, now.to_s, now.to_s)
            end
            @redis_connection.expire(key, @interval_in_seconds)
            results = @redis_connection.exec
          else
            results = [cur[1]]
          end
        else
          raise Errors::LockWaiting, "Could not aquire lock"
        end
      end
    else
      raise Errors::MaxRetryReachedOnLockAcquire, "Unable to acquire lock for rate limit after 100 attempts"
    end
  rescue Errors::LockWaiting
    sleep 0.2
    max_retry_counter +=1 
    retry
  end

  if results
    call_set = results[0]
    too_many_in_interval = call_set.count >= @max_calls_per_interval
    time_since_last_request = (@min_distance_between_calls_in_seconds * 1000 * 1000) && (now - call_set.last.to_i)

    if too_many_in_interval
      @current_error = {code: 1, result: false, error: "Too many requests", retry_in: (call_set.first.to_i - now + interval) / 1000 / 1000, retry_in_micro: (call_set.first.to_i - now + interval)}
      return false
    elsif (call_set.count+call_size) > @max_calls_per_interval
      @current_error = {code: 2, result: false, error: "Call Size too big for available access, trying to make #{call_size} with only #{call_set.count} calls available in window", retry_in: (call_set.first.to_i - now + interval) / 1000 / 1000, retry_in_micro: (call_set.first.to_i - now + interval)}
      return false
    elsif time_since_last_request < (@min_distance_between_calls_in_seconds * 1000 * 1000)
      @current_error = {code: 3, result: false, error: "Attempting to thrash faster than the minimal distance between calls", retry_in: @min_distance_between_calls_in_seconds, retry_in_micro: (@min_distance_between_calls_in_seconds * 1000 * 1000)}
      return false
    end
    return true
  end
  return false
end

#set_call_identifier(id) ⇒ Object



28
29
30
31
# File 'lib/ruby_rolling_rate_limiter.rb', line 28

def set_call_identifier(id)
  raise Errors::ArgumentInvalid, "The id must be a string or number with length greater than zero" unless id.length > 0
  @id = id
end