Class: Prorate::Throttle
- Inherits:
-
Object
- Object
- Prorate::Throttle
- 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 Attribute Summary collapse
-
#block_for ⇒ Object
readonly
Returns the value of attribute block_for.
-
#limit ⇒ Object
readonly
Returns the value of attribute limit.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#period ⇒ Object
readonly
Returns the value of attribute period.
-
#redis ⇒ Object
readonly
Returns the value of attribute redis.
Instance Method Summary collapse
-
#<<(discriminator) ⇒ Object
Add a value that will be used to distinguish this throttle from others.
-
#initialize(name:, limit:, period:, block_for:, redis:, logger: Prorate::NullLogger) ⇒ Throttle
constructor
A new instance of Throttle.
- #status ⇒ Object
-
#throttle!(n_tokens: 1) ⇒ Object
Applies the throttle and raises a Throttled exception if it has been triggered.
Constructor Details
#initialize(name:, limit:, period:, block_for:, redis:, logger: Prorate::NullLogger) ⇒ Throttle
Returns a new instance of Throttle.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/prorate/throttle.rb', line 13 def initialize(name:, limit:, period:, block_for:, redis:, logger: Prorate::NullLogger) @name = name.to_s @discriminators = [name.to_s] @redis = redis.respond_to?(:with) ? redis : NullPool.new(redis) @logger = logger @block_for = block_for raise MisconfiguredThrottle if (period <= 0) || (limit <= 0) # Do not do type conversions here since we want to allow the caller to read # those values back later # (API contract which the previous implementation of Throttle already supported) @limit = limit @period = period @leak_rate = limit.to_f / period # tokens per second; end |
Instance Attribute Details
#block_for ⇒ Object (readonly)
Returns the value of attribute block_for.
11 12 13 |
# File 'lib/prorate/throttle.rb', line 11 def block_for @block_for end |
#limit ⇒ Object (readonly)
Returns the value of attribute limit.
11 12 13 |
# File 'lib/prorate/throttle.rb', line 11 def limit @limit end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
11 12 13 |
# File 'lib/prorate/throttle.rb', line 11 def logger @logger end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
11 12 13 |
# File 'lib/prorate/throttle.rb', line 11 def name @name end |
#period ⇒ Object (readonly)
Returns the value of attribute period.
11 12 13 |
# File 'lib/prorate/throttle.rb', line 11 def period @period end |
#redis ⇒ Object (readonly)
Returns the value of attribute redis.
11 12 13 |
# File 'lib/prorate/throttle.rb', line 11 def redis @redis 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
39 40 41 |
# File 'lib/prorate/throttle.rb', line 39 def <<(discriminator) @discriminators << discriminator end |
#status ⇒ Object
108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/prorate/throttle.rb', line 108 def status redis_block_key = "#{identifier}.block" @redis.with do |r| is_blocked = redis_key_exists?(r, redis_block_key) if is_blocked remaining_seconds = r.get(redis_block_key).to_i - Time.now.to_i Status.new(_is_throttled = true, remaining_seconds) else remaining_seconds = 0 Status.new(_is_throttled = false, remaining_seconds) end 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.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/prorate/throttle.rb', line 89 def throttle!(n_tokens: 1) @logger.debug { "Applying throttle counter %s" % @name } remaining_block_time, bucket_level = run_lua_throttler( identifier: identifier, bucket_capacity: @limit, leak_rate: @leak_rate, block_for: @block_for, n_tokens: n_tokens) if bucket_level == -1 @logger.warn do "Throttle %s exceeded limit of %d in %d seconds and is blocked for the next %s seconds" % [@name, @limit, @period, remaining_block_time] end raise ::Prorate::Throttled.new(@name, remaining_block_time) end @limit - bucket_level # Return how many calls remain end |