Class: Prorate::Throttle

Inherits:
Object
  • Object
show all
Defined in:
lib/prorate/throttle.rb

Defined Under Namespace

Classes: Status

Constant Summary collapse

LUA_SCRIPT_CODE =
File.read(File.join(__dir__, "rate_limit.lua"))
LUA_SCRIPT_HASH =
Digest::SHA1.hexdigest(LUA_SCRIPT_CODE)

Instance Method Summary collapse

Constructor Details

#initializeThrottle

Returns a new instance of Throttle.



11
12
13
14
15
16
17
# File 'lib/prorate/throttle.rb', line 11

def initialize(*)
  super
  @discriminators = [name.to_s]
  self.redis = NullPool.new(redis) unless redis.respond_to?(:with)
  raise MisconfiguredThrottle if (period <= 0) || (limit <= 0)
  @leak_rate = limit.to_f / period # tokens per second;
end

Instance Method Details

#<<(discriminator) ⇒ Object

Add a value that will be used to distinguish this throttle from others. It has to be something user- or connection-specific, and multiple discriminators can be combined:

throttle << ip_address << user_agent_fingerprint

Parameters:

  • discriminator (Object)

    a Ruby object that can be marshaled in an equivalent way between requests, using ‘Marshal.dump



27
28
29
# File 'lib/prorate/throttle.rb', line 27

def <<(discriminator)
  @discriminators << discriminator
end

#statusObject



99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/prorate/throttle.rb', line 99

def status
  discriminator = Digest::SHA1.hexdigest(Marshal.dump(@discriminators))
  identifier = [name, discriminator].join(':')

  redis.with do |r|
    is_blocked = r.exists("#{identifier}.block")
    return Status.new(is_throttled: false, remaining_throttle_seconds: 0) unless is_blocked

    remaining_seconds = r.get("#{identifier}.block").to_i - Time.now.to_i
    Status.new(is_throttled: true, remaining_throttle_seconds: remaining_seconds)
  end
end

#throttle!(n_tokens: 1) ⇒ Object

Applies the throttle and raises a Prorate::Throttled exception if it has been triggered

Accepts an optional number of tokens to put in the bucket (default is 1). The effect of ‘n_tokens:` set to 0 is a “ping”. It makes sure the throttle keys in Redis get created and adjusts the last invoked time of the leaky bucket. Can be used when a throttle is applied in a “shadow” fashion. For example, imagine you have a cascade of throttles with the following block times:

Throttle A: [-------]
Throttle B: [----------]

You apply Throttle A: and it fires, but when that happens you also want to enable a throttle that is applied to “repeat offenders” only -

  • for instance ones that probe for tokens and/or passwords.

    Throttle C: [——————————-]

If your “Throttle A” fires, you can trigger Throttle C

Throttle A: [-----|-]
Throttle C: [-----|-------------------------]

because you know that Throttle A has fired and thus Throttle C comes into effect. What you want to do, however, is to fire Throttle C even though Throttle A: would have unlatched, which would create this call sequence:

Throttle A: [-------]    *(A not triggered)
Throttle C: [------------|------------------]

To achieve that you can keep Throttle C alive using ‘throttle!(n_tokens: 0)`, on every check that touches Throttle A and/or Throttle C. It keeps the leaky bucket updated but does not add any tokens to it:

Throttle A: [------]    *(A not triggered since block period has ended)
Throttle C: [-----------|(ping)------------------]  C is still blocking

So you can effectively “keep a throttle alive” without ever triggering it, or keep it alive in combination with other throttles.

Parameters:

  • n_tokens (Integer) (defaults to: 1)

    the number of tokens to put in the bucket. If you are using Prorate for rate limiting, and a single request is adding N objects to your database for example, you can “top up” the bucket with a set number of tokens with a arbitrary ratio - like 1 token per inserted row. Once the bucket fills up the Throttled exception is going to be raised. Defaults to 1.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/prorate/throttle.rb', line 77

def throttle!(n_tokens: 1)
  discriminator = Digest::SHA1.hexdigest(Marshal.dump(@discriminators))
  identifier = [name, discriminator].join(':')

  redis.with do |r|
    logger.debug { "Applying throttle counter %s" % name }
    remaining_block_time, bucket_level = run_lua_throttler(
      redis: r,
      identifier: identifier,
      bucket_capacity: limit,
      leak_rate: @leak_rate,
      block_for: block_for,
      n_tokens: n_tokens)

    if remaining_block_time > 0
      logger.warn { "Throttle %s exceeded limit of %d in %d seconds and is blocked for the next %d seconds" % [name, limit, period, remaining_block_time] }
      raise ::Prorate::Throttled.new(name, remaining_block_time)
    end
    return limit - bucket_level # How many calls remain
  end
end