Class: Istox::RateLimit

Inherits:
Object
  • Object
show all
Defined in:
lib/istox/helpers/rate_limit.rb

Instance Method Summary collapse

Constructor Details

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

Create a Ratelimit object.

Cannot be larger than the bucket_span.

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.

  • :redis (Redis) — default: nil

    Redis client if you need to customize connection options

Raises:

  • (ArgumentError)


20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/istox/helpers/rate_limit.rb', line 20

def initialize(key, options = {})
  @key = key
  raise ArgumentError, 'Redis object is now passed in via the options hash - options[:redis]' unless options.is_a?(Hash)

  @bucket_span = options[:bucket_span] || 600
  @bucket_interval = options[:bucket_interval] || 5
  @bucket_expiry = options[:bucket_expiry] || @bucket_span
  raise ArgumentError, 'Bucket expiry cannot be larger than the bucket span' if @bucket_expiry > @bucket_span

  @bucket_count = (@bucket_span / @bucket_interval).round
  raise ArgumentError, 'Cannot have less than 3 buckets' if @bucket_count < 3

  @raw_redis = options[:redis]
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



41
42
43
44
45
46
47
48
49
50
# File 'lib/istox/helpers/rate_limit.rb', line 41

def add(subject, count = 1)
  bucket = get_bucket
  subject = "#{@key}:#{subject}"
  redis.multi do
    redis.hincrby(subject, bucket, count)
    redis.hdel(subject, (bucket + 1) % @bucket_count)
    redis.hdel(subject, (bucket + 2) % @bucket_count)
    redis.expire(subject, @bucket_expiry)
  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.



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/istox/helpers/rate_limit.rb', line 56

def count(subject, interval)
  bucket = get_bucket
  interval = [[interval, @bucket_interval].max, @bucket_span].min
  count = (interval / @bucket_interval).floor
  subject = "#{@key}:#{subject}"

  keys = (0..count - 1).map do |i|
    (bucket - i) % @bucket_count
  end
  redis.hmget(subject, *keys).inject(0) { |a, i| a + i.to_i }
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)


74
75
76
# File 'lib/istox/helpers/rate_limit.rb', line 74

def exceeded?(subject, options = {})
  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



130
131
132
133
134
135
# File 'lib/istox/helpers/rate_limit.rb', line 130

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

#limit!(subject, options = {}) ⇒ Object

Raise error if exceeded the limit

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

Raises:



94
95
96
97
98
# File 'lib/istox/helpers/rate_limit.rb', line 94

def limit!(subject, options = {})
  raise ::Istox::RateLimitExceedError if exceeded?(subject, options)

  add(subject)
end

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

Return false if exceeded the limit, else return true

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)


106
107
108
109
110
111
112
113
114
# File 'lib/istox/helpers/rate_limit.rb', line 106

def limit?(subject, options = {})
  result = within_bounds?(subject, options)

  return result unless result

  add(subject)

  true
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)


84
85
86
# File 'lib/istox/helpers/rate_limit.rb', line 84

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