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 Method Summary collapse
-
#<<(discriminator) ⇒ Object
Add a value that will be used to distinguish this throttle from others.
-
#initialize ⇒ 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 ⇒ Throttle
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
27 28 29 |
# File 'lib/prorate/throttle.rb', line 27 def <<(discriminator) @discriminators << discriminator end |
#status ⇒ Object
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.
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 |