Class: Cybertrain::RateLimit

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#alert_message ⇒ String (readonly)

Returns the flash alert set before that redirect ("": none).

Returns:

  • (String) —

    the flash alert set before that redirect ("": none)



48
49
50
# File 'cybertrain/rate_limit.rb', line 48

def alert_message
  @alert_message
end

#limit_count ⇒ Integer (readonly)

Returns requests allowed per window (to:).

Returns:

  • (Integer) —

    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.

Returns:

  • (Integer) —

    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:).

Returns:

  • (Integer) —

    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).

Returns:

  • (String) —

    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).

Returns:

  • (Array<RateLimit>) —

    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.

Returns:

  • (Integer)


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.

Parameters:

  • seconds (Integer) —

    epoch seconds, or -1

Returns:

  • (nil)


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.

Returns:

  • (nil)


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.

Returns:

  • (Boolean) —

    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).

Parameters:

  • key (String)
  • now (Integer)

Returns:

  • (Integer)


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.

Returns:

  • (nil)


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.

Parameters:

  • key (String)

Returns:

  • (Boolean) —

    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.

Returns:

  • (Integer) —

    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