class TrafficJam::Limit

This class represents a rate limit on an action, value pair. For example, if rate limiting the number of requests per IP address, the action could be :requests and the value would be the IP address. The class exposes atomic increment operations and allows querying of the current amount used and amount remaining.

Attributes

action[R]

@!attribute [r] action

@return [Symbol] the name of the action being rate limited.

@!attribute [r] value

@return [String] the target of the limit. The value should be a string
  or convertible to a distinct string when +to_s+ is called. If you
  would like to use objects that can be converted to a unique string,
  like a database-mapped object with an ID, you can implement
  +to_rate_limit_value+ on the object, which returns a deterministic
  string unique to that object.

@!attribute [r] max

@return [Integer] the integral cap of the limit amount.

@!attribute [r] period

@return [Integer] the duration of the limit in seconds. Regardless of
  the current amount used, after the period passes, the amount used will
  be 0.
max[R]

@!attribute [r] action

@return [Symbol] the name of the action being rate limited.

@!attribute [r] value

@return [String] the target of the limit. The value should be a string
  or convertible to a distinct string when +to_s+ is called. If you
  would like to use objects that can be converted to a unique string,
  like a database-mapped object with an ID, you can implement
  +to_rate_limit_value+ on the object, which returns a deterministic
  string unique to that object.

@!attribute [r] max

@return [Integer] the integral cap of the limit amount.

@!attribute [r] period

@return [Integer] the duration of the limit in seconds. Regardless of
  the current amount used, after the period passes, the amount used will
  be 0.
period[R]

@!attribute [r] action

@return [Symbol] the name of the action being rate limited.

@!attribute [r] value

@return [String] the target of the limit. The value should be a string
  or convertible to a distinct string when +to_s+ is called. If you
  would like to use objects that can be converted to a unique string,
  like a database-mapped object with an ID, you can implement
  +to_rate_limit_value+ on the object, which returns a deterministic
  string unique to that object.

@!attribute [r] max

@return [Integer] the integral cap of the limit amount.

@!attribute [r] period

@return [Integer] the duration of the limit in seconds. Regardless of
  the current amount used, after the period passes, the amount used will
  be 0.
value[R]

@!attribute [r] action

@return [Symbol] the name of the action being rate limited.

@!attribute [r] value

@return [String] the target of the limit. The value should be a string
  or convertible to a distinct string when +to_s+ is called. If you
  would like to use objects that can be converted to a unique string,
  like a database-mapped object with an ID, you can implement
  +to_rate_limit_value+ on the object, which returns a deterministic
  string unique to that object.

@!attribute [r] max

@return [Integer] the integral cap of the limit amount.

@!attribute [r] period

@return [Integer] the duration of the limit in seconds. Regardless of
  the current amount used, after the period passes, the amount used will
  be 0.

Public Class Methods

new(action, value, max: nil, period: nil) click to toggle source

Constructor takes an action name as a symbol, a maximum cap, and the period of limit. max and period are required keyword arguments.

@param action [Symbol] action name @param value [String] limit target value @param max [Integer] required limit maximum @param period [Integer] required limit period in seconds @raise [ArgumentError] if max or period is nil

# File lib/traffic_jam/limit.rb, line 35
def initialize(action, value, max: nil, period: nil)
  raise ArgumentError.new('Max is required') if max.nil?
  raise ArgumentError.new('Period is required') if period.nil?
  @action, @value, @max, @period = action, value, max, period
end

Public Instance Methods

decrement(amount = 1, time: Time.now) click to toggle source

Decrement the amount used by the given number. Time of decrement can be specified optionally with a keyword argument, which is useful for rolling back an increment operation at a certain time.

@param amount [Integer] amount to increment by @param time [Time] time when increment occurs @return [true]

# File lib/traffic_jam/limit.rb, line 112
def decrement(amount = 1, time: Time.now)
  increment(-amount, time: time)
end
exceeded?(amount = 1) click to toggle source

Return whether incrementing by the given amount would exceed limit. Does not change amount used.

@param amount [Integer] @return [Boolean]

# File lib/traffic_jam/limit.rb, line 46
def exceeded?(amount = 1)
  used + amount > max
end
flatten() click to toggle source
# File lib/traffic_jam/limit.rb, line 148
def flatten
  [self]
end
increment(amount = 1, time: Time.now) click to toggle source

Increment the amount used by the given number. Does not perform increment if the operation would exceed the limit. Returns whether the operation was successful. Time of increment can be specified optionally with a keyword argument, which is useful for rolling back with a decrement.

@param amount [Integer] amount to increment by @param time [Time] time when increment occurs @return [Boolean] true if increment succeded and false if incrementing

would exceed the limit
# File lib/traffic_jam/limit.rb, line 67
def increment(amount = 1, time: Time.now)
  return amount <= 0 if max.zero?

  if amount != amount.to_i
    raise ArgumentError.new("Amount must be an integer")
  end

  timestamp = (time.to_f * 1000).to_i
  argv = [timestamp, amount.to_i, max, period * 1000]

  result =
    begin
      redis.evalsha(
        Scripts::INCREMENT_SCRIPT_HASH, keys: [key], argv: argv)
    rescue Redis::CommandError
      redis.eval(Scripts::INCREMENT_SCRIPT, keys: [key], argv: argv)
    end

  !!result
end
increment!(amount = 1, time: Time.now) click to toggle source

Increment the amount used by the given number. Does not perform increment if the operation would exceed the limit. Raises an exception if the operation is unsuccessful. Time of# increment can be specified optionally with a keyword argument, which is useful for rolling back with a decrement.

@param amount [Integer] amount to increment by @param time [Time] time when increment occurs @return [nil] @raise [TrafficJam::LimitExceededError] if incrementing would exceed the

limit
# File lib/traffic_jam/limit.rb, line 99
def increment!(amount = 1, time: Time.now)
  if !increment(amount, time: time)
    raise TrafficJam::LimitExceededError.new(self)
  end
end
limit_exceeded(amount = 1) click to toggle source

Return itself if incrementing by the given amount would exceed limit, otherwise nil. Does not change amount used.

@return [TrafficJam::Limit, nil]

# File lib/traffic_jam/limit.rb, line 54
def limit_exceeded(amount = 1)
  self if exceeded?(amount)
end
remaining() click to toggle source

Return amount of limit remaining, taking time drift into account.

@return [Integer] amount remaining

# File lib/traffic_jam/limit.rb, line 144
def remaining
  max - used
end
reset() click to toggle source

Reset amount used to 0.

@return [nil]

# File lib/traffic_jam/limit.rb, line 119
def reset
  redis.del(key)
  nil
end
used() click to toggle source

Return amount of limit used, taking time drift into account.

@return [Integer] amount used

# File lib/traffic_jam/limit.rb, line 127
def used
  return 0 if max.zero?

  timestamp, amount = redis.hmget(key, 'timestamp', 'amount')
  if timestamp && amount
    time_passed = Time.now.to_f - timestamp.to_i / 1000.0
    drift = max * time_passed / period
    last_amount = [amount.to_f, max].min
    [(last_amount - drift).ceil, 0].max
  else
    0
  end
end

Private Instance Methods

config() click to toggle source
# File lib/traffic_jam/limit.rb, line 153
def config
  TrafficJam.config
end
key() click to toggle source
# File lib/traffic_jam/limit.rb, line 161
def key
  if !defined?(@key) || @key.nil?
    converted_value =
      begin
        value.to_rate_limit_value
      rescue NoMethodError
        value
      end
    hash = Digest::MD5.base64digest(converted_value.to_s)
    hash = hash[0...config.hash_length]
    @key = "#{key_prefix}:#{action}:#{hash}"
  end
  @key
end
key_prefix() click to toggle source
# File lib/traffic_jam/limit.rb, line 176
def key_prefix
  config.key_prefix
end
redis() click to toggle source
# File lib/traffic_jam/limit.rb, line 157
def redis
  config.redis
end