Class: Cybertrain::RateLimit
- Inherits:
-
Object
- Object
- Cybertrain::RateLimit
- Defined in:
- cybertrain/rate_limit.rb
Overview
The counter behind one Controller.rate_limit declaration: how many requests each key (a client address, prefixed with the controller’s path) has made in its current window, in this process.
A fixed window per key: the first request opens it, the to-th is the
last one let through, and the window closes within seconds after it
opened, when the next request opens a fresh one. Counting happens under
one Mutex, before the action runs; only the actions the declaration
names take it (NOTES rule 57: a lock on every request stalls under
several workers, so keep only: narrow).
Memory is bounded: at most #max_keys keys (10 000 by default). The keys are kept in the order their windows opened (a key whose window reopens is moved to the end), so when a new key finds the table full the expired windows are at its front; they go first, then the oldest open ones, a tenth of the table at a time. A flood of addresses can therefore end a client’s window early (it starts again from zero), never grow the process.
The counts are per process and lost on restart: with several processes each counts on its own. A later version may keep them in the database.
Constant Summary collapse
- MAX_KEYS =
The most keys one limiter keeps.
10_000- BODY =
The body of the default answer, a 429.
"Too Many Requests"
Instance Attribute Summary collapse
-
#alert_message ⇒ String
readonly
The flash alert set before that redirect (
"": none). -
#limit_count ⇒ Integer
readonly
Requests allowed per window (
to:). -
#max_keys ⇒ Integer
The most keys kept before eviction.
-
#period_seconds ⇒ Integer
readonly
The window, in seconds (
within:). -
#redirect_path ⇒ String
readonly
Where an exhausted client is redirected (
"": a 429 instead).
Class Method Summary collapse
-
.all ⇒ Array<RateLimit>
Every declared limiter, in declaration order (to reach one from a test).
-
.current_time ⇒ Integer
The time the limiters count with, in epoch seconds: the system clock, unless a test fixed it.
-
.current_time=(seconds) ⇒ nil
Fixes the time the limiters see, for a test that needs a window to pass; -1 goes back to the system clock.
-
.reset_all! ⇒ nil
Forgets every count of every limiter.
Instance Method Summary collapse
-
#by_remote_addr? ⇒ Boolean
True for
by: :remote_addr, false for the defaultby: :client_ip. -
#hit(key, now) ⇒ Integer
Counts one request for
keyatnow(epoch seconds) and answers 0 when it is allowed, or else the seconds until the key’s window closes (at least 1: the Retry-After value). -
#reset! ⇒ nil
Forgets every count.
-
#tracked?(key) ⇒ Boolean
Whether the table holds
key. -
#tracked_keys ⇒ Integer
How many keys the table holds now.
Instance Attribute Details
#alert_message ⇒ String (readonly)
Returns the flash alert set before that redirect ("": none).
48 49 50 |
# File 'cybertrain/rate_limit.rb', line 48 def @alert_message end |
#limit_count ⇒ Integer (readonly)
Returns requests allowed per window (to:).
35 36 37 |
# File 'cybertrain/rate_limit.rb', line 35 def limit_count @limit_count end |
#max_keys ⇒ Integer
Returns the most keys kept before eviction.
52 53 54 |
# File 'cybertrain/rate_limit.rb', line 52 def max_keys @max_keys end |
#period_seconds ⇒ Integer (readonly)
Returns the window, in seconds (within:).
39 40 41 |
# File 'cybertrain/rate_limit.rb', line 39 def period_seconds @period_seconds end |
#redirect_path ⇒ String (readonly)
Returns where an exhausted client is redirected ("": a
429 instead).
44 45 46 |
# File 'cybertrain/rate_limit.rb', line 44 def redirect_path @redirect_path end |
Class Method Details
.all ⇒ Array<RateLimit>
Returns every declared limiter, in declaration order (to reach one from a test).
186 187 188 |
# File 'cybertrain/rate_limit.rb', line 186 def self.all ALL end |
.current_time ⇒ Integer
The time the limiters count with, in epoch seconds: the system clock, unless a test fixed it.
211 212 213 214 |
# File 'cybertrain/rate_limit.rb', line 211 def self.current_time fixed = @fixed_time fixed < 0 ? Time.now.to_i : fixed end |
.current_time=(seconds) ⇒ nil
Fixes the time the limiters see, for a test that needs a window to pass; -1 goes back to the system clock.
221 222 223 224 |
# File 'cybertrain/rate_limit.rb', line 221 def self.current_time=(seconds) @fixed_time = seconds nil end |
.reset_all! ⇒ nil
Forgets every count of every limiter. The generated
AppTest.sign_in calls it, so that a test suite that signs in in
every test never reaches the sign-in limit.
195 196 197 198 199 200 201 202 |
# File 'cybertrain/rate_limit.rb', line 195 def self.reset_all! i = 0 while i < ALL.length ALL[i].reset! i += 1 end nil end |
Instance Method Details
#by_remote_addr? ⇒ Boolean
Returns true for by: :remote_addr, false for the default
by: :client_ip.
73 74 75 |
# File 'cybertrain/rate_limit.rb', line 73 def by_remote_addr? @by_remote_addr end |
#hit(key, now) ⇒ Integer
Counts one request for key at now (epoch seconds) and answers 0
when it is allowed, or else the seconds until the key’s window closes
(at least 1: the Retry-After value).
93 94 95 96 97 98 99 |
# File 'cybertrain/rate_limit.rb', line 93 def hit(key, now) wait = 0 @lock.synchronize do wait = count_hit(key, now) end wait end |
#reset! ⇒ nil
Forgets every count.
121 122 123 124 125 126 127 |
# File 'cybertrain/rate_limit.rb', line 121 def reset! @lock.synchronize do @starts.clear @counts.clear end nil end |
#tracked?(key) ⇒ Boolean
Returns whether the table holds key.
112 113 114 115 116 |
# File 'cybertrain/rate_limit.rb', line 112 def tracked?(key) held = false @lock.synchronize { held = @starts.key?(key) } held end |
#tracked_keys ⇒ Integer
Returns how many keys the table holds now.
103 104 105 106 107 |
# File 'cybertrain/rate_limit.rb', line 103 def tracked_keys held = 0 @lock.synchronize { held = @starts.length } held end |