Skip to content

Commit 78c0f11

Browse files
committed
Eagerly check for store validity
Ref: #315 `respond_to?` is a relatively expensive method that doesn't benefit from inline caching, so it has to keep looking up the method every time. Hence it's best to avoid it in hotspots. In this case, `@store` can only change through the `store=` method, so we might as well eagerly validate it there. First because this way it's a one time fixed cost, but also because this way the error is raised early during boot/configuration rather than at runtime where it has availability impact.
1 parent e938879 commit 78c0f11

7 files changed

Lines changed: 65 additions & 37 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
ruby:
21+
- '4.0'
2122
- '3.4'
2223
- '3.3'
2324
- '3.2'

lib/rack/attack/cache.rb

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def store=(store)
2626
else
2727
store
2828
end
29+
if @store
30+
enforce_store_method_presence!(:read)
31+
enforce_store_method_presence!(:write)
32+
enforce_store_method_presence!(:delete)
33+
enforce_store_method_presence!(:increment)
34+
end
2935
end
3036

3137
def count(unprefixed_key, period)
@@ -34,13 +40,14 @@ def count(unprefixed_key, period)
3440
end
3541

3642
def read(unprefixed_key)
37-
enforce_store_presence!
38-
enforce_store_method_presence!(:read)
43+
raise Rack::Attack::MissingStoreError if store.nil?
3944

4045
store.read("#{prefix}:#{unprefixed_key}")
4146
end
4247

4348
def write(unprefixed_key, value, expires_in)
49+
raise Rack::Attack::MissingStoreError if store.nil?
50+
4451
store.write("#{prefix}:#{unprefixed_key}", value, expires_in: expires_in)
4552
end
4653

@@ -74,26 +81,17 @@ def key_and_expiry(unprefixed_key, period)
7481
end
7582

7683
def do_count(key, expires_in)
77-
enforce_store_presence!
78-
enforce_store_method_presence!(:increment)
84+
raise Rack::Attack::MissingStoreError if store.nil?
7985

8086
result = store.increment(key, 1, expires_in: expires_in)
8187

8288
# NB: Some stores return nil when incrementing uninitialized values
8389
if result.nil?
84-
enforce_store_method_presence!(:write)
85-
8690
store.write(key, 1, expires_in: expires_in)
8791
end
8892
result || 1
8993
end
9094

91-
def enforce_store_presence!
92-
if store.nil?
93-
raise Rack::Attack::MissingStoreError
94-
end
95-
end
96-
9795
def enforce_store_method_presence!(method_name)
9896
if !store.respond_to?(method_name)
9997
raise(

rack-attack.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ Gem::Specification.new do |s|
2929
s.add_runtime_dependency 'rack', ">= 1.0", "< 4"
3030

3131
s.add_development_dependency 'appraisal', '~> 2.2'
32-
s.add_development_dependency "bundler", ">= 1.17", "< 3.0"
3332
s.add_development_dependency 'minitest', "~> 5.11"
3433
s.add_development_dependency "minitest-stub-const", "~> 0.6"
3534
s.add_development_dependency 'rack-test', "~> 2.0"
3635
s.add_development_dependency 'rake', "~> 13.0"
36+
s.add_development_dependency 'ostruct'
3737
s.add_development_dependency "rubocop", "1.82"
3838
s.add_development_dependency "rubocop-minitest", "~> 0.38"
3939
s.add_development_dependency "rubocop-performance", "~> 1.26"

spec/acceptance/cache_store_config_for_allow2ban_spec.rb

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727
def write(key, value); end
2828

2929
def increment(key, count, options = {}); end
30+
31+
def delete(key); end
3032
end
3133

3234
Object.stub_const(:FakeStore, fake_store_class) do
33-
Rack::Attack.cache.store = FakeStore.new
34-
3535
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
36-
get "/scarce-resource"
36+
Rack::Attack.cache.store = FakeStore.new
3737
end
3838
end
3939

@@ -47,13 +47,13 @@ def increment(key, count, options = {}); end
4747
def read(key); end
4848

4949
def increment(key, count, options = {}); end
50+
51+
def delete(key); end
5052
end
5153

5254
Object.stub_const(:FakeStore, fake_store_class) do
53-
Rack::Attack.cache.store = FakeStore.new
54-
5555
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
56-
get "/scarce-resource"
56+
Rack::Attack.cache.store = FakeStore.new
5757
end
5858
end
5959

@@ -67,13 +67,13 @@ def increment(key, count, options = {}); end
6767
def read(key); end
6868

6969
def write(key, value); end
70+
71+
def delete(key); end
7072
end
7173

7274
Object.stub_const(:FakeStore, fake_store_class) do
73-
Rack::Attack.cache.store = FakeStore.new
74-
7575
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
76-
get "/scarce-resource"
76+
Rack::Attack.cache.store = FakeStore.new
7777
end
7878
end
7979

@@ -100,6 +100,10 @@ def increment(key, _count, _options = {})
100100
@backend[key] ||= 0
101101
@backend[key] += 1
102102
end
103+
104+
def delete(key)
105+
@backend.delete(key)
106+
end
103107
end
104108

105109
Object.stub_const(:FakeStore, fake_store_class) do

spec/acceptance/cache_store_config_for_fail2ban_spec.rb

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@ def increment(key, count, options = {}); end
3030
end
3131

3232
Object.stub_const(:FakeStore, fake_store_class) do
33-
Rack::Attack.cache.store = FakeStore.new
34-
3533
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
36-
get "/private-place"
34+
Rack::Attack.cache.store = FakeStore.new
3735
end
3836
end
3937

@@ -47,13 +45,13 @@ def increment(key, count, options = {}); end
4745
def read(key); end
4846

4947
def increment(key, count, options = {}); end
48+
49+
def delete(key); end
5050
end
5151

5252
Object.stub_const(:FakeStore, fake_store_class) do
53-
Rack::Attack.cache.store = FakeStore.new
54-
5553
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
56-
get "/private-place"
54+
Rack::Attack.cache.store = FakeStore.new
5755
end
5856
end
5957

@@ -67,13 +65,13 @@ def increment(key, count, options = {}); end
6765
def read(key); end
6866

6967
def write(key, value); end
68+
69+
def delete(key); end
7070
end
7171

7272
Object.stub_const(:FakeStore, fake_store_class) do
73-
Rack::Attack.cache.store = FakeStore.new
74-
7573
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
76-
get "/private-place"
74+
Rack::Attack.cache.store = FakeStore.new
7775
end
7876
end
7977

@@ -100,6 +98,10 @@ def increment(key, _count, _options = {})
10098
@backend[key] ||= 0
10199
@backend[key] += 1
102100
end
101+
102+
def delete(key)
103+
@backend.delete(key)
104+
end
103105
end
104106

105107
Rack::Attack.cache.store = fake_store_class.new

spec/acceptance/cache_store_config_for_throttle_spec.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@
1818
end
1919

2020
it "gives semantic error if incompatible store was configured" do
21-
Rack::Attack.cache.store = Object.new
22-
2321
assert_raises(Rack::Attack::MisconfiguredStoreError) do
24-
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
22+
Rack::Attack.cache.store = Object.new
2523
end
2624
end
2725

@@ -33,10 +31,22 @@ def initialize
3331
@counts = {}
3432
end
3533

34+
def read(key)
35+
@counts[key]
36+
end
37+
38+
def write(key, count)
39+
@counts[key] = count
40+
end
41+
3642
def increment(key, _count, _options)
3743
@counts[key] ||= 0
3844
@counts[key] += 1
3945
end
46+
47+
def delete(key)
48+
@counts.delete(key)
49+
end
4050
end
4151

4252
Rack::Attack.cache.store = basic_store_class.new

spec/rack_attack_reset_spec.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@
44

55
describe "Rack::Attack.reset!" do
66
it "raises an error when is not supported by cache store" do
7-
Rack::Attack.cache.store = Class.new
8-
assert_raises(Rack::Attack::IncompatibleStoreError) do
9-
Rack::Attack.reset!
7+
fake_store_class = Class.new do
8+
def read(key); end
9+
10+
def write(key, value); end
11+
12+
def increment(key, count, options = {}); end
13+
14+
def delete(key); end
15+
16+
end
17+
18+
Object.stub_const(:FakeStore, fake_store_class) do
19+
Rack::Attack.cache.store = FakeStore.new
20+
assert_raises(Rack::Attack::IncompatibleStoreError) do
21+
Rack::Attack.reset!
22+
end
1023
end
1124
end
1225

0 commit comments

Comments
 (0)