Class: TrafficJam::Limit

Inherits:
Object
  • Object
show all
Defined in:
lib/traffic_jam/limit.rb

Overview

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.

Direct Known Subclasses

GCRALimit, LifetimeLimit, RollingLimit

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(action, value, max: nil, period: nil) ⇒ Limit

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

Parameters:

  • action (Symbol)

    action name

  • value (String)

    limit target value

  • max (Integer) (defaults to: nil)

    required limit maximum

  • period (Integer) (defaults to: nil)

    required limit period in seconds

Raises:

  • (ArgumentError)

    if max or period is nil



35
36
37
38
39
# 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

Instance Attribute Details

#actionSymbol (readonly)

Returns the name of the action being rate limited.

Returns:

  • (Symbol)

    the name of the action being rate limited.



25
26
27
# File 'lib/traffic_jam/limit.rb', line 25

def action
  @action
end

#maxInteger (readonly)

Returns the integral cap of the limit amount.

Returns:

  • (Integer)

    the integral cap of the limit amount.



25
# File 'lib/traffic_jam/limit.rb', line 25

attr_reader :action, :max, :period, :value

#periodInteger (readonly)

Returns the duration of the limit in seconds. Regardless of the current amount used, after the period passes, the amount used will be 0.

Returns:

  • (Integer)

    the duration of the limit in seconds. Regardless of the current amount used, after the period passes, the amount used will be 0.



25
# File 'lib/traffic_jam/limit.rb', line 25

attr_reader :action, :max, :period, :value

#valueObject (readonly)

Returns the value of attribute value.



25
# File 'lib/traffic_jam/limit.rb', line 25

attr_reader :action, :max, :period, :value

Instance Method Details

#decrement(amount = 1, time: Time.now) ⇒ true

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.

Parameters:

  • amount (Integer) (defaults to: 1)

    amount to increment by

  • time (Time) (defaults to: Time.now)

    time when increment occurs

Returns:

  • (true)


112
113
114
# File 'lib/traffic_jam/limit.rb', line 112

def decrement(amount = 1, time: Time.now)
  increment(-amount, time: time)
end

#exceeded?(amount = 1) ⇒ Boolean

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

Parameters:

  • amount (Integer) (defaults to: 1)

Returns:

  • (Boolean)


46
47
48
# File 'lib/traffic_jam/limit.rb', line 46

def exceeded?(amount = 1)
  used + amount > max
end

#flattenObject



148
149
150
# File 'lib/traffic_jam/limit.rb', line 148

def flatten
  [self]
end

#increment(amount = 1, time: Time.now) ⇒ Boolean

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.

Parameters:

  • amount (Integer) (defaults to: 1)

    amount to increment by

  • time (Time) (defaults to: Time.now)

    time when increment occurs

Returns:

  • (Boolean)

    true if increment succeded and false if incrementing would exceed the limit



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 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) ⇒ nil

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.

Parameters:

  • amount (Integer) (defaults to: 1)

    amount to increment by

  • time (Time) (defaults to: Time.now)

    time when increment occurs

Returns:

  • (nil)

Raises:



99
100
101
102
103
# 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) ⇒ TrafficJam::Limit?

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

Returns:



54
55
56
# File 'lib/traffic_jam/limit.rb', line 54

def limit_exceeded(amount = 1)
  self if exceeded?(amount)
end

#remainingInteger

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

Returns:

  • (Integer)

    amount remaining



144
145
146
# File 'lib/traffic_jam/limit.rb', line 144

def remaining
  max - used
end

#resetnil

Reset amount used to 0.

Returns:

  • (nil)


119
120
121
122
# File 'lib/traffic_jam/limit.rb', line 119

def reset
  redis.del(key)
  nil
end

#usedInteger

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

Returns:

  • (Integer)

    amount used



127
128
129
130
131
132
133
134
135
136
137
138
139
# 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