pecorino 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cd65abe4917de817a0b0b08672376d5a7dd1e16c01b3a4e0c47ff1467600a1e
4
- data.tar.gz: 58ca1578813b7a5bcc058d9a37a869e778a1614b736becced4bc82f78efba2c5
3
+ metadata.gz: fbbf2138b0295606f7a2a217aa41f30bcd1d1b994a9e079f810b5c0088045714
4
+ data.tar.gz: b1b594fbdbeb1d1e4f2019a50869b0ba038eb161d4385d36707e5b74d82eedb6
5
5
  SHA512:
6
- metadata.gz: bbbcdc936bef119b1b02695dc626f8421576a619414f4493a541876562ca3cf5136dcdd1a796c9c08cd6cc2a8345be8ac7fecd8f6371fddc9b480bd26d017296
7
- data.tar.gz: cd4e2ba40164eb0c9f56e17aeef656727703cb98f7e70cedaafa3d9af9202195f9c0e1a1dbc3dfdb411f277f31e84a6a2641d5f81bf85d6d07b3b92937877cbd
6
+ metadata.gz: 4b3b0ba688ae6b04d85943b03da908c140db2d2ec7a326da8e87a54e4fa6a077a7b36d4a8ba29a9ed051762d771c036d0f3d2599c8d48d40f5073810a5524234
7
+ data.tar.gz: cc7b1d692935a8af49ec479fd3c5c98ea65749879ecaf22addeaeb285eedf37406df755c6dfa17d379d6cbcb07bc96150573b1262bbb98d4509ba1170fc195d9
data/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
- ## [0.5.0] - 2024-02-11
1
+ ## 0.6.0
2
+
3
+ - Add `Pecorino::Block` for setting blocks directly. These are available both to `Throttle` with the same key and on their own. This can be used to set arbitrary blocks without having to configure a `Throttle` first.
4
+
5
+ ## 0.5.0
2
6
 
3
7
  - Add `CachedThrottle` for caching the throttle blocks. This allows protection to the database when the throttle is in a blocked state.
4
8
  - Add `Throttle#throttled` for silencing alerts
@@ -6,11 +10,11 @@
6
10
  - Allow accessing `Throttle::State` from the `Throttled` exception so that the blocked throttle state can be cached downstream (in Rails cache, for example)
7
11
  - Make `Throttle#request!` return the new state if there was no exception raised
8
12
 
9
- ## [0.4.1] - 2024-02-11
13
+ ## 0.4.1
10
14
 
11
15
  - Make sure Pecorino works on Ruby 2.7 as well by removing 3.x-exclusive syntax
12
16
 
13
- ## [0.4.0] - 2024-01-22
17
+ ## 0.4.0
14
18
 
15
19
  - Use Bucket#connditional_fillup inside Throttle and throttle only when the capacity _would_ be exceeded, as opposed
16
20
  to throttling when capacity has already been exceeded. This allows for finer-grained throttles such as
@@ -21,17 +25,17 @@
21
25
  - Allow "conditional fillup" - only add tokens to the leaky bucket if the bucket has enough space.
22
26
  - Fix `over_time` leading to incorrect `leak_rate`. The divider/divisor were swapped, leading to the inverse leak rate getting computed.
23
27
 
24
- ## [0.3.0] - 2024-01-18
28
+ ## 0.3.0
25
29
 
26
30
  - Allow `over_time` in addition to `leak_rate`, which is a more intuitive parameter to tweak
27
31
  - Set default `block_for` to the time it takes the bucket to leak out completely instead of 30 seconds
28
32
 
29
- ## [0.2.0] - 2024-01-09
33
+ ## 0.2.0
30
34
 
31
35
  - [Add support for SQLite](https://github.com/cheddar-me/pecorino/pull/9)
32
36
  - [Use comparisons in SQL to determine whether the leaky bucket did overflow](https://github.com/cheddar-me/pecorino/pull/8)
33
37
  - [Change the way Structs are defined to appease Tapioca/Sorbet](https://github.com/cheddar-me/pecorino/pull/6)
34
38
 
35
- ## [0.1.0] - 2023-10-30
39
+ ## 0.1.0
36
40
 
37
41
  - Initial release
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Pecorino
2
2
 
3
- Pecorino is a rate limiter based on the concept of leaky buckets. It uses your DB as the storage backend for the throttles. It is compact, easy to install, and does not require additional infrastructure. The approach used by Pecorino has been previously used by [prorate](https://github.com/WeTransfer/prorate) with Redis, and that approach has proven itself.
3
+ Pecorino is a rate limiter based on the concept of leaky buckets, or more specifically - based on the [generic cell rate](https://brandur.org/rate-limiting) algorithm. It uses your DB as the storage backend for the throttles. It is compact, easy to install, and does not require additional infrastructure. The approach used by Pecorino has been previously used by [prorate](https://github.com/WeTransfer/prorate) with Redis, and that approach has proven itself.
4
4
 
5
5
  Pecorino is designed to integrate seamlessly into any Rails application using a PostgreSQL or SQLite database (at the moment there is no MySQL support, we would be delighted if you could add it).
6
6
 
7
- If you would like to know more about the leaky bucket algorithm: [this article](http://live.julik.nl/2022/08/the-unreasonable-effectiveness-of-leaky-buckets) or the [Wikipedia article](https://en.wikipedia.org/wiki/Leaky_bucket) are both good starting points.
7
+ If you would like to know more about the leaky bucket algorithm: [this article](http://live.julik.nl/2022/08/the-unreasonable-effectiveness-of-leaky-buckets) or the [Wikipedia article](https://en.wikipedia.org/wiki/Leaky_bucket) are both good starting points. [This Wikipedia article](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm) describes the generic cell rate algorithm in more detail as well.
8
8
 
9
9
  ## Installation
10
10
 
@@ -103,6 +103,58 @@ end
103
103
 
104
104
  This way, every time there is an error on the "fancy AI service" the throttle will be triggered, and if it overflows - a subsequent request will be blocked.
105
105
 
106
+ ## A note on database transactions
107
+
108
+ Pecorino uses your main database. When calling the `Throttle` or `LeakyBucket` objects, SQL queries will be performed by Pecorino and those queries may result in changes to data. If you are currently inside a database transaction, your bucket topups or set blocks may get reverted. For example, imagine you have a controller like this:
109
+
110
+ ```ruby
111
+ class WalletController < ApplicationController
112
+ rescue_from Pecorino::Throttle::Throttled do |e|
113
+ response.set_header('Retry-After', e.retry_after.to_s)
114
+ render nothing: true, status: 429
115
+ end
116
+
117
+ def withdraw
118
+ Wallet.transaction do
119
+ t = Pecorino::Throttle.new("wallet_#{current_user.id}_max_withdrawal", capacity: 200_00, over_time: 5.minutes)
120
+ t.request!(10_00)
121
+ current_user.wallet.withdraw(Money.new(10, "EUR"))
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ what will happen is that even though the `withdraw()` call is not going to be performed, the increment of the throttle will not either, because the exception will result in a `ROLLBACK`.
128
+
129
+ If you need to use Pecorino in combination with transactions, you will need to design with that in mind. Either call `Throttle` before entering the `transaction do`:
130
+
131
+ ```ruby
132
+ def withdraw
133
+ t = Pecorino::Throttle.new("wallet_#{current_user.id}_max_withdrawal", capacity: 200_00, over_time: 5.minutes)
134
+ t.request!(10_00)
135
+ Wallet.transaction do
136
+ current_user.wallet.withdraw(Money.new(10, "EUR"))
137
+ end
138
+ end
139
+ ```
140
+
141
+ or use the `request()` method instead to still commit:
142
+
143
+ ```ruby
144
+ def withdraw
145
+ Wallet.transaction do
146
+ t = Pecorino::Throttle.new("wallet_#{current_user.id}_max_withdrawal", capacity: 200_00, over_time: 5.minutes)
147
+ throttle_state = t.request(10_00)
148
+ return render(nothing: true, status: 429) if throttle_state.blocked?
149
+
150
+ current_user.wallet.withdraw(Money.new(10, "EUR"))
151
+ end
152
+ end
153
+ ```
154
+
155
+ Note also that this behaviour might be desirable for your use case (that the throttle and the data update together in
156
+ a transactional manner) – it just helps to be aware of it.
157
+
106
158
  ## Using just the leaky bucket
107
159
 
108
160
  Sometimes you don't want to use a throttle, but you want to track the amount added to the leaky bucket over time. A lower-level abstraction is available for that purpose in the form of the `LeakyBucket` class. It will not raise any exceptions and will not install blocks, but will permit you to track a bucket's state over time:
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides access to Pecorino blocks - same blocks which get set when a throttle triggers. The blocks
4
+ # are just keys in the data store which have an expiry value. This can be useful if you want to restrict
5
+ # access to a resource for an arbitrary timespan.
6
+ class Pecorino::Block
7
+ # Sets a block for the given key. The block will also be seen by the Pecorino::Throttle with the same key
8
+ #
9
+ # @param key[String] the key to set the block for
10
+ # @param block_for[Float] the number of seconds or a time interval to block for
11
+ # @return [Time] the time when the block will be released
12
+ def self.set!(key:, block_for:)
13
+ Pecorino.adapter.set_block(key: key, block_for: block_for)
14
+ Time.now + block_for
15
+ end
16
+
17
+ # Returns the time until a certain block is in effect
18
+ #
19
+ # @return [Time,nil] the time when the block will be released
20
+ def self.blocked_until(key:)
21
+ t = Pecorino.adapter.blocked_until(key: key)
22
+ (t && t > Time.now) ? t : nil
23
+ end
24
+ end
@@ -156,7 +156,7 @@ class Pecorino::Throttle
156
156
  #
157
157
  # @return [State] the state of the throttle after filling up the leaky bucket / trying to pass the block
158
158
  def request(n = 1)
159
- existing_blocked_until = Pecorino.adapter.blocked_until(key: @key)
159
+ existing_blocked_until = Pecorino::Block.blocked_until(key: @key)
160
160
  return State.new(existing_blocked_until.utc) if existing_blocked_until
161
161
 
162
162
  # Topup the leaky bucket, and if the topup gets rejected - block the caller
@@ -165,7 +165,7 @@ class Pecorino::Throttle
165
165
  State.new(nil)
166
166
  else
167
167
  # and set the block if the fillup was rejected
168
- fresh_blocked_until = Pecorino.adapter.set_block(key: @key, block_for: @block_for)
168
+ fresh_blocked_until = Pecorino::Block.set!(key: @key, block_for: @block_for)
169
169
  State.new(fresh_blocked_until.utc)
170
170
  end
171
171
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pecorino
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/pecorino.rb CHANGED
@@ -4,14 +4,15 @@ require "active_support/concern"
4
4
  require "active_record/sanitization"
5
5
 
6
6
  require_relative "pecorino/version"
7
- require_relative "pecorino/leaky_bucket"
8
- require_relative "pecorino/throttle"
9
7
  require_relative "pecorino/railtie" if defined?(Rails::Railtie)
10
- require_relative "pecorino/cached_throttle"
11
8
 
12
9
  module Pecorino
13
10
  autoload :Postgres, "pecorino/postgres"
14
11
  autoload :Sqlite, "pecorino/sqlite"
12
+ autoload :LeakyBucket, "pecorino/leaky_bucket"
13
+ autoload :Block, "pecorino/block"
14
+ autoload :Throttle, "pecorino/throttle"
15
+ autoload :CachedThrottle, "pecorino/cached_throttle"
15
16
 
16
17
  # Deletes stale leaky buckets and blocks which have expired. Run this method regularly to
17
18
  # avoid accumulating too many unused rows in your tables.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pecorino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-11 00:00:00.000000000 Z
11
+ date: 2024-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -154,6 +154,7 @@ files:
154
154
  - README.md
155
155
  - Rakefile
156
156
  - lib/pecorino.rb
157
+ - lib/pecorino/block.rb
157
158
  - lib/pecorino/cached_throttle.rb
158
159
  - lib/pecorino/install_generator.rb
159
160
  - lib/pecorino/leaky_bucket.rb