Skip to content

Commit d77280f

Browse files
committed
Discard bucket when rate limit settings change
1 parent a0e6144 commit d77280f

File tree

3 files changed

+52
-22
lines changed

3 files changed

+52
-22
lines changed

lib/aikido/zen/rate_limiter.rb

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@ def initialize(
1616
)
1717
@config = config
1818
@settings = settings
19-
@buckets = Hash.new { |store, route|
20-
synchronize {
21-
settings = settings_for(route)
22-
store[route] = Bucket.new(ttl: settings.period, max_size: settings.max_requests)
23-
}
24-
}
19+
@buckets = {}
2520
end
2621

2722
# Calculate based on the configuration whether a request will be
@@ -30,24 +25,30 @@ def initialize(
3025
# @param request [Aikido::Zen::Request]
3126
# @return [Aikido::Zen::RateLimiter::Result, nil]
3227
def calculate_rate_limits(request)
33-
route, enabled = resolve_route_enabled(request)
34-
return nil unless enabled
28+
route, settings = @settings.endpoints.match(request.route)
3529

36-
bucket = @buckets[route]
37-
key = @config.rate_limiting_discriminator.call(request)
38-
bucket.increment(key)
39-
end
30+
rate_limit_settings = settings&.rate_limiting
31+
32+
return nil unless rate_limit_settings&.enabled?
33+
34+
bucket = nil
4035

41-
private
36+
synchronize do
37+
bucket = @buckets[route]
4238

43-
def resolve_route_enabled(request)
44-
@settings.endpoints.match(request.route) do |route, settings|
45-
[route, settings.rate_limiting.enabled?]
39+
if bucket.nil? || bucket.settings_changed?(rate_limit_settings)
40+
bucket = Bucket.new(
41+
ttl: rate_limit_settings.period,
42+
max_size: rate_limit_settings.max_requests,
43+
settings: rate_limit_settings
44+
)
45+
46+
@buckets[route] = bucket
47+
end
4648
end
47-
end
4849

49-
def settings_for(route)
50-
@settings.endpoints[route].rate_limiting
50+
key = @config.rate_limiting_discriminator.call(request)
51+
bucket.increment(key)
5152
end
5253
end
5354
end

lib/aikido/zen/rate_limiter/bucket.rb

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,21 @@ class RateLimiter::Bucket
2929
# @!visibility private
3030
#
3131
# Use the monotonic clock to ensure time differences are consistent
32-
# and not affected by timezones.or daylight savings changes.
32+
# and not affected by timezones or daylight savings changes.
3333
DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC).round }
3434

35-
def initialize(ttl:, max_size:, clock: DEFAULT_CLOCK)
35+
# @param ttl [Integer] the time to live in seconds
36+
# @param max_size [Integer] the number of requests before throttling
37+
# @param clock [#call() => Integer] a callable that returns the current
38+
# monotonic time in seconds
39+
# @param settings [Aikido::Zen::RuntimeSettings::RateLimitSettings, nil]
40+
# the settings that might change
41+
def initialize(ttl:, max_size:, clock: DEFAULT_CLOCK, settings: nil)
3642
@ttl = ttl
3743
@max_size = max_size
3844
@data = Hash.new { |h, k| h[k] = [] }
3945
@clock = clock
46+
@settings = settings
4047
end
4148

4249
# Increments the key if the number of entries within the current TTL window
@@ -67,10 +74,21 @@ def increment(key)
6774
end
6875
end
6976

77+
# Checks whether the settings provided at initialization have changed.
78+
#
79+
# @param settings [Aikido::Zen::RuntimeSettings::RateLimitSettings]
80+
# @return [Boolean] true if settings were provided at initialization and
81+
# the settings have changed
82+
def settings_changed?(settings)
83+
!@settings.nil? && @settings != settings
84+
end
85+
7086
private
7187

7288
def evict(key, at: @clock.call)
73-
synchronize { @data[key].delete_if { |time| time < (at - @ttl) } }
89+
synchronize do
90+
@data[key].delete_if { |time| time < (at - @ttl) }
91+
end
7492
end
7593
end
7694
end

lib/aikido/zen/runtime_settings/rate_limit_settings.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def self.disabled
2828
new(enabled: false)
2929
end
3030

31+
# @return [Boolean]
32+
attr_reader :enabled
33+
3134
# @return [Integer] the fixed window to bucket requests in, in seconds.
3235
attr_reader :period
3336

@@ -43,5 +46,13 @@ def initialize(enabled: false, max_requests: 1000, period: 60)
4346
def enabled?
4447
@enabled
4548
end
49+
50+
def ==(other)
51+
other.is_a?(self.class) &&
52+
other.enabled == enabled &&
53+
other.period == period &&
54+
other.max_requests == max_requests
55+
end
56+
alias_method :eql?, :==
4657
end
4758
end

0 commit comments

Comments
 (0)