pecorino 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +42 -19
- data/.gitignore +1 -0
- data/CHANGELOG.md +4 -0
- data/README.md +22 -2
- data/Rakefile +12 -1
- data/gemfiles/Gemfile_ruby27_rails7 +19 -0
- data/gemfiles/Gemfile_ruby30_rails8 +15 -0
- data/lib/pecorino/adapters/base_adapter.rb +1 -1
- data/lib/pecorino/version.rb +1 -1
- data/pecorino.gemspec +1 -14
- data/rbi/pecorino.rbi +905 -0
- data/test/adapters/adapter_test_methods.rb +259 -0
- data/test/adapters/memory_adapter_test.rb +12 -0
- data/test/adapters/postgres_adapter_test.rb +69 -0
- data/test/adapters/redis_adapter_test.rb +27 -0
- data/test/adapters/sqlite_adapter_test.rb +46 -0
- data/test/block_test.rb +23 -0
- data/test/cached_throttle_test.rb +100 -0
- data/test/leaky_bucket_test.rb +161 -0
- data/test/pecorino_test.rb +9 -0
- data/test/test_helper.rb +12 -0
- data/test/throttle_test.rb +119 -0
- metadata +19 -138
- data/Gemfile +0 -6
data/rbi/pecorino.rbi
ADDED
@@ -0,0 +1,905 @@
|
|
1
|
+
# typed: strong
|
2
|
+
module Pecorino
|
3
|
+
VERSION = T.let("0.7.1", T.untyped)
|
4
|
+
|
5
|
+
# Deletes stale leaky buckets and blocks which have expired. Run this method regularly to
|
6
|
+
# avoid accumulating too many unused rows in your tables.
|
7
|
+
#
|
8
|
+
# _@return_ — void
|
9
|
+
sig { returns(T.untyped) }
|
10
|
+
def self.prune!; end
|
11
|
+
|
12
|
+
# sord warn - ActiveRecord::SchemaMigration wasn't able to be resolved to a constant in this project
|
13
|
+
# Creates the tables and indexes needed for Pecorino. Call this from your migrations like so:
|
14
|
+
#
|
15
|
+
# class CreatePecorinoTables < ActiveRecord::Migration[7.0]
|
16
|
+
# def change
|
17
|
+
# Pecorino.create_tables(self)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# _@param_ `active_record_schema` — the migration through which we will create the tables
|
22
|
+
#
|
23
|
+
# _@return_ — void
|
24
|
+
sig { params(active_record_schema: ActiveRecord::SchemaMigration).returns(T.untyped) }
|
25
|
+
def self.create_tables(active_record_schema); end
|
26
|
+
|
27
|
+
# Allows assignment of an adapter for storing throttles. Normally this would be a subclass of `Pecorino::Adapters::BaseAdapter`, but
|
28
|
+
# you can assign anything you like. Set this in an initializer. By default Pecorino will use the adapter configured from your main
|
29
|
+
# database, but you can also create a separate database for it - or use Redis or memory storage.
|
30
|
+
#
|
31
|
+
# _@param_ `adapter`
|
32
|
+
sig { params(adapter: Pecorino::Adapters::BaseAdapter).returns(Pecorino::Adapters::BaseAdapter) }
|
33
|
+
def self.adapter=(adapter); end
|
34
|
+
|
35
|
+
# Returns the currently configured adapter, or the default adapter from the main database
|
36
|
+
sig { returns(Pecorino::Adapters::BaseAdapter) }
|
37
|
+
def self.adapter; end
|
38
|
+
|
39
|
+
# sord omit - no YARD return type given, using untyped
|
40
|
+
# Returns the database implementation for setting the values atomically. Since the implementation
|
41
|
+
# differs per database, this method will return a different adapter depending on which database is
|
42
|
+
# being used
|
43
|
+
#
|
44
|
+
# _@param_ `adapter`
|
45
|
+
sig { returns(T.untyped) }
|
46
|
+
def self.default_adapter_from_main_database; end
|
47
|
+
|
48
|
+
module Adapters
|
49
|
+
# An adapter allows Pecorino throttles, leaky buckets and other
|
50
|
+
# resources to interfact to a data storage backend - a database, usually.
|
51
|
+
class BaseAdapter
|
52
|
+
# Returns the state of a leaky bucket. The state should be a tuple of two
|
53
|
+
# values: the current level (Float) and whether the bucket is now at capacity (Boolean)
|
54
|
+
#
|
55
|
+
# _@param_ `key` — the key of the leaky bucket
|
56
|
+
#
|
57
|
+
# _@param_ `capacity` — the capacity of the leaky bucket to limit to
|
58
|
+
#
|
59
|
+
# _@param_ `leak_rate` — how many tokens leak out of the bucket per second
|
60
|
+
sig { params(key: String, capacity: Float, leak_rate: Float).returns(T::Array[T.untyped]) }
|
61
|
+
def state(key:, capacity:, leak_rate:); end
|
62
|
+
|
63
|
+
# Adds tokens to the leaky bucket. The return value is a tuple of two
|
64
|
+
# values: the current level (Float) and whether the bucket is now at capacity (Boolean)
|
65
|
+
#
|
66
|
+
# _@param_ `key` — the key of the leaky bucket
|
67
|
+
#
|
68
|
+
# _@param_ `capacity` — the capacity of the leaky bucket to limit to
|
69
|
+
#
|
70
|
+
# _@param_ `leak_rate` — how many tokens leak out of the bucket per second
|
71
|
+
#
|
72
|
+
# _@param_ `n_tokens` — how many tokens to add
|
73
|
+
sig do
|
74
|
+
params(
|
75
|
+
key: String,
|
76
|
+
capacity: Float,
|
77
|
+
leak_rate: Float,
|
78
|
+
n_tokens: Float
|
79
|
+
).returns(T::Array[T.untyped])
|
80
|
+
end
|
81
|
+
def add_tokens(key:, capacity:, leak_rate:, n_tokens:); end
|
82
|
+
|
83
|
+
# Adds tokens to the leaky bucket conditionally. If there is capacity, the tokens will
|
84
|
+
# be added. If there isn't - the fillup will be rejected. The return value is a triplet of
|
85
|
+
# the current level (Float), whether the bucket is now at capacity (Boolean)
|
86
|
+
# and whether the fillup was accepted (Boolean)
|
87
|
+
#
|
88
|
+
# _@param_ `key` — the key of the leaky bucket
|
89
|
+
#
|
90
|
+
# _@param_ `capacity` — the capacity of the leaky bucket to limit to
|
91
|
+
#
|
92
|
+
# _@param_ `leak_rate` — how many tokens leak out of the bucket per second
|
93
|
+
#
|
94
|
+
# _@param_ `n_tokens` — how many tokens to add
|
95
|
+
sig do
|
96
|
+
params(
|
97
|
+
key: String,
|
98
|
+
capacity: Float,
|
99
|
+
leak_rate: Float,
|
100
|
+
n_tokens: Float
|
101
|
+
).returns(T::Array[T.untyped])
|
102
|
+
end
|
103
|
+
def add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:); end
|
104
|
+
|
105
|
+
# sord duck - #to_f looks like a duck type, replacing with untyped
|
106
|
+
# sord warn - "Active Support Duration" does not appear to be a type
|
107
|
+
# sord omit - no YARD return type given, using untyped
|
108
|
+
# Sets a timed block for the given key - this is used when a throttle fires. The return value
|
109
|
+
# is not defined - the call should always succeed.
|
110
|
+
#
|
111
|
+
# _@param_ `key` — the key of the block
|
112
|
+
#
|
113
|
+
# _@param_ `block_for` — the duration of the block, in seconds
|
114
|
+
sig { params(key: String, block_for: T.any(T.untyped, SORD_ERROR_ActiveSupportDuration)).returns(T.untyped) }
|
115
|
+
def set_block(key:, block_for:); end
|
116
|
+
|
117
|
+
# sord omit - no YARD return type given, using untyped
|
118
|
+
# Returns the time until which a block for a given key is in effect. If there is no block in
|
119
|
+
# effect, the method should return `nil`. The return value is either a `Time` or `nil`
|
120
|
+
#
|
121
|
+
# _@param_ `key` — the key of the block
|
122
|
+
sig { params(key: String).returns(T.untyped) }
|
123
|
+
def blocked_until(key:); end
|
124
|
+
|
125
|
+
# Deletes leaky buckets which have an expiry value prior to now and throttle blocks which have
|
126
|
+
# now lapsed
|
127
|
+
sig { void }
|
128
|
+
def prune; end
|
129
|
+
|
130
|
+
# sord omit - no YARD type given for "active_record_schema", using untyped
|
131
|
+
# sord omit - no YARD return type given, using untyped
|
132
|
+
# Creates the database tables for Pecorino to operate, or initializes other
|
133
|
+
# schema-like resources the adapter needs to operate
|
134
|
+
sig { params(active_record_schema: T.untyped).returns(T.untyped) }
|
135
|
+
def create_tables(active_record_schema); end
|
136
|
+
end
|
137
|
+
|
138
|
+
# An adapter for storing Pecorino leaky buckets and blocks in Redis. It uses Lua
|
139
|
+
# to enforce atomicity for leaky bucket operations
|
140
|
+
class RedisAdapter < Pecorino::Adapters::BaseAdapter
|
141
|
+
ADD_TOKENS_SCRIPT = T.let(RedisScript.new("add_tokens_conditionally.lua"), T.untyped)
|
142
|
+
|
143
|
+
# sord omit - no YARD type given for "redis_connection_or_connection_pool", using untyped
|
144
|
+
# sord omit - no YARD type given for "key_prefix:", using untyped
|
145
|
+
sig { params(redis_connection_or_connection_pool: T.untyped, key_prefix: T.untyped).void }
|
146
|
+
def initialize(redis_connection_or_connection_pool, key_prefix: "pecorino"); end
|
147
|
+
|
148
|
+
# Returns the state of a leaky bucket. The state should be a tuple of two
|
149
|
+
# values: the current level (Float) and whether the bucket is now at capacity (Boolean)
|
150
|
+
sig { params(key: String, capacity: Float, leak_rate: Float).returns(T::Array[T.untyped]) }
|
151
|
+
def state(key:, capacity:, leak_rate:); end
|
152
|
+
|
153
|
+
# Adds tokens to the leaky bucket. The return value is a tuple of two
|
154
|
+
# values: the current level (Float) and whether the bucket is now at capacity (Boolean)
|
155
|
+
sig do
|
156
|
+
params(
|
157
|
+
key: String,
|
158
|
+
capacity: Float,
|
159
|
+
leak_rate: Float,
|
160
|
+
n_tokens: Float
|
161
|
+
).returns(T::Array[T.untyped])
|
162
|
+
end
|
163
|
+
def add_tokens(key:, capacity:, leak_rate:, n_tokens:); end
|
164
|
+
|
165
|
+
# Adds tokens to the leaky bucket conditionally. If there is capacity, the tokens will
|
166
|
+
# be added. If there isn't - the fillup will be rejected. The return value is a triplet of
|
167
|
+
# the current level (Float), whether the bucket is now at capacity (Boolean)
|
168
|
+
# and whether the fillup was accepted (Boolean)
|
169
|
+
sig do
|
170
|
+
params(
|
171
|
+
key: String,
|
172
|
+
capacity: Float,
|
173
|
+
leak_rate: Float,
|
174
|
+
n_tokens: Float
|
175
|
+
).returns(T::Array[T.untyped])
|
176
|
+
end
|
177
|
+
def add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:); end
|
178
|
+
|
179
|
+
# sord duck - #to_f looks like a duck type, replacing with untyped
|
180
|
+
# sord warn - "Active Support Duration" does not appear to be a type
|
181
|
+
# sord omit - no YARD return type given, using untyped
|
182
|
+
# Sets a timed block for the given key - this is used when a throttle fires. The return value
|
183
|
+
# is not defined - the call should always succeed.
|
184
|
+
sig { params(key: String, block_for: T.any(T.untyped, SORD_ERROR_ActiveSupportDuration)).returns(T.untyped) }
|
185
|
+
def set_block(key:, block_for:); end
|
186
|
+
|
187
|
+
# sord omit - no YARD return type given, using untyped
|
188
|
+
# Returns the time until which a block for a given key is in effect. If there is no block in
|
189
|
+
# effect, the method should return `nil`. The return value is either a `Time` or `nil`
|
190
|
+
sig { params(key: String).returns(T.untyped) }
|
191
|
+
def blocked_until(key:); end
|
192
|
+
|
193
|
+
# sord omit - no YARD return type given, using untyped
|
194
|
+
sig { returns(T.untyped) }
|
195
|
+
def with_redis; end
|
196
|
+
|
197
|
+
class RedisScript
|
198
|
+
# sord omit - no YARD type given for "script_filename", using untyped
|
199
|
+
sig { params(script_filename: T.untyped).void }
|
200
|
+
def initialize(script_filename); end
|
201
|
+
|
202
|
+
# sord omit - no YARD type given for "redis", using untyped
|
203
|
+
# sord omit - no YARD type given for "keys", using untyped
|
204
|
+
# sord omit - no YARD type given for "argv", using untyped
|
205
|
+
# sord omit - no YARD return type given, using untyped
|
206
|
+
sig { params(redis: T.untyped, keys: T.untyped, argv: T.untyped).returns(T.untyped) }
|
207
|
+
def load_and_eval(redis, keys, argv); end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# A memory store for leaky buckets and blocks
|
212
|
+
class MemoryAdapter
|
213
|
+
sig { void }
|
214
|
+
def initialize; end
|
215
|
+
|
216
|
+
# sord omit - no YARD type given for "key:", using untyped
|
217
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
218
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
219
|
+
# sord omit - no YARD return type given, using untyped
|
220
|
+
# Returns the state of a leaky bucket. The state should be a tuple of two
|
221
|
+
# values: the current level (Float) and whether the bucket is now at capacity (Boolean)
|
222
|
+
sig { params(key: T.untyped, capacity: T.untyped, leak_rate: T.untyped).returns(T.untyped) }
|
223
|
+
def state(key:, capacity:, leak_rate:); end
|
224
|
+
|
225
|
+
# sord omit - no YARD type given for "key:", using untyped
|
226
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
227
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
228
|
+
# sord omit - no YARD type given for "n_tokens:", using untyped
|
229
|
+
# sord omit - no YARD return type given, using untyped
|
230
|
+
# Adds tokens to the leaky bucket. The return value is a tuple of two
|
231
|
+
# values: the current level (Float) and whether the bucket is now at capacity (Boolean)
|
232
|
+
sig do
|
233
|
+
params(
|
234
|
+
key: T.untyped,
|
235
|
+
capacity: T.untyped,
|
236
|
+
leak_rate: T.untyped,
|
237
|
+
n_tokens: T.untyped
|
238
|
+
).returns(T.untyped)
|
239
|
+
end
|
240
|
+
def add_tokens(key:, capacity:, leak_rate:, n_tokens:); end
|
241
|
+
|
242
|
+
# sord omit - no YARD type given for "key:", using untyped
|
243
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
244
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
245
|
+
# sord omit - no YARD type given for "n_tokens:", using untyped
|
246
|
+
# sord omit - no YARD return type given, using untyped
|
247
|
+
# Adds tokens to the leaky bucket conditionally. If there is capacity, the tokens will
|
248
|
+
# be added. If there isn't - the fillup will be rejected. The return value is a triplet of
|
249
|
+
# the current level (Float), whether the bucket is now at capacity (Boolean)
|
250
|
+
# and whether the fillup was accepted (Boolean)
|
251
|
+
sig do
|
252
|
+
params(
|
253
|
+
key: T.untyped,
|
254
|
+
capacity: T.untyped,
|
255
|
+
leak_rate: T.untyped,
|
256
|
+
n_tokens: T.untyped
|
257
|
+
).returns(T.untyped)
|
258
|
+
end
|
259
|
+
def add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:); end
|
260
|
+
|
261
|
+
# sord omit - no YARD type given for "key:", using untyped
|
262
|
+
# sord omit - no YARD type given for "block_for:", using untyped
|
263
|
+
# sord omit - no YARD return type given, using untyped
|
264
|
+
# Sets a timed block for the given key - this is used when a throttle fires. The return value
|
265
|
+
# is not defined - the call should always succeed.
|
266
|
+
sig { params(key: T.untyped, block_for: T.untyped).returns(T.untyped) }
|
267
|
+
def set_block(key:, block_for:); end
|
268
|
+
|
269
|
+
# sord omit - no YARD type given for "key:", using untyped
|
270
|
+
# sord omit - no YARD return type given, using untyped
|
271
|
+
# Returns the time until which a block for a given key is in effect. If there is no block in
|
272
|
+
# effect, the method should return `nil`. The return value is either a `Time` or `nil`
|
273
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
274
|
+
def blocked_until(key:); end
|
275
|
+
|
276
|
+
# sord omit - no YARD return type given, using untyped
|
277
|
+
# Deletes leaky buckets which have an expiry value prior to now and throttle blocks which have
|
278
|
+
# now lapsed
|
279
|
+
sig { returns(T.untyped) }
|
280
|
+
def prune; end
|
281
|
+
|
282
|
+
# sord omit - no YARD type given for "active_record_schema", using untyped
|
283
|
+
# sord omit - no YARD return type given, using untyped
|
284
|
+
# No-op
|
285
|
+
sig { params(active_record_schema: T.untyped).returns(T.untyped) }
|
286
|
+
def create_tables(active_record_schema); end
|
287
|
+
|
288
|
+
# sord omit - no YARD type given for "key", using untyped
|
289
|
+
# sord omit - no YARD type given for "capacity", using untyped
|
290
|
+
# sord omit - no YARD type given for "leak_rate", using untyped
|
291
|
+
# sord omit - no YARD type given for "n_tokens", using untyped
|
292
|
+
# sord omit - no YARD type given for "conditionally", using untyped
|
293
|
+
# sord omit - no YARD return type given, using untyped
|
294
|
+
sig do
|
295
|
+
params(
|
296
|
+
key: T.untyped,
|
297
|
+
capacity: T.untyped,
|
298
|
+
leak_rate: T.untyped,
|
299
|
+
n_tokens: T.untyped,
|
300
|
+
conditionally: T.untyped
|
301
|
+
).returns(T.untyped)
|
302
|
+
end
|
303
|
+
def add_tokens_with_lock(key, capacity, leak_rate, n_tokens, conditionally); end
|
304
|
+
|
305
|
+
# sord omit - no YARD return type given, using untyped
|
306
|
+
sig { returns(T.untyped) }
|
307
|
+
def get_mono_time; end
|
308
|
+
|
309
|
+
# sord omit - no YARD type given for "min", using untyped
|
310
|
+
# sord omit - no YARD type given for "value", using untyped
|
311
|
+
# sord omit - no YARD type given for "max", using untyped
|
312
|
+
# sord omit - no YARD return type given, using untyped
|
313
|
+
sig { params(min: T.untyped, value: T.untyped, max: T.untyped).returns(T.untyped) }
|
314
|
+
def clamp(min, value, max); end
|
315
|
+
|
316
|
+
class KeyedLock
|
317
|
+
sig { void }
|
318
|
+
def initialize; end
|
319
|
+
|
320
|
+
# sord omit - no YARD type given for "key", using untyped
|
321
|
+
# sord omit - no YARD return type given, using untyped
|
322
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
323
|
+
def lock(key); end
|
324
|
+
|
325
|
+
# sord omit - no YARD type given for "key", using untyped
|
326
|
+
# sord omit - no YARD return type given, using untyped
|
327
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
328
|
+
def unlock(key); end
|
329
|
+
|
330
|
+
# sord omit - no YARD type given for "key", using untyped
|
331
|
+
# sord omit - no YARD return type given, using untyped
|
332
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
333
|
+
def with(key); end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
class SqliteAdapter
|
338
|
+
# sord omit - no YARD type given for "model_class", using untyped
|
339
|
+
sig { params(model_class: T.untyped).void }
|
340
|
+
def initialize(model_class); end
|
341
|
+
|
342
|
+
# sord omit - no YARD type given for "key:", using untyped
|
343
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
344
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
345
|
+
# sord omit - no YARD return type given, using untyped
|
346
|
+
sig { params(key: T.untyped, capacity: T.untyped, leak_rate: T.untyped).returns(T.untyped) }
|
347
|
+
def state(key:, capacity:, leak_rate:); end
|
348
|
+
|
349
|
+
# sord omit - no YARD type given for "key:", using untyped
|
350
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
351
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
352
|
+
# sord omit - no YARD type given for "n_tokens:", using untyped
|
353
|
+
# sord omit - no YARD return type given, using untyped
|
354
|
+
sig do
|
355
|
+
params(
|
356
|
+
key: T.untyped,
|
357
|
+
capacity: T.untyped,
|
358
|
+
leak_rate: T.untyped,
|
359
|
+
n_tokens: T.untyped
|
360
|
+
).returns(T.untyped)
|
361
|
+
end
|
362
|
+
def add_tokens(key:, capacity:, leak_rate:, n_tokens:); end
|
363
|
+
|
364
|
+
# sord omit - no YARD type given for "key:", using untyped
|
365
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
366
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
367
|
+
# sord omit - no YARD type given for "n_tokens:", using untyped
|
368
|
+
# sord omit - no YARD return type given, using untyped
|
369
|
+
sig do
|
370
|
+
params(
|
371
|
+
key: T.untyped,
|
372
|
+
capacity: T.untyped,
|
373
|
+
leak_rate: T.untyped,
|
374
|
+
n_tokens: T.untyped
|
375
|
+
).returns(T.untyped)
|
376
|
+
end
|
377
|
+
def add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:); end
|
378
|
+
|
379
|
+
# sord omit - no YARD type given for "key:", using untyped
|
380
|
+
# sord omit - no YARD type given for "block_for:", using untyped
|
381
|
+
# sord omit - no YARD return type given, using untyped
|
382
|
+
sig { params(key: T.untyped, block_for: T.untyped).returns(T.untyped) }
|
383
|
+
def set_block(key:, block_for:); end
|
384
|
+
|
385
|
+
# sord omit - no YARD type given for "key:", using untyped
|
386
|
+
# sord omit - no YARD return type given, using untyped
|
387
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
388
|
+
def blocked_until(key:); end
|
389
|
+
|
390
|
+
# sord omit - no YARD return type given, using untyped
|
391
|
+
sig { returns(T.untyped) }
|
392
|
+
def prune; end
|
393
|
+
|
394
|
+
# sord omit - no YARD type given for "active_record_schema", using untyped
|
395
|
+
# sord omit - no YARD return type given, using untyped
|
396
|
+
sig { params(active_record_schema: T.untyped).returns(T.untyped) }
|
397
|
+
def create_tables(active_record_schema); end
|
398
|
+
end
|
399
|
+
|
400
|
+
class PostgresAdapter
|
401
|
+
# sord omit - no YARD type given for "model_class", using untyped
|
402
|
+
sig { params(model_class: T.untyped).void }
|
403
|
+
def initialize(model_class); end
|
404
|
+
|
405
|
+
# sord omit - no YARD type given for "key:", using untyped
|
406
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
407
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
408
|
+
# sord omit - no YARD return type given, using untyped
|
409
|
+
sig { params(key: T.untyped, capacity: T.untyped, leak_rate: T.untyped).returns(T.untyped) }
|
410
|
+
def state(key:, capacity:, leak_rate:); end
|
411
|
+
|
412
|
+
# sord omit - no YARD type given for "key:", using untyped
|
413
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
414
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
415
|
+
# sord omit - no YARD type given for "n_tokens:", using untyped
|
416
|
+
# sord omit - no YARD return type given, using untyped
|
417
|
+
sig do
|
418
|
+
params(
|
419
|
+
key: T.untyped,
|
420
|
+
capacity: T.untyped,
|
421
|
+
leak_rate: T.untyped,
|
422
|
+
n_tokens: T.untyped
|
423
|
+
).returns(T.untyped)
|
424
|
+
end
|
425
|
+
def add_tokens(key:, capacity:, leak_rate:, n_tokens:); end
|
426
|
+
|
427
|
+
# sord omit - no YARD type given for "key:", using untyped
|
428
|
+
# sord omit - no YARD type given for "capacity:", using untyped
|
429
|
+
# sord omit - no YARD type given for "leak_rate:", using untyped
|
430
|
+
# sord omit - no YARD type given for "n_tokens:", using untyped
|
431
|
+
# sord omit - no YARD return type given, using untyped
|
432
|
+
sig do
|
433
|
+
params(
|
434
|
+
key: T.untyped,
|
435
|
+
capacity: T.untyped,
|
436
|
+
leak_rate: T.untyped,
|
437
|
+
n_tokens: T.untyped
|
438
|
+
).returns(T.untyped)
|
439
|
+
end
|
440
|
+
def add_tokens_conditionally(key:, capacity:, leak_rate:, n_tokens:); end
|
441
|
+
|
442
|
+
# sord omit - no YARD type given for "key:", using untyped
|
443
|
+
# sord omit - no YARD type given for "block_for:", using untyped
|
444
|
+
# sord omit - no YARD return type given, using untyped
|
445
|
+
sig { params(key: T.untyped, block_for: T.untyped).returns(T.untyped) }
|
446
|
+
def set_block(key:, block_for:); end
|
447
|
+
|
448
|
+
# sord omit - no YARD type given for "key:", using untyped
|
449
|
+
# sord omit - no YARD return type given, using untyped
|
450
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
451
|
+
def blocked_until(key:); end
|
452
|
+
|
453
|
+
# sord omit - no YARD return type given, using untyped
|
454
|
+
sig { returns(T.untyped) }
|
455
|
+
def prune; end
|
456
|
+
|
457
|
+
# sord omit - no YARD type given for "active_record_schema", using untyped
|
458
|
+
# sord omit - no YARD return type given, using untyped
|
459
|
+
sig { params(active_record_schema: T.untyped).returns(T.untyped) }
|
460
|
+
def create_tables(active_record_schema); end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# Provides access to Pecorino blocks - same blocks which get set when a throttle triggers. The blocks
|
465
|
+
# are just keys in the data store which have an expiry value. This can be useful if you want to restrict
|
466
|
+
# access to a resource for an arbitrary timespan.
|
467
|
+
class Block
|
468
|
+
# Sets a block for the given key. The block will also be seen by the Pecorino::Throttle with the same key
|
469
|
+
#
|
470
|
+
# _@param_ `key` — the key to set the block for
|
471
|
+
#
|
472
|
+
# _@param_ `block_for` — the number of seconds or a time interval to block for
|
473
|
+
#
|
474
|
+
# _@param_ `adapter` — the adapter to set the value in.
|
475
|
+
#
|
476
|
+
# _@return_ — the time when the block will be released
|
477
|
+
sig { params(key: String, block_for: Float, adapter: Pecorino::Adapters::BaseAdapter).returns(Time) }
|
478
|
+
def self.set!(key:, block_for:, adapter: Pecorino.adapter); end
|
479
|
+
|
480
|
+
# Returns the time until a certain block is in effect
|
481
|
+
#
|
482
|
+
# _@param_ `key` — the key to get the expiry time for
|
483
|
+
#
|
484
|
+
# _@param_ `adapter` — the adapter to get the value from
|
485
|
+
#
|
486
|
+
# _@return_ — the time when the block will be released
|
487
|
+
sig { params(key: String, adapter: Pecorino::Adapters::BaseAdapter).returns(T.nilable(Time)) }
|
488
|
+
def self.blocked_until(key:, adapter: Pecorino.adapter); end
|
489
|
+
end
|
490
|
+
|
491
|
+
class Railtie < Rails::Railtie
|
492
|
+
end
|
493
|
+
|
494
|
+
# Provides a throttle with a block based on the `LeakyBucket`. Once a bucket fills up,
|
495
|
+
# a block will be installed and an exception will be raised. Once a block is set, no
|
496
|
+
# checks will be done on the leaky bucket - any further requests will be refused until
|
497
|
+
# the block is lifted. The block time can be arbitrarily higher or lower than the amount
|
498
|
+
# of time it takes for the leaky bucket to leak out
|
499
|
+
class Throttle
|
500
|
+
# _@param_ `key` — the key for both the block record and the leaky bucket
|
501
|
+
#
|
502
|
+
# _@param_ `block_for` — the number of seconds to block any further requests for. Defaults to time it takes the bucket to leak out to the level of 0
|
503
|
+
#
|
504
|
+
# _@param_ `adapter` — a compatible adapter
|
505
|
+
#
|
506
|
+
# _@param_ `leaky_bucket_options` — Options for `Pecorino::LeakyBucket.new`
|
507
|
+
#
|
508
|
+
# _@see_ `PecorinoLeakyBucket.new`
|
509
|
+
sig do
|
510
|
+
params(
|
511
|
+
key: String,
|
512
|
+
block_for: T.nilable(Numeric),
|
513
|
+
adapter: Pecorino::Adapters::BaseAdapter,
|
514
|
+
leaky_bucket_options: T.untyped
|
515
|
+
).void
|
516
|
+
end
|
517
|
+
def initialize(key:, block_for: nil, adapter: Pecorino.adapter, **leaky_bucket_options); end
|
518
|
+
|
519
|
+
# Tells whether the throttle will let this number of requests pass without raising
|
520
|
+
# a Throttled. Note that this is not race-safe. Another request could overflow the bucket
|
521
|
+
# after you call `able_to_accept?` but before you call `throttle!`. So before performing
|
522
|
+
# the action you still need to call `throttle!`. You may still use `able_to_accept?` to
|
523
|
+
# provide better UX to your users before they cause an action that would otherwise throttle.
|
524
|
+
#
|
525
|
+
# _@param_ `n_tokens`
|
526
|
+
sig { params(n_tokens: Float).returns(T::Boolean) }
|
527
|
+
def able_to_accept?(n_tokens = 1); end
|
528
|
+
|
529
|
+
# sord omit - no YARD type given for "n", using untyped
|
530
|
+
# Register that a request is being performed. Will raise Throttled
|
531
|
+
# if there is a block in place for that throttle, or if the bucket cannot accept
|
532
|
+
# this fillup and the block has just been installed as a result of this particular request.
|
533
|
+
#
|
534
|
+
# The exception can be rescued later to provide a 429 response. This method is better
|
535
|
+
# to use before performing the unit of work that the throttle is guarding:
|
536
|
+
#
|
537
|
+
# If the method call succeeds it means that the request is not getting throttled.
|
538
|
+
#
|
539
|
+
# _@return_ — the state of the throttle after filling up the leaky bucket / trying to pass the block
|
540
|
+
#
|
541
|
+
# ```ruby
|
542
|
+
# begin
|
543
|
+
# t.request!
|
544
|
+
# Note.create!(note_params)
|
545
|
+
# rescue Pecorino::Throttle::Throttled => e
|
546
|
+
# [429, {"Retry-After" => e.retry_after.to_s}, []]
|
547
|
+
# end
|
548
|
+
# ```
|
549
|
+
sig { params(n: T.untyped).returns(State) }
|
550
|
+
def request!(n = 1); end
|
551
|
+
|
552
|
+
# sord omit - no YARD type given for "n", using untyped
|
553
|
+
# Register that a request is being performed. Will not raise any exceptions but return
|
554
|
+
# the time at which the block will be lifted if a block resulted from this request or
|
555
|
+
# was already in effect. Can be used for registering actions which already took place,
|
556
|
+
# but should result in subsequent actions being blocked.
|
557
|
+
#
|
558
|
+
# _@return_ — the state of the throttle after filling up the leaky bucket / trying to pass the block
|
559
|
+
#
|
560
|
+
# ```ruby
|
561
|
+
# if t.able_to_accept?
|
562
|
+
# Entry.create!(entry_params)
|
563
|
+
# t.request
|
564
|
+
# end
|
565
|
+
# ```
|
566
|
+
sig { params(n: T.untyped).returns(State) }
|
567
|
+
def request(n = 1); end
|
568
|
+
|
569
|
+
# Fillup the throttle with 1 request and then perform the passed block. This is useful to perform actions which should
|
570
|
+
# be rate-limited - alerts, calls to external services and the like. If the call is allowed to proceed,
|
571
|
+
# the passed block will be executed. If the throttle is in the blocked state or if the call puts the throttle in
|
572
|
+
# the blocked state the block will not be executed
|
573
|
+
#
|
574
|
+
# _@return_ — the return value of the block if the block gets executed, or `nil` if the call got throttled
|
575
|
+
#
|
576
|
+
# ```ruby
|
577
|
+
# t.throttled { Slack.alert("Things are going wrong") }
|
578
|
+
# ```
|
579
|
+
sig { params(blk: T.untyped).returns(Object) }
|
580
|
+
def throttled(&blk); end
|
581
|
+
|
582
|
+
# The key for that throttle. Each key defines a unique throttle based on either a given name or
|
583
|
+
# discriminators. If there is a component you want to key your throttle by, include it in the
|
584
|
+
# `key` keyword argument to the constructor, like `"t-ip-#{request.ip}"`
|
585
|
+
sig { returns(String) }
|
586
|
+
attr_reader :key
|
587
|
+
|
588
|
+
# The state represents a snapshot of the throttle state in time
|
589
|
+
class State
|
590
|
+
# sord omit - no YARD type given for "blocked_until", using untyped
|
591
|
+
sig { params(blocked_until: T.untyped).void }
|
592
|
+
def initialize(blocked_until); end
|
593
|
+
|
594
|
+
# Tells whether this throttle still is in the blocked state.
|
595
|
+
# If the `blocked_until` value lies in the past, the method will
|
596
|
+
# return `false` - this is done so that the `State` can be cached.
|
597
|
+
sig { returns(T::Boolean) }
|
598
|
+
def blocked?; end
|
599
|
+
|
600
|
+
sig { returns(Time) }
|
601
|
+
attr_reader :blocked_until
|
602
|
+
end
|
603
|
+
|
604
|
+
# {Pecorino::Throttle} will raise this exception from `request!`. The exception can be used
|
605
|
+
# to do matching, for setting appropriate response headers, and for distinguishing between
|
606
|
+
# multiple different throttles.
|
607
|
+
class Throttled < StandardError
|
608
|
+
# sord omit - no YARD type given for "from_throttle", using untyped
|
609
|
+
# sord omit - no YARD type given for "state", using untyped
|
610
|
+
sig { params(from_throttle: T.untyped, state: T.untyped).void }
|
611
|
+
def initialize(from_throttle, state); end
|
612
|
+
|
613
|
+
# Returns the `retry_after` value in seconds, suitable for use in an HTTP header
|
614
|
+
sig { returns(Integer) }
|
615
|
+
def retry_after; end
|
616
|
+
|
617
|
+
# Returns the throttle which raised the exception. Can be used to disambiguiate between
|
618
|
+
# multiple Throttled exceptions when multiple throttles are applied in a layered fashion:
|
619
|
+
#
|
620
|
+
# ```ruby
|
621
|
+
# begin
|
622
|
+
# ip_addr_throttle.request!
|
623
|
+
# user_email_throttle.request!
|
624
|
+
# db_insert_throttle.request!(n_items_to_insert)
|
625
|
+
# rescue Pecorino::Throttled => e
|
626
|
+
# deliver_notification(user) if e.throttle == user_email_throttle
|
627
|
+
# firewall.ban_ip(ip) if e.throttle == ip_addr_throttle
|
628
|
+
# end
|
629
|
+
# ```
|
630
|
+
sig { returns(Throttle) }
|
631
|
+
attr_reader :throttle
|
632
|
+
|
633
|
+
# Returns the throttle state based on which the exception is getting raised. This can
|
634
|
+
# be used for caching the exception, because the state can tell when the block will be
|
635
|
+
# lifted. This can be used to shift the throttle verification into a faster layer of the
|
636
|
+
# system (like a blocklist in a firewall) or caching the state in an upstream cache. A block
|
637
|
+
# in Pecorino is set once and is active until expiry. If your service is under an attack
|
638
|
+
# and you know that the call is blocked until a certain future time, the block can be
|
639
|
+
# lifted up into a faster/cheaper storage destination, like Rails cache:
|
640
|
+
#
|
641
|
+
# ```ruby
|
642
|
+
# begin
|
643
|
+
# ip_addr_throttle.request!
|
644
|
+
# rescue Pecorino::Throttled => e
|
645
|
+
# firewall.ban_ip(request.ip, ttl_seconds: e.state.retry_after)
|
646
|
+
# render :rate_limit_exceeded
|
647
|
+
# end
|
648
|
+
# ```
|
649
|
+
#
|
650
|
+
# ```ruby
|
651
|
+
# state = Rails.cache.read(ip_addr_throttle.key)
|
652
|
+
# return render :rate_limit_exceeded if state && state.blocked? # No need to call Pecorino for this
|
653
|
+
#
|
654
|
+
# begin
|
655
|
+
# ip_addr_throttle.request!
|
656
|
+
# rescue Pecorino::Throttled => e
|
657
|
+
# Rails.cache.write(ip_addr_throttle.key, e.state, expires_in: (e.state.blocked_until - Time.now))
|
658
|
+
# render :rate_limit_exceeded
|
659
|
+
# end
|
660
|
+
# ```
|
661
|
+
sig { returns(Throttle::State) }
|
662
|
+
attr_reader :state
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
# This offers just the leaky bucket implementation with fill control, but without the timed lock.
|
667
|
+
# It does not raise any exceptions, it just tracks the state of a leaky bucket in the database.
|
668
|
+
#
|
669
|
+
# Leak rate is specified directly in tokens per second, instead of specifying the block period.
|
670
|
+
# The bucket level is stored and returned as a Float which allows for finer-grained measurement,
|
671
|
+
# but more importantly - makes testing from the outside easier.
|
672
|
+
#
|
673
|
+
# Note that this implementation has a peculiar property: the bucket is only "full" once it overflows.
|
674
|
+
# Due to a leak rate just a few microseconds after that moment the bucket is no longer going to be full
|
675
|
+
# anymore as it will have leaked some tokens by then. This means that the information about whether a
|
676
|
+
# bucket has become full or not gets returned in the bucket `State` struct right after the database
|
677
|
+
# update gets executed, and if your code needs to make decisions based on that data it has to use
|
678
|
+
# this returned state, not query the leaky bucket again. Specifically:
|
679
|
+
#
|
680
|
+
# state = bucket.fillup(1) # Record 1 request
|
681
|
+
# state.full? #=> true, this is timely information
|
682
|
+
#
|
683
|
+
# ...is the correct way to perform the check. This, however, is not:
|
684
|
+
#
|
685
|
+
# bucket.fillup(1)
|
686
|
+
# bucket.state.full? #=> false, some time has passed after the topup and some tokens have already leaked
|
687
|
+
#
|
688
|
+
# The storage use is one DB row per leaky bucket you need to manage (likely - one throttled entity such
|
689
|
+
# as a combination of an IP address + the URL you need to procect). The `key` is an arbitrary string you provide.
|
690
|
+
class LeakyBucket
|
691
|
+
# sord duck - #to_f looks like a duck type, replacing with untyped
|
692
|
+
# Creates a new LeakyBucket. The object controls 1 row in the database is
|
693
|
+
# specific to the bucket key.
|
694
|
+
#
|
695
|
+
# _@param_ `key` — the key for the bucket. The key also gets used to derive locking keys, so that operations on a particular bucket are always serialized.
|
696
|
+
#
|
697
|
+
# _@param_ `leak_rate` — the leak rate of the bucket, in tokens per second. Either `leak_rate` or `over_time` can be used, but not both.
|
698
|
+
#
|
699
|
+
# _@param_ `over_time` — over how many seconds the bucket will leak out to 0 tokens. The value is assumed to be the number of seconds - or a duration which returns the number of seconds from `to_f`. Either `leak_rate` or `over_time` can be used, but not both.
|
700
|
+
#
|
701
|
+
# _@param_ `capacity` — how many tokens is the bucket capped at. Filling up the bucket using `fillup()` will add to that number, but the bucket contents will then be capped at this value. So with bucket_capacity set to 12 and a `fillup(14)` the bucket will reach the level of 12, and will then immediately start leaking again.
|
702
|
+
#
|
703
|
+
# _@param_ `adapter` — a compatible adapter
|
704
|
+
sig do
|
705
|
+
params(
|
706
|
+
key: String,
|
707
|
+
capacity: Numeric,
|
708
|
+
adapter: Pecorino::Adapters::BaseAdapter,
|
709
|
+
leak_rate: T.nilable(Float),
|
710
|
+
over_time: T.untyped
|
711
|
+
).void
|
712
|
+
end
|
713
|
+
def initialize(key:, capacity:, adapter: Pecorino.adapter, leak_rate: nil, over_time: nil); end
|
714
|
+
|
715
|
+
# Places `n` tokens in the bucket. If the bucket has less capacity than `n` tokens, the bucket will be filled to capacity.
|
716
|
+
# If the bucket has less capacity than `n` tokens, it will be filled to capacity. If the bucket is already full
|
717
|
+
# when the fillup is requested, the bucket stays at capacity.
|
718
|
+
#
|
719
|
+
# Once tokens are placed, the bucket is set to expire within 2 times the time it would take it to leak to 0,
|
720
|
+
# regardless of how many tokens get put in - since the amount of tokens put in the bucket will always be capped
|
721
|
+
# to the `capacity:` value you pass to the constructor.
|
722
|
+
#
|
723
|
+
# _@param_ `n_tokens` — How many tokens to fillup by
|
724
|
+
#
|
725
|
+
# _@return_ — the state of the bucket after the operation
|
726
|
+
sig { params(n_tokens: Float).returns(State) }
|
727
|
+
def fillup(n_tokens); end
|
728
|
+
|
729
|
+
# Places `n` tokens in the bucket. If the bucket has less capacity than `n` tokens, the fillup will be rejected.
|
730
|
+
# This can be used for "exactly once" semantics or just more precise rate limiting. Note that if the bucket has
|
731
|
+
# _exactly_ `n` tokens of capacity the fillup will be accepted.
|
732
|
+
#
|
733
|
+
# Once tokens are placed, the bucket is set to expire within 2 times the time it would take it to leak to 0,
|
734
|
+
# regardless of how many tokens get put in - since the amount of tokens put in the bucket will always be capped
|
735
|
+
# to the `capacity:` value you pass to the constructor.
|
736
|
+
#
|
737
|
+
# _@param_ `n_tokens` — How many tokens to fillup by
|
738
|
+
#
|
739
|
+
# _@return_ — the state of the bucket after the operation and whether the operation succeeded
|
740
|
+
#
|
741
|
+
# ```ruby
|
742
|
+
# withdrawals = LeakyBuket.new(key: "wallet-#{user.id}", capacity: 200, over_time: 1.day)
|
743
|
+
# if withdrawals.fillup_conditionally(amount_to_withdraw).accepted?
|
744
|
+
# user.wallet.withdraw(amount_to_withdraw)
|
745
|
+
# else
|
746
|
+
# raise "You need to wait a bit before withdrawing more"
|
747
|
+
# end
|
748
|
+
# ```
|
749
|
+
sig { params(n_tokens: Float).returns(ConditionalFillupResult) }
|
750
|
+
def fillup_conditionally(n_tokens); end
|
751
|
+
|
752
|
+
# Returns the current state of the bucket, containing the level and whether the bucket is full.
|
753
|
+
# Calling this method will not perform any database writes.
|
754
|
+
#
|
755
|
+
# _@return_ — the snapshotted state of the bucket at time of query
|
756
|
+
sig { returns(State) }
|
757
|
+
def state; end
|
758
|
+
|
759
|
+
# Tells whether the bucket can accept the amount of tokens without overflowing.
|
760
|
+
# Calling this method will not perform any database writes. Note that this call is
|
761
|
+
# not race-safe - another caller may still overflow the bucket. Before performing
|
762
|
+
# your action, you still need to call `fillup()` - but you can preemptively refuse
|
763
|
+
# a request if you already know the bucket is full.
|
764
|
+
#
|
765
|
+
# _@param_ `n_tokens`
|
766
|
+
sig { params(n_tokens: Float).returns(T::Boolean) }
|
767
|
+
def able_to_accept?(n_tokens); end
|
768
|
+
|
769
|
+
# sord omit - no YARD type given for :key, using untyped
|
770
|
+
# The key (name) of the leaky bucket
|
771
|
+
# @return [String]
|
772
|
+
sig { returns(T.untyped) }
|
773
|
+
attr_reader :key
|
774
|
+
|
775
|
+
# sord omit - no YARD type given for :leak_rate, using untyped
|
776
|
+
# The leak rate (tokens per second) of the bucket
|
777
|
+
# @return [Float]
|
778
|
+
sig { returns(T.untyped) }
|
779
|
+
attr_reader :leak_rate
|
780
|
+
|
781
|
+
# sord omit - no YARD type given for :capacity, using untyped
|
782
|
+
# The capacity of the bucket in tokens
|
783
|
+
# @return [Float]
|
784
|
+
sig { returns(T.untyped) }
|
785
|
+
attr_reader :capacity
|
786
|
+
|
787
|
+
# Returned from `.state` and `.fillup`
|
788
|
+
class State
|
789
|
+
# sord omit - no YARD type given for "level", using untyped
|
790
|
+
# sord omit - no YARD type given for "is_full", using untyped
|
791
|
+
sig { params(level: T.untyped, is_full: T.untyped).void }
|
792
|
+
def initialize(level, is_full); end
|
793
|
+
|
794
|
+
# Tells whether the bucket was detected to be full when the operation on
|
795
|
+
# the LeakyBucket was performed.
|
796
|
+
sig { returns(T::Boolean) }
|
797
|
+
def full?; end
|
798
|
+
|
799
|
+
# Returns the level of the bucket
|
800
|
+
sig { returns(Float) }
|
801
|
+
attr_reader :level
|
802
|
+
end
|
803
|
+
|
804
|
+
# Same as `State` but also communicates whether the write has been permitted or not. A conditional fillup
|
805
|
+
# may refuse a write if it would make the bucket overflow
|
806
|
+
class ConditionalFillupResult < Pecorino::LeakyBucket::State
|
807
|
+
# sord omit - no YARD type given for "level", using untyped
|
808
|
+
# sord omit - no YARD type given for "is_full", using untyped
|
809
|
+
# sord omit - no YARD type given for "accepted", using untyped
|
810
|
+
sig { params(level: T.untyped, is_full: T.untyped, accepted: T.untyped).void }
|
811
|
+
def initialize(level, is_full, accepted); end
|
812
|
+
|
813
|
+
# Tells whether the bucket did accept the requested fillup
|
814
|
+
sig { returns(T::Boolean) }
|
815
|
+
def accepted?; end
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
# The cached throttles can be used when you want to lift your throttle blocks into
|
820
|
+
# a higher-level cache. If you are dealing with clients which are hammering on your
|
821
|
+
# throttles a lot, it is useful to have a process-local cache of the timestamp when
|
822
|
+
# the blocks that are set are going to expire. If you are running, say, 10 web app
|
823
|
+
# containers - and someone is hammering at an endpoint which starts blocking -
|
824
|
+
# you don't really need to query your DB for every request. The first request indicated
|
825
|
+
# as "blocked" by Pecorino can write a cache entry into a shared in-memory table,
|
826
|
+
# and all subsequent calls to the same process can reuse that `blocked_until` value
|
827
|
+
# to quickly refuse the request
|
828
|
+
class CachedThrottle
|
829
|
+
# sord warn - ActiveSupport::Cache::Store wasn't able to be resolved to a constant in this project
|
830
|
+
# _@param_ `cache_store` — the store for the cached blocks. We recommend a MemoryStore per-process.
|
831
|
+
#
|
832
|
+
# _@param_ `throttle` — the throttle to cache
|
833
|
+
sig { params(cache_store: ActiveSupport::Cache::Store, throttle: Pecorino::Throttle).void }
|
834
|
+
def initialize(cache_store, throttle); end
|
835
|
+
|
836
|
+
# sord omit - no YARD type given for "n", using untyped
|
837
|
+
# sord omit - no YARD return type given, using untyped
|
838
|
+
#
|
839
|
+
# _@see_ `Pecorino::Throttle#request!`
|
840
|
+
sig { params(n: T.untyped).returns(T.untyped) }
|
841
|
+
def request!(n = 1); end
|
842
|
+
|
843
|
+
# sord omit - no YARD type given for "n", using untyped
|
844
|
+
# sord omit - no YARD return type given, using untyped
|
845
|
+
# Returns cached `state` for the throttle if there is a currently active block for that throttle in the cache. Otherwise forwards to underlying throttle.
|
846
|
+
#
|
847
|
+
# _@see_ `Pecorino::Throttle#request`
|
848
|
+
sig { params(n: T.untyped).returns(T.untyped) }
|
849
|
+
def request(n = 1); end
|
850
|
+
|
851
|
+
# sord omit - no YARD type given for "n", using untyped
|
852
|
+
# Returns `false` if there is a currently active block for that throttle in the cache. Otherwise forwards to underlying throttle.
|
853
|
+
#
|
854
|
+
# _@see_ `Pecorino::Throttle#able_to_accept?`
|
855
|
+
sig { params(n: T.untyped).returns(T::Boolean) }
|
856
|
+
def able_to_accept?(n = 1); end
|
857
|
+
|
858
|
+
# sord omit - no YARD return type given, using untyped
|
859
|
+
# Does not run the block if there is a currently active block for that throttle in the cache. Otherwise forwards to underlying throttle.
|
860
|
+
#
|
861
|
+
# _@see_ `Pecorino::Throttle#throttled`
|
862
|
+
sig { params(blk: T.untyped).returns(T.untyped) }
|
863
|
+
def throttled(&blk); end
|
864
|
+
|
865
|
+
# sord omit - no YARD return type given, using untyped
|
866
|
+
# Returns the key of the throttle
|
867
|
+
#
|
868
|
+
# _@see_ `Pecorino::Throttle#key`
|
869
|
+
sig { returns(T.untyped) }
|
870
|
+
def key; end
|
871
|
+
|
872
|
+
# sord omit - no YARD return type given, using untyped
|
873
|
+
# Returns `false` if there is a currently active block for that throttle in the cache. Otherwise forwards to underlying throttle.
|
874
|
+
#
|
875
|
+
# _@see_ `Pecorino::Throttle#able_to_accept?`
|
876
|
+
sig { returns(T.untyped) }
|
877
|
+
def state; end
|
878
|
+
|
879
|
+
# sord omit - no YARD type given for "state", using untyped
|
880
|
+
# sord omit - no YARD return type given, using untyped
|
881
|
+
sig { params(state: T.untyped).returns(T.untyped) }
|
882
|
+
def write_cache_blocked_state(state); end
|
883
|
+
|
884
|
+
# sord omit - no YARD return type given, using untyped
|
885
|
+
sig { returns(T.untyped) }
|
886
|
+
def read_cached_blocked_state; end
|
887
|
+
end
|
888
|
+
|
889
|
+
#
|
890
|
+
# Rails generator used for setting up Pecorino in a Rails application.
|
891
|
+
# Run it with +bin/rails g pecorino:install+ in your console.
|
892
|
+
class InstallGenerator < Rails::Generators::Base
|
893
|
+
include ActiveRecord::Generators::Migration
|
894
|
+
TEMPLATES = T.let(File.join(File.dirname(__FILE__)), T.untyped)
|
895
|
+
|
896
|
+
# sord omit - no YARD return type given, using untyped
|
897
|
+
# Generates monolithic migration file that contains all database changes.
|
898
|
+
sig { returns(T.untyped) }
|
899
|
+
def create_migration_file; end
|
900
|
+
|
901
|
+
# sord omit - no YARD return type given, using untyped
|
902
|
+
sig { returns(T.untyped) }
|
903
|
+
def migration_version; end
|
904
|
+
end
|
905
|
+
end
|