pecorino 0.7.1 → 0.7.3

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