throttle_machines 0.2.4-x86_64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +204 -0
- data/lib/throttle_machines/async_breaker.rb +12 -0
- data/lib/throttle_machines/async_limiter.rb +24 -0
- data/lib/throttle_machines/async_support.rb +75 -0
- data/lib/throttle_machines/control.rb +95 -0
- data/lib/throttle_machines/controller_helpers.rb +79 -0
- data/lib/throttle_machines/dependency_error.rb +6 -0
- data/lib/throttle_machines/engine.rb +18 -0
- data/lib/throttle_machines/hedged_breaker.rb +23 -0
- data/lib/throttle_machines/hedged_request.rb +117 -0
- data/lib/throttle_machines/instrumentation.rb +158 -0
- data/lib/throttle_machines/limiter.rb +167 -0
- data/lib/throttle_machines/middleware.rb +89 -0
- data/lib/throttle_machines/native_speedup.rb +98 -0
- data/lib/throttle_machines/rack_middleware/allow2_ban.rb +51 -0
- data/lib/throttle_machines/rack_middleware/ban_filter.rb +29 -0
- data/lib/throttle_machines/rack_middleware/blocklist.rb +14 -0
- data/lib/throttle_machines/rack_middleware/configuration.rb +103 -0
- data/lib/throttle_machines/rack_middleware/fail2_ban.rb +78 -0
- data/lib/throttle_machines/rack_middleware/list_filter.rb +39 -0
- data/lib/throttle_machines/rack_middleware/request.rb +12 -0
- data/lib/throttle_machines/rack_middleware/safelist.rb +14 -0
- data/lib/throttle_machines/rack_middleware/throttle.rb +95 -0
- data/lib/throttle_machines/rack_middleware/track.rb +51 -0
- data/lib/throttle_machines/rack_middleware.rb +87 -0
- data/lib/throttle_machines/storage/base.rb +72 -0
- data/lib/throttle_machines/storage/memory.rb +267 -0
- data/lib/throttle_machines/storage/null.rb +67 -0
- data/lib/throttle_machines/storage/redis/gcra.lua +22 -0
- data/lib/throttle_machines/storage/redis/increment_counter.lua +9 -0
- data/lib/throttle_machines/storage/redis/peek_gcra.lua +16 -0
- data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +18 -0
- data/lib/throttle_machines/storage/redis/token_bucket.lua +23 -0
- data/lib/throttle_machines/storage/redis.rb +231 -0
- data/lib/throttle_machines/throttled_error.rb +14 -0
- data/lib/throttle_machines/version.rb +5 -0
- data/lib/throttle_machines.rb +200 -0
- data/lib/throttle_machines_native/throttle_machines_native.so +0 -0
- metadata +218 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
5
|
+
module ThrottleMachines
|
|
6
|
+
class RackMiddleware
|
|
7
|
+
class Configuration
|
|
8
|
+
DEFAULT_BLOCKLISTED_RESPONDER = ->(_req) { [403, { 'content-type' => 'text/plain' }, ["Forbidden\n"]] }
|
|
9
|
+
|
|
10
|
+
DEFAULT_THROTTLED_RESPONDER = lambda do |req|
|
|
11
|
+
match_data = req.env['rack.attack.match_data']
|
|
12
|
+
retry_after = match_data[:retry_after] || 60
|
|
13
|
+
|
|
14
|
+
[429, { 'content-type' => 'text/plain', 'retry-after' => retry_after.to_s }, ["Retry later\n"]]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :throttles, :tracks, :safelists, :blocklists
|
|
18
|
+
attr_accessor :throttled_responder, :blocklisted_responder
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@throttles = {}
|
|
22
|
+
@tracks = {}
|
|
23
|
+
@safelists = {}
|
|
24
|
+
@blocklists = {}
|
|
25
|
+
@anonymous_safelists = []
|
|
26
|
+
@anonymous_blocklists = []
|
|
27
|
+
@fail2bans = {}
|
|
28
|
+
@allow2bans = {}
|
|
29
|
+
|
|
30
|
+
@throttled_responder = DEFAULT_THROTTLED_RESPONDER
|
|
31
|
+
@blocklisted_responder = DEFAULT_BLOCKLISTED_RESPONDER
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# DSL Methods
|
|
35
|
+
def throttle(name, options = {}, &block)
|
|
36
|
+
@throttles[name] = Throttle.new(name, options, &block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def track(name, options = {}, &block)
|
|
40
|
+
@tracks[name] = Track.new(name, options, &block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def safelist(name = nil, &block)
|
|
44
|
+
if name
|
|
45
|
+
@safelists[name] = Safelist.new(name, &block)
|
|
46
|
+
else
|
|
47
|
+
@anonymous_safelists << Safelist.new(nil, &block)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def blocklist(name = nil, &block)
|
|
52
|
+
if name
|
|
53
|
+
@blocklists[name] = Blocklist.new(name, &block)
|
|
54
|
+
else
|
|
55
|
+
@anonymous_blocklists << Blocklist.new(nil, &block)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def safelist_ip(ip_address)
|
|
60
|
+
@anonymous_safelists << Safelist.new(nil) { |req| req.ip == ip_address }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def blocklist_ip(ip_address)
|
|
64
|
+
@anonymous_blocklists << Blocklist.new(nil) { |req| req.ip == ip_address }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fail2ban(name, options = {}, &block)
|
|
68
|
+
@fail2bans[name] = Fail2Ban.new(name, options, &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def allow2ban(name, options = {}, &block)
|
|
72
|
+
@allow2bans[name] = Allow2Ban.new(name, options, &block)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check methods
|
|
76
|
+
def safelisted?(request)
|
|
77
|
+
@anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
|
|
78
|
+
@safelists.values.any? { |safelist| safelist.matched_by?(request) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def blocklisted?(request)
|
|
82
|
+
# Check explicit blocklists
|
|
83
|
+
return true if @anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) }
|
|
84
|
+
return true if @blocklists.values.any? { |blocklist| blocklist.matched_by?(request) }
|
|
85
|
+
|
|
86
|
+
# Check fail2bans
|
|
87
|
+
@fail2bans.values.any? { |fail2ban| fail2ban.banned?(request) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def throttled?(request)
|
|
91
|
+
# Process allow2bans first (they can reset fail2ban counters)
|
|
92
|
+
@allow2bans.each_value { |allow2ban| allow2ban.matched_by?(request) }
|
|
93
|
+
|
|
94
|
+
# Check throttles
|
|
95
|
+
@throttles.values.any? { |throttle| throttle.matched_by?(request) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def tracked?(request)
|
|
99
|
+
@tracks.each_value { |track| track.matched_by?(request) }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
class RackMiddleware
|
|
5
|
+
class Fail2Ban < BanFilter
|
|
6
|
+
def banned?(request)
|
|
7
|
+
discriminator = discriminator_for(request)
|
|
8
|
+
return false unless discriminator
|
|
9
|
+
|
|
10
|
+
key = "fail2ban:#{@name}:#{discriminator}"
|
|
11
|
+
|
|
12
|
+
# Use a globally managed BreakerMachines circuit as the ban mechanism
|
|
13
|
+
breaker = BreakerMachines::Registry.instance.get_or_create_dynamic_circuit(
|
|
14
|
+
key,
|
|
15
|
+
self,
|
|
16
|
+
failure_threshold: @maxretry,
|
|
17
|
+
failure_window: @findtime,
|
|
18
|
+
reset_timeout: @bantime
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Check if circuit is open (banned)
|
|
22
|
+
if breaker.open?
|
|
23
|
+
stats = breaker.stats
|
|
24
|
+
now = BreakerMachines.monotonic_time
|
|
25
|
+
time_until_unban = if stats.opened_at
|
|
26
|
+
remaining = @bantime - (now - stats.opened_at)
|
|
27
|
+
remaining.positive? ? remaining : 0
|
|
28
|
+
else
|
|
29
|
+
@bantime
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
request.env['rack.attack.matched'] = @name
|
|
33
|
+
request.env['rack.attack.match_type'] = :fail2ban
|
|
34
|
+
request.env['rack.attack.match_discriminator'] = discriminator
|
|
35
|
+
request.env['rack.attack.match_data'] = {
|
|
36
|
+
discriminator: discriminator,
|
|
37
|
+
maxretry: @maxretry,
|
|
38
|
+
findtime: @findtime,
|
|
39
|
+
bantime: @bantime,
|
|
40
|
+
failures: stats.failure_count,
|
|
41
|
+
time_until_unban: time_until_unban
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ThrottleMachines::RackMiddleware.instrument(request)
|
|
45
|
+
true
|
|
46
|
+
else
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def count(request)
|
|
52
|
+
discriminator = discriminator_for(request)
|
|
53
|
+
return unless discriminator
|
|
54
|
+
|
|
55
|
+
key = "fail2ban:#{@name}:#{discriminator}"
|
|
56
|
+
|
|
57
|
+
# Use the breaker to record a failure if block returns true
|
|
58
|
+
return unless yield
|
|
59
|
+
|
|
60
|
+
breaker = BreakerMachines::Registry.instance.get_or_create_dynamic_circuit(
|
|
61
|
+
key,
|
|
62
|
+
self,
|
|
63
|
+
failure_threshold: @maxretry,
|
|
64
|
+
failure_window: @findtime,
|
|
65
|
+
reset_timeout: @bantime
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Record failure by trying to call through the breaker
|
|
69
|
+
# and letting it fail
|
|
70
|
+
begin
|
|
71
|
+
breaker.call { raise 'Fail2Ban failure' }
|
|
72
|
+
rescue StandardError
|
|
73
|
+
# Expected - this records the failure
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
class RackMiddleware
|
|
5
|
+
# Base class for Safelist and Blocklist filters.
|
|
6
|
+
#
|
|
7
|
+
# Both match a request against a user-supplied block and, on a match, tag the
|
|
8
|
+
# rack env and emit instrumentation. They differ only in the +match_type+
|
|
9
|
+
# recorded on the env, which subclasses provide.
|
|
10
|
+
class ListFilter
|
|
11
|
+
attr_reader :name, :block
|
|
12
|
+
|
|
13
|
+
def initialize(name, &block)
|
|
14
|
+
@name = name
|
|
15
|
+
@block = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def matched_by?(request)
|
|
19
|
+
return false unless @block
|
|
20
|
+
|
|
21
|
+
if @block.call(request)
|
|
22
|
+
request.env['rack.attack.matched'] = @name
|
|
23
|
+
request.env['rack.attack.match_type'] = match_type
|
|
24
|
+
ThrottleMachines::RackMiddleware.instrument(request)
|
|
25
|
+
true
|
|
26
|
+
else
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] the rack.attack match type recorded for this filter
|
|
34
|
+
def match_type
|
|
35
|
+
raise NotImplementedError, "#{self.class} must implement #match_type"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
class RackMiddleware
|
|
5
|
+
class Throttle
|
|
6
|
+
attr_reader :name, :limit, :period, :block, :algorithm
|
|
7
|
+
|
|
8
|
+
def initialize(name, options, &block)
|
|
9
|
+
@name = name
|
|
10
|
+
@block = block
|
|
11
|
+
|
|
12
|
+
raise ArgumentError, 'Must pass :limit option' unless options[:limit]
|
|
13
|
+
raise ArgumentError, 'Must pass :period option' unless options[:period]
|
|
14
|
+
|
|
15
|
+
@limit = options[:limit]
|
|
16
|
+
@period = options[:period].to_i
|
|
17
|
+
@algorithm = options[:algorithm] || :fixed_window
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def matched_by?(request)
|
|
21
|
+
discriminator = discriminator_for(request)
|
|
22
|
+
|
|
23
|
+
return false unless discriminator
|
|
24
|
+
|
|
25
|
+
key = "#{@name}:#{discriminator}"
|
|
26
|
+
current_limit = limit_for(request)
|
|
27
|
+
current_period = period_for(request)
|
|
28
|
+
|
|
29
|
+
# Use ThrottleMachines limiter
|
|
30
|
+
limiter = ThrottleMachines.limiter(
|
|
31
|
+
key,
|
|
32
|
+
limit: current_limit,
|
|
33
|
+
period: current_period,
|
|
34
|
+
algorithm: @algorithm
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Try to consume the request atomically
|
|
38
|
+
throttled = false
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
# This will either succeed and consume a request, or raise ThrottledError
|
|
42
|
+
limiter.throttle!
|
|
43
|
+
|
|
44
|
+
# If we get here, request was allowed
|
|
45
|
+
rescue ThrottledError
|
|
46
|
+
# Request was throttled
|
|
47
|
+
throttled = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get current state for instrumentation
|
|
51
|
+
data = {
|
|
52
|
+
discriminator: discriminator,
|
|
53
|
+
count: current_limit - limiter.remaining,
|
|
54
|
+
period: current_period,
|
|
55
|
+
limit: current_limit,
|
|
56
|
+
retry_after: limiter.retry_after
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
annotate_request_with_throttle_data(request, data)
|
|
60
|
+
|
|
61
|
+
if throttled
|
|
62
|
+
annotate_request_with_matched_data(request, data)
|
|
63
|
+
ThrottleMachines::RackMiddleware.instrument(request)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
throttled
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def discriminator_for(request)
|
|
72
|
+
@block.call(request)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def limit_for(request)
|
|
76
|
+
@limit.respond_to?(:call) ? @limit.call(request) : @limit
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def period_for(request)
|
|
80
|
+
@period.respond_to?(:call) ? @period.call(request) : @period
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def annotate_request_with_throttle_data(request, data)
|
|
84
|
+
(request.env['rack.attack.throttle_data'] ||= {})[@name] = data
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def annotate_request_with_matched_data(request, data)
|
|
88
|
+
request.env['rack.attack.matched'] = @name
|
|
89
|
+
request.env['rack.attack.match_discriminator'] = data[:discriminator]
|
|
90
|
+
request.env['rack.attack.match_type'] = :throttle
|
|
91
|
+
request.env['rack.attack.match_data'] = data
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
class RackMiddleware
|
|
5
|
+
class Track
|
|
6
|
+
attr_reader :name, :block, :limit, :period
|
|
7
|
+
|
|
8
|
+
def initialize(name, options = {}, &block)
|
|
9
|
+
@name = name
|
|
10
|
+
@block = block
|
|
11
|
+
@limit = options[:limit]
|
|
12
|
+
@period = options[:period]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def matched_by?(request)
|
|
16
|
+
discriminator = @block.call(request)
|
|
17
|
+
return false unless discriminator
|
|
18
|
+
|
|
19
|
+
# Track is just instrumentation without blocking
|
|
20
|
+
data = {
|
|
21
|
+
discriminator: discriminator
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# If limit and period are provided, track the count
|
|
25
|
+
if @limit && @period
|
|
26
|
+
key = "track:#{@name}:#{discriminator}"
|
|
27
|
+
limiter = ThrottleMachines.limiter(
|
|
28
|
+
key,
|
|
29
|
+
limit: @limit,
|
|
30
|
+
period: @period,
|
|
31
|
+
algorithm: :fixed_window
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Just check, don't consume
|
|
35
|
+
data[:count] = @limit - limiter.remaining
|
|
36
|
+
data[:limit] = @limit
|
|
37
|
+
data[:period] = @period
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
request.env['rack.attack.matched'] = @name
|
|
41
|
+
request.env['rack.attack.match_type'] = :track
|
|
42
|
+
request.env['rack.attack.match_discriminator'] = discriminator
|
|
43
|
+
request.env['rack.attack.match_data'] = data
|
|
44
|
+
|
|
45
|
+
ThrottleMachines::RackMiddleware.instrument(request)
|
|
46
|
+
|
|
47
|
+
false # Track never blocks
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
|
|
6
|
+
module ThrottleMachines
|
|
7
|
+
# Advanced Rack middleware for rate limiting and request filtering
|
|
8
|
+
class RackMiddleware
|
|
9
|
+
class << self
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
attr_accessor :enabled, :notifier
|
|
13
|
+
attr_reader :configuration
|
|
14
|
+
|
|
15
|
+
def_delegators :@configuration,
|
|
16
|
+
:throttle,
|
|
17
|
+
:track,
|
|
18
|
+
:safelist,
|
|
19
|
+
:blocklist,
|
|
20
|
+
:blocklist_ip,
|
|
21
|
+
:safelist_ip,
|
|
22
|
+
:fail2ban,
|
|
23
|
+
:allow2ban,
|
|
24
|
+
:throttled_responder,
|
|
25
|
+
:throttled_responder=,
|
|
26
|
+
:blocklisted_responder,
|
|
27
|
+
:blocklisted_responder=,
|
|
28
|
+
:throttles,
|
|
29
|
+
:tracks,
|
|
30
|
+
:safelists,
|
|
31
|
+
:blocklists
|
|
32
|
+
|
|
33
|
+
def configure(&block)
|
|
34
|
+
@configuration ||= Configuration.new
|
|
35
|
+
@configuration.instance_eval(&block) if block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reset!
|
|
39
|
+
ThrottleMachines.reset!
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear!
|
|
43
|
+
@configuration = Configuration.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Instrument for ActiveSupport::Notifications compatibility
|
|
47
|
+
def instrument(request)
|
|
48
|
+
return unless notifier
|
|
49
|
+
|
|
50
|
+
event_type = request.env['rack.attack.match_type']
|
|
51
|
+
notifier.instrument("#{event_type}.throttle_machines", request: request)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Set defaults
|
|
56
|
+
@enabled = true
|
|
57
|
+
@notifier = ActiveSupport::Notifications
|
|
58
|
+
@configuration = Configuration.new
|
|
59
|
+
|
|
60
|
+
def initialize(app)
|
|
61
|
+
@app = app
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(env)
|
|
65
|
+
return @app.call(env) if !self.class.enabled || env['rack.attack.called']
|
|
66
|
+
|
|
67
|
+
env['rack.attack.called'] = true
|
|
68
|
+
request = Request.new(env)
|
|
69
|
+
|
|
70
|
+
# Always use the current class-level configuration
|
|
71
|
+
configuration = self.class.configuration
|
|
72
|
+
|
|
73
|
+
if configuration.safelisted?(request)
|
|
74
|
+
@app.call(env)
|
|
75
|
+
elsif configuration.blocklisted?(request)
|
|
76
|
+
configuration.blocklisted_responder.call(request)
|
|
77
|
+
elsif configuration.throttled?(request)
|
|
78
|
+
configuration.throttled_responder.call(request)
|
|
79
|
+
else
|
|
80
|
+
configuration.tracked?(request)
|
|
81
|
+
@app.call(env)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# No alias - users should use the explicit name to avoid confusion
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
module Storage
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(options = {})
|
|
7
|
+
@options = options
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Rate limiting operations
|
|
11
|
+
def increment_counter(key, window, amount = 1)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get_counter(key, window)
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_counter_ttl(key, window)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset_counter(key, window)
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# GCRA operations (atomic)
|
|
28
|
+
def check_gcra_limit(key, emission_interval, delay_tolerance, ttl)
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def peek_gcra_limit(key, emission_interval, delay_tolerance)
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Token bucket operations (atomic)
|
|
37
|
+
def check_token_bucket(key, capacity, refill_rate, ttl)
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def peek_token_bucket(key, capacity, refill_rate)
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Utility operations
|
|
46
|
+
def clear(pattern = nil)
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def healthy?
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def with_timeout(timeout, &block)
|
|
55
|
+
Timeout.timeout(timeout, &block)
|
|
56
|
+
rescue Timeout::Error
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
def current_time
|
|
63
|
+
# Use monotonic time for consistency with BreakerMachines
|
|
64
|
+
ThrottleMachines.monotonic_time
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def monotonic_time
|
|
68
|
+
ThrottleMachines.monotonic_time
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|