-
Notifications
You must be signed in to change notification settings - Fork 335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor: less-spiky throttles #578
Open
jamiemccarthy
wants to merge
19
commits into
rack:main
Choose a base branch
from
jamiemccarthy:jm-random-period-offset
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+203
−34
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
fc58d6e
test: private method replaces Time.now
jamiemccarthy 146eeea
refactor: retry_after in match_data
jamiemccarthy 6bbb758
refactor: offset_for, offset into period, at 0
jamiemccarthy c96112b
refactor: new use_offset argument
jamiemccarthy 86deb35
test: add offset tests
jamiemccarthy 2a25360
refactor: change offset_for to pseudo-random
jamiemccarthy d453514
refactor: throttled_callback_is_offset_aware
jamiemccarthy 06787b5
docs: update changelog
jamiemccarthy daf0435
Merge tag 'v6.6.0' into jm-random-period-offset
jamiemccarthy 71198e7
update: throttled_callback is now throttled_responder
jamiemccarthy ca42333
test: changing the throttled responder can change the offset algorithm
jamiemccarthy deba4d6
Merge branch 'main' into jm-random-period-offset
jamiemccarthy 24ae6b2
readme: resolve lambda interface change
jamiemccarthy dc18167
readme: fix typo
jamiemccarthy 0085685
Merge branch 'main' into jm-random-period-offset
jamiemccarthy 0f5a832
Rubocop auto-corrections
jamiemccarthy d925ef7
Merge branch 'main' into jm-random-period-offset
jamiemccarthy 71a63ad
Merge branch 'main' into jm-random-period-offset
jamiemccarthy a91807b
Merge branch 'main' into jm-random-period-offset
grzuy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,20 +22,21 @@ def cache | |
Rack::Attack.cache | ||
end | ||
|
||
def matched_by?(request) | ||
def matched_by?(request, use_offset = false) | ||
discriminator = discriminator_for(request) | ||
return false unless discriminator | ||
|
||
current_period = period_for(request) | ||
current_limit = limit_for(request) | ||
count = cache.count("#{name}:#{discriminator}", current_period) | ||
count = cache.count("#{name}:#{discriminator}", current_period, use_offset) | ||
|
||
data = { | ||
discriminator: discriminator, | ||
count: count, | ||
period: current_period, | ||
limit: current_limit, | ||
epoch_time: cache.last_epoch_time | ||
epoch_time: cache.last_epoch_time, | ||
retry_after: cache.last_retry_after_time | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this will set the |
||
} | ||
|
||
annotate_request_with_throttle_data(request, data) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "../spec_helper" | ||
require "timecop" | ||
|
||
describe "Customizing throttled response" do | ||
before do | ||
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new | ||
|
||
Rack::Attack.throttle("by ip", limit: 1, period: 6) do |request| | ||
request.ip | ||
end | ||
end | ||
|
||
it "uses offset if responder is default" do | ||
assert Rack::Attack.cache.store.is_a? ActiveSupport::Cache::MemoryStore | ||
assert_equal 85, count_429_responses | ||
end | ||
|
||
it "does not use offset if responder is not default and aware is not set" do | ||
assert_equal false, Rack::Attack.configuration.throttled_responder_is_offset_aware | ||
Rack::Attack.throttled_responder = lambda do |_req| | ||
[429, {}, ["Throttled"]] | ||
end | ||
assert_equal 100, count_429_responses | ||
end | ||
|
||
it "uses offset if responder is not default and aware is set" do | ||
Rack::Attack.throttled_responder = lambda do |_req| | ||
[429, {}, ["Throttled"]] | ||
end | ||
Rack::Attack.configuration.throttled_responder_is_offset_aware = true | ||
assert_equal 85, count_429_responses | ||
end | ||
|
||
# Count the number of responses with 429 status, out of 100 requests, | ||
# when the clock advances by one second. | ||
# | ||
# When this is invoked with a throttle with period 6 active, using | ||
# a random offset, we would expect about one in six to expire in the | ||
# first second. For the fixed start_time we're using, the offset_for | ||
# MD5 hash happens to come out to 15 expired, 85 throttled, out of 100. | ||
# (If anything about the algorithm changes, that count probably would | ||
# too.) | ||
# | ||
# When using an old-style period, the start_time is at the beginning of | ||
# the period, since 2020-01-01 00:00:00 == 1577836800 == 262972800*6, | ||
# and after 1 second we would expect 0 expires, thus 100 of 100 requests | ||
# to be throttled. | ||
|
||
def count_429_responses | ||
addresses = (1..100).map { |i| "1.2.3.#{i}" } | ||
start_time = Time.gm('2020-01-01 00:00:00') | ||
Timecop.freeze(start_time) do | ||
initial_200_response_count = 0 | ||
addresses.each do |ip| | ||
get "/", {}, "REMOTE_ADDR" => ip | ||
initial_200_response_count += 1 if last_response.status == 200 | ||
end | ||
assert_equal 100, initial_200_response_count | ||
|
||
final_429_response_count = 0 | ||
Timecop.travel(start_time + 1) do | ||
addresses.each do |ip| | ||
get "/", {}, "REMOTE_ADDR" => ip | ||
final_429_response_count += 1 if last_response.status == 429 | ||
end | ||
end | ||
final_429_response_count | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "../spec_helper" | ||
require "timecop" | ||
|
||
describe "Behavior of key_and_expiry" do | ||
it "forms keys and expirations without offset as expected" do | ||
unprefixed_key = "abc789" | ||
period = 1000 | ||
time = Time.at(1_000_000_000) | ||
|
||
Timecop.freeze(time) do | ||
key, expiry = Rack::Attack.cache.send(:key_and_expiry, unprefixed_key, period, false) | ||
assert_equal "rack::attack:1000000:abc789", key | ||
assert_equal 1001, expiry | ||
end | ||
end | ||
|
||
it "forms keys and expirations with offset as expected" do | ||
unprefixed_key = "abc789" | ||
period = 1000 | ||
time = Time.at(1_000_000_000) | ||
|
||
Timecop.freeze(time) do | ||
Rack::Attack.cache.stub :offset_for, 0 do | ||
key, expiry = Rack::Attack.cache.send(:key_and_expiry, unprefixed_key, period, true) | ||
assert_equal "rack::attack:1000000:abc789", key | ||
assert_equal 1001, expiry | ||
end | ||
|
||
Rack::Attack.cache.stub :offset_for, 123 do | ||
key, expiry = Rack::Attack.cache.send(:key_and_expiry, unprefixed_key, period, true) | ||
assert_equal "rack::attack:1000000:abc789", key | ||
assert_equal 1001 - 123, expiry | ||
end | ||
|
||
Digest::MD5.stub :hexdigest, "123" do | ||
key, expiry = Rack::Attack.cache.send(:key_and_expiry, unprefixed_key, period, true) | ||
assert_equal "rack::attack:1000000:abc789", key | ||
assert_equal 1001 - "123".hex, expiry | ||
end | ||
end | ||
end | ||
|
||
it "expires correctly when period is 1 second" do | ||
Timecop.freeze do | ||
current_epoch = Time.now.to_i | ||
key, expiry = Rack::Attack.cache.send(:key_and_expiry, "abc789", 1) | ||
assert_equal "rack::attack:#{current_epoch}:abc789", key | ||
assert_equal 2, expiry | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think both of these sentences are somehow inaccurate (or incomplete) 🤔. In
spec/rack_attack_throttle_spec.rb
we can see thatrequest.env['rack.attack.throttle_data'][name]
is always set (when using the throttling feature), except when the provided block returnsnil
, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so maybe this is just some confusion between
throttle_data
('rack.attack.throttle_data') andmatched_data
which include 'rack.attack.matched', 'rack.attack.match_discriminator', 'rack.attack.match_type', and 'rack.attack.match_data'.