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.1"

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
# 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

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)


32
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
# File 'lib/ruby_rolling_rate_limiter.rb', line 32

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
  @redis_connection.lock("#{key}-lock") do |lock|
    @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
  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



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

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