Skip to content

Commit

Permalink
Add naked Block
Browse files Browse the repository at this point in the history
for blocking for an arbitrary timespan
  • Loading branch information
julik committed Mar 12, 2024
1 parent f900938 commit 35fd837
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 12 deletions.
16 changes: 10 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
## [0.5.0] - 2024-02-11
## 0.6.0

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

## 0.5.0

- Add `CachedThrottle` for caching the throttle blocks. This allows protection to the database when the throttle is in a blocked state.
- Add `Throttle#throttled` for silencing alerts
- **BREAKING CHANGE** Remove `Throttle::State#retry_after`, because there is no reasonable value for that member if the throttle is not in the "blocked" state
- Allow accessing `Throttle::State` from the `Throttled` exception so that the blocked throttle state can be cached downstream (in Rails cache, for example)
- Make `Throttle#request!` return the new state if there was no exception raised

## [0.4.1] - 2024-02-11
## 0.4.1

- Make sure Pecorino works on Ruby 2.7 as well by removing 3.x-exclusive syntax

## [0.4.0] - 2024-01-22
## 0.4.0

- Use Bucket#connditional_fillup inside Throttle and throttle only when the capacity _would_ be exceeded, as opposed
to throttling when capacity has already been exceeded. This allows for finer-grained throttles such as
Expand All @@ -21,17 +25,17 @@
- Allow "conditional fillup" - only add tokens to the leaky bucket if the bucket has enough space.
- Fix `over_time` leading to incorrect `leak_rate`. The divider/divisor were swapped, leading to the inverse leak rate getting computed.

## [0.3.0] - 2024-01-18
## 0.3.0

- Allow `over_time` in addition to `leak_rate`, which is a more intuitive parameter to tweak
- Set default `block_for` to the time it takes the bucket to leak out completely instead of 30 seconds

## [0.2.0] - 2024-01-09
## 0.2.0

- [Add support for SQLite](https://github.com/cheddar-me/pecorino/pull/9)
- [Use comparisons in SQL to determine whether the leaky bucket did overflow](https://github.com/cheddar-me/pecorino/pull/8)
- [Change the way Structs are defined to appease Tapioca/Sorbet](https://github.com/cheddar-me/pecorino/pull/6)

## [0.1.0] - 2023-10-30
## 0.1.0

- Initial release
7 changes: 4 additions & 3 deletions lib/pecorino.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
require "active_record/sanitization"

require_relative "pecorino/version"
require_relative "pecorino/leaky_bucket"
require_relative "pecorino/throttle"
require_relative "pecorino/railtie" if defined?(Rails::Railtie)
require_relative "pecorino/cached_throttle"

module Pecorino
autoload :Postgres, "pecorino/postgres"
autoload :Sqlite, "pecorino/sqlite"
autoload :LeakyBucket, "pecorino/leaky_bucket"
autoload :Block, "pecorino/block"
autoload :Throttle, "pecorino/throttle"
autoload :CachedThrottle, "pecorino/cached_throttle"

# Deletes stale leaky buckets and blocks which have expired. Run this method regularly to
# avoid accumulating too many unused rows in your tables.
Expand Down
24 changes: 24 additions & 0 deletions lib/pecorino/block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

# Provides access to Pecorino blocks - same blocks which get set when a throttle triggers. The blocks
# are just keys in the data store which have an expiry value. This can be useful if you want to restrict
# access to a resource for an arbitrary timespan.
class Pecorino::Block
# Sets a block for the given key. The block will also be seen by the Pecorino::Throttle with the same key
#
# @param key[String] the key to set the block for
# @param block_for[Float] the number of seconds or a time interval to block for
# @return [Time] the time when the block will be released
def self.set!(key:, block_for:)
Pecorino.adapter.set_block(key: key, block_for: block_for)
Time.now + block_for
end

# Returns the time until a certain block is in effect
#
# @return [Time,nil] the time when the block will be released
def self.blocked_until(key:)
t = Pecorino.adapter.blocked_until(key: key)
(t && t > Time.now) ? t : nil
end
end
4 changes: 2 additions & 2 deletions lib/pecorino/throttle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def request!(n = 1)
#
# @return [State] the state of the throttle after filling up the leaky bucket / trying to pass the block
def request(n = 1)
existing_blocked_until = Pecorino.adapter.blocked_until(key: @key)
existing_blocked_until = Pecorino::Block.blocked_until(key: @key)
return State.new(existing_blocked_until.utc) if existing_blocked_until

# Topup the leaky bucket, and if the topup gets rejected - block the caller
Expand All @@ -165,7 +165,7 @@ def request(n = 1)
State.new(nil)
else
# and set the block if the fillup was rejected
fresh_blocked_until = Pecorino.adapter.set_block(key: @key, block_for: @block_for)
fresh_blocked_until = Pecorino::Block.set!(key: @key, block_for: @block_for)
State.new(fresh_blocked_until.utc)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/pecorino/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Pecorino
VERSION = "0.5.0"
VERSION = "0.6.0"
end
31 changes: 31 additions & 0 deletions test/block_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require "test_helper"

class BlockTest < ActiveSupport::TestCase
def setup
create_postgres_database
end

def teardown
drop_postgres_database
end

test "sets a block" do
k = Base64.strict_encode64(Random.bytes(4))
assert_nil Pecorino::Block.blocked_until(key: k)
assert Pecorino::Block.set!(key: k, block_for: 30.minutes)

blocked_until = Pecorino::Block.blocked_until(key: k)
assert_in_delta Time.now + 30.minutes, blocked_until, 10
end

test "does not return a block which has lapsed" do
k = Base64.strict_encode64(Random.bytes(4))
assert_nil Pecorino::Block.blocked_until(key: k)
assert Pecorino::Block.set!(key: k, block_for: -30.minutes)

blocked_until = Pecorino::Block.blocked_until(key: k)
assert_nil blocked_until
end
end

0 comments on commit 35fd837

Please sign in to comment.