Class: Ratelimit

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

Constant Summary collapse

COUNT_LUA_SCRIPT =
<<-LUA.freeze
  local subject = KEYS[1]
  local oldest_bucket = tonumber(ARGV[1])
  local current_bucket = tonumber(ARGV[2])
  local count = 0

  for bucket = oldest_bucket + 1, current_bucket do
    local value = redis.call('HGET', subject, tostring(bucket))
    if value then
      count = count + tonumber(value)
    end
  end

  return count
LUA
MAINTENANCE_LUA_SCRIPT =
<<-LUA.freeze
  local subject = KEYS[1]
  local oldest_bucket = tonumber(ARGV[1])

  -- Delete expired keys
  local all_keys = redis.call('HKEYS', subject)
  for _, key in ipairs(all_keys) do
    local bucket_key = tonumber(key)
    if bucket_key < oldest_bucket then
      redis.call('HDEL', subject, tostring(bucket_key))
    end
  end
LUA
VERSION =
"1.1.0"

Instance Method Summary collapse

Constructor Details

#initialize(key, options = {}) ⇒ Ratelimit

Create a Ratelimit object.

Parameters:

  • key (String)

    A name to uniquely identify this rate limit. For example, ‘emails’

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :bucket_span (Integer) — default: 600

    Time span to track in seconds

  • :bucket_interval (Integer) — default: 5

    How many seconds each bucket represents

  • :bucket_expiry (Integer) — default: @bucket_span

    How long we keep data in each bucket before it is auto expired. Cannot be larger than the bucket_span.

  • :redis (Redis) — default: nil

    Redis client if you need to customize connection options



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ratelimit.rb', line 46

def initialize(key, options = {})
  @key = key
  unless options.is_a?(Hash)
    raise ArgumentError.new("Redis object is now passed in via the options hash - options[:redis]")
  end
  @bucket_span = options[:bucket_span] || 600
  @bucket_interval = options[:bucket_interval] || 5
  @bucket_expiry = options[:bucket_expiry] || @bucket_span
  if @bucket_expiry > @bucket_span
    raise ArgumentError.new("Bucket expiry cannot be larger than the bucket span")
  end
  @bucket_count = (@bucket_span / @bucket_interval).round
  if @bucket_count < 3
    raise ArgumentError.new("Cannot have less than 3 buckets")
  end
  @raw_redis = options[:redis]
  load_scripts
end

Instance Method Details

#add(subject, count = 1) ⇒ Integer

Add to the counter for a given subject.

Parameters:

  • subject (String)

    A unique key to identify the subject. For example, ‘[email protected]

  • count (Integer) (defaults to: 1)

    The number by which to increase the counter

Returns:

  • (Integer)

    The counter value



71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/ratelimit.rb', line 71

def add(subject, count = 1)
  bucket = get_bucket
  subject = "#{@key}:#{subject}"

  # Cleanup expired keys every 100th request
  cleanup_expired_keys(subject) if rand < 0.01

  redis.multi do |transaction|
    transaction.hincrby(subject, bucket, count)
    transaction.expire(subject, @bucket_expiry + @bucket_interval)
  end.first
end

#count(subject, interval) ⇒ Object

Returns the count for a given subject and interval

Parameters:

  • subject (String)

    Subject for the count

  • interval (Integer)

    How far back (in seconds) to retrieve activity.



88
89
90
91
92
93
94
95
# File 'lib/ratelimit.rb', line 88

def count(subject, interval)
  interval = [[interval, @bucket_interval].max, @bucket_span].min
  oldest_bucket = get_bucket(Time.now.to_i - interval)
  current_bucket = get_bucket
  subject = "#{@key}:#{subject}"

  execute_script(@count_script_sha, [subject], [oldest_bucket, current_bucket])
end

#exceeded?(subject, options = {}) ⇒ Boolean

Check if the rate limit has been exceeded.

Parameters:

  • subject (String)

    Subject to check

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :interval (Integer)

    How far back to retrieve activity.

  • :threshold (Integer)

    Maximum number of actions

Returns:

  • (Boolean)


103
104
105
# File 'lib/ratelimit.rb', line 103

def exceeded?(subject, options = {})
  return count(subject, options[:interval]) >= options[:threshold]
end

#exec_within_threshold(subject, options = {}) { ... } ⇒ Object

Execute a block once the rate limit is within bounds WARNING This will block the current thread until the rate limit is within bounds.

Examples:

Send an email as long as we haven’t send 5 in the last 10 minutes

ratelimit.exec_with_threshold(email, [:threshold => 5, :interval => 600]) do
  send_another_email
  ratelimit.add(email)
end

Parameters:

  • subject (String)

    Subject for this rate limit

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :interval (Integer)

    How far back to retrieve activity.

  • :threshold (Integer)

    Maximum number of actions

Yields:

  • The block to be run



131
132
133
134
135
136
137
138
# File 'lib/ratelimit.rb', line 131

def exec_within_threshold(subject, options = {}, &block)
  options[:threshold] ||= 30
  options[:interval] ||= 30
  while exceeded?(subject, options)
    sleep @bucket_interval
  end
  yield(self)
end

#within_bounds?(subject, options = {}) ⇒ Boolean

Check if the rate limit is within bounds

Parameters:

  • subject (String)

    Subject to check

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :interval (Integer)

    How far back to retrieve activity.

  • :threshold (Integer)

    Maximum number of actions

Returns:

  • (Boolean)


113
114
115
# File 'lib/ratelimit.rb', line 113

def within_bounds?(subject, options = {})
  return !exceeded?(subject, options)
end