throttle_machines 0.1.1 → 0.2.4

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -0
  3. data/lib/throttle_machines/async_breaker.rb +12 -0
  4. data/lib/throttle_machines/async_limiter.rb +7 -117
  5. data/lib/throttle_machines/async_support.rb +75 -0
  6. data/lib/throttle_machines/controller_helpers.rb +2 -2
  7. data/lib/throttle_machines/engine.rb +1 -6
  8. data/lib/throttle_machines/hedged_breaker.rb +2 -2
  9. data/lib/throttle_machines/hedged_request.rb +2 -2
  10. data/lib/throttle_machines/instrumentation.rb +19 -19
  11. data/lib/throttle_machines/middleware.rb +1 -2
  12. data/lib/throttle_machines/native_speedup.rb +98 -0
  13. data/lib/throttle_machines/rack_middleware/allow2_ban.rb +11 -22
  14. data/lib/throttle_machines/rack_middleware/ban_filter.rb +29 -0
  15. data/lib/throttle_machines/rack_middleware/blocklist.rb +5 -18
  16. data/lib/throttle_machines/rack_middleware/configuration.rb +14 -14
  17. data/lib/throttle_machines/rack_middleware/fail2_ban.rb +20 -29
  18. data/lib/throttle_machines/rack_middleware/list_filter.rb +39 -0
  19. data/lib/throttle_machines/rack_middleware/safelist.rb +5 -18
  20. data/lib/throttle_machines/rack_middleware.rb +2 -4
  21. data/lib/throttle_machines/storage/base.rb +2 -23
  22. data/lib/throttle_machines/storage/memory.rb +7 -113
  23. data/lib/throttle_machines/storage/null.rb +0 -21
  24. data/lib/throttle_machines/storage/redis/gcra.lua +1 -1
  25. data/lib/throttle_machines/storage/redis/increment_counter.lua +1 -1
  26. data/lib/throttle_machines/storage/redis/peek_gcra.lua +1 -1
  27. data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +1 -1
  28. data/lib/throttle_machines/storage/redis/token_bucket.lua +1 -1
  29. data/lib/throttle_machines/storage/redis.rb +5 -68
  30. data/lib/throttle_machines/version.rb +1 -1
  31. data/lib/throttle_machines.rb +87 -21
  32. metadata +74 -15
  33. data/Rakefile +0 -12
  34. data/lib/throttle_machines/storage/redis/get_breaker_state.lua +0 -23
  35. data/lib/throttle_machines/storage/redis/record_breaker_failure.lua +0 -24
  36. data/lib/throttle_machines/storage/redis/record_breaker_success.lua +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c498f9f097c1eac7384d573ea5e90b04e3850d6b236aae5a0f5b9bb0cdf29ae
4
- data.tar.gz: 515fae0aad7083290b12cec368148e021ff9312fb026892fb4574fd89a1a0f6c
3
+ metadata.gz: 6450b4499ed28a6520bdc5d51a2c5bc7fcc55ba8e1f1e327def0ca9ab5c80d62
4
+ data.tar.gz: af1addd1f524c9f5663f3f241ce15bfb1aa9a6b5365463f3c9d453a9e03f0b05
5
5
  SHA512:
6
- metadata.gz: ec8ac810c8696a93a44bcf305245eef336ce338418b82a5236e73fa919afda7125cee2ac391c0230419f136525ee1104723dc3a81e42517c4c0a48ebd6878d76
7
- data.tar.gz: 3908d0866286abc0556b79160c8543da2b542ab3cfb55b2c2d636fc69bb0393196848f920714e89e4926d7e4450ae5de33329d0774ae4a0f8f59860bbbaad620
6
+ metadata.gz: 564076859ef7219e48060400a26295cffc830a74fbf82d757766753e19d2020fce390ce8b91b9daf996147689dd47fec0911037a34a0208cd160ec3d6009ac70
7
+ data.tar.gz: 39590d460f59b66a700487146ea3309aa8df4d9f3b31e0248acb1b0748bc04b3a26f9a55f52f18b4beb3fca7618a8142092b4d434f45a4ce833a265d8e2c5fdb
data/README.md CHANGED
@@ -33,6 +33,9 @@ gem 'redis'
33
33
 
34
34
  # For planetary Rails integration
35
35
  gem 'rails' # or just railties
36
+
37
+ # For fiber-safe async throttling
38
+ gem 'async'
36
39
  ```
37
40
 
38
41
  Then initialize systems:
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ class AsyncBreaker
5
+ class << self
6
+ def new(...)
7
+ ThrottleMachines.load_async_runtime!
8
+ BreakerMachines::AsyncCircuit.new(...)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,16 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent-ruby'
4
-
5
3
  module ThrottleMachines
6
- # Async-aware rate limiter for fiber-safe operations
7
- #
8
- # Like quantum entanglement communications - allows multiple spacecraft
9
- # to communicate simultaneously without interference, each in their own
10
- # quantum state (fiber).
4
+ # Backward-compatible async-aware limiter class.
11
5
  #
12
6
  # Example:
13
- # limiter = ThrottleMachines::AsyncLimiter.new("quantum_comms",
7
+ # limiter = ThrottleMachines::AsyncLimiter.new("api",
14
8
  # limit: 100,
15
9
  # period: 60,
16
10
  # algorithm: :gcra
@@ -18,117 +12,13 @@ module ThrottleMachines
18
12
  #
19
13
  # Async do
20
14
  # if limiter.allowed_async?
21
- # # Non-blocking operation
15
+ # # Currently allowed without consuming capacity
16
+ # end
17
+ #
18
+ # limiter.throttle_async(max_wait: 5) do
19
+ # # Consumes capacity through the same atomic path as throttle!
22
20
  # end
23
21
  # end
24
22
  class AsyncLimiter < Limiter
25
- def initialize(key, limit:, period:, algorithm: :fixed_window, storage: nil)
26
- super
27
- @fiber_storage = Concurrent::Map.new # Thread-safe fiber storage
28
- end
29
-
30
- # Async version of allowed? that's fiber-safe
31
- def allowed_async?
32
- allowed = if defined?(Async::Task) && Async::Task.current?
33
- # In async context, use fiber-local checking
34
- fiber_allowed?
35
- else
36
- # Fall back to regular synchronous check
37
- allowed?
38
- end
39
-
40
- # Don't double-instrument when calling parent allowed?
41
- return allowed unless defined?(Async::Task) && Async::Task.current?
42
-
43
- # Instrument the async check
44
- Instrumentation.rate_limit_checked(self, allowed: allowed, remaining: nil)
45
- allowed
46
- end
47
-
48
- # Non-blocking check with async support
49
- def check_async
50
- if allowed_async?
51
- yield if block_given?
52
- true
53
- else
54
- false
55
- end
56
- end
57
-
58
- # Async throttle with automatic retry
59
- def throttle_async(max_wait: nil)
60
- start_time = current_time
61
-
62
- loop do
63
- if allowed_async?
64
- return yield if block_given?
65
-
66
- return true
67
- end
68
-
69
- wait_time = retry_after
70
-
71
- # Check if we've exceeded max wait time
72
- if max_wait && (current_time - start_time + wait_time) > max_wait
73
- raise ThrottleError, 'Maximum wait time exceeded'
74
- end
75
-
76
- # Non-blocking sleep in async context
77
- if defined?(Async::Task) && Async::Task.current?
78
- Async::Task.current.sleep(wait_time)
79
- else
80
- sleep(wait_time)
81
- end
82
- end
83
- end
84
-
85
- # Get current fiber's state
86
- def fiber_state
87
- fiber_id = Fiber.current.object_id
88
- @fiber_storage.compute_if_absent(fiber_id) do
89
- {
90
- last_check: 0,
91
- tokens: @limit.to_f
92
- }
93
- end
94
- end
95
-
96
- # Clean up fiber storage periodically
97
- def cleanup_fiber_storage
98
- current_fibers = ObjectSpace.each_object(Fiber).map(&:object_id)
99
- @fiber_storage.each_key do |fiber_id|
100
- @fiber_storage.delete(fiber_id) unless current_fibers.include?(fiber_id)
101
- end
102
- end
103
-
104
- private
105
-
106
- def fiber_allowed?
107
- state = fiber_state
108
- now = current_time
109
-
110
- case @algorithm
111
- when :token_bucket
112
- # Refill tokens based on time passed
113
- time_passed = now - state[:last_check]
114
- tokens_to_add = time_passed * (@limit.to_f / @period)
115
- state[:tokens] = [state[:tokens] + tokens_to_add, @limit.to_f].min
116
- state[:last_check] = now
117
-
118
- if state[:tokens] >= 1
119
- state[:tokens] -= 1
120
- true
121
- else
122
- false
123
- end
124
- else
125
- # Delegate to parent for other algorithms
126
- allowed?
127
- end
128
- end
129
-
130
- def current_time
131
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
- end
133
23
  end
134
24
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Async/Fiber support for ThrottleMachines.
4
+ #
5
+ # The async gem is optional. When an Async task is active, waits yield back to
6
+ # the scheduler; otherwise the limiter falls back to normal sleep.
7
+
8
+ module ThrottleMachines
9
+ # Fiber-aware rate limiter helpers.
10
+ module AsyncSupport
11
+ def allowed_async?
12
+ allow?
13
+ end
14
+
15
+ alias allow_async? allowed_async?
16
+
17
+ def check_async
18
+ return false unless allowed_async?
19
+
20
+ yield if block_given?
21
+ true
22
+ end
23
+
24
+ def throttle_async(max_wait: nil, &block)
25
+ deadline = max_wait.nil? ? nil : ThrottleMachines.monotonic_time + max_wait.to_f
26
+
27
+ loop do
28
+ begin
29
+ throttle!
30
+ rescue ThrottledError => e
31
+ wait_time = [e.retry_after.to_f, 0.0].max
32
+
33
+ if deadline
34
+ remaining_wait = deadline - ThrottleMachines.monotonic_time
35
+ raise if remaining_wait <= 0 || wait_time > remaining_wait
36
+ end
37
+
38
+ sleep_for(wait_time)
39
+ next
40
+ end
41
+
42
+ return block.call if block
43
+
44
+ return true
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Use Async's fiber-aware sleep when in an async context.
51
+ def sleep_for(duration)
52
+ if (task = current_async_task)
53
+ task.sleep(duration)
54
+ else
55
+ sleep(duration)
56
+ end
57
+ end
58
+
59
+ def current_async_task
60
+ return nil unless defined?(::Async::Task)
61
+
62
+ if ::Async::Task.respond_to?(:current?)
63
+ ::Async::Task.current?
64
+ else
65
+ ::Async::Task.current
66
+ end
67
+ rescue StandardError
68
+ nil
69
+ end
70
+ end
71
+ end
72
+
73
+ ThrottleMachines::Limiter.include(ThrottleMachines::AsyncSupport)
74
+
75
+ warn "[ThrottleMachines] Async support loaded" if ENV['DEBUG_MATRYOSHKA']
@@ -21,10 +21,10 @@ module ThrottleMachines
21
21
  set_rate_limit_headers(limiter)
22
22
  end
23
23
 
24
- def with_throttle(key = nil, limit:, period:, &)
24
+ def with_throttle(key = nil, limit:, period:, &block)
25
25
  key ||= default_throttle_key
26
26
 
27
- ThrottleMachines.limit(key, limit: limit, period: period, &)
27
+ ThrottleMachines.limit(key, limit: limit, period: period, &block)
28
28
  rescue ThrottledError => e
29
29
  render_rate_limited(e.limiter)
30
30
  end
@@ -13,11 +13,6 @@ module ThrottleMachines
13
13
  end
14
14
  end
15
15
 
16
- initializer 'throttle_machines.configure_defaults' do |_app|
17
- ThrottleMachines.configure do |config|
18
- # Use Rails.cache , user can override
19
- config.store = Rails.cache
20
- end
21
- end
16
+ # No default Rails.cache binding; storage is managed by ThrottleMachines
22
17
  end
23
18
  end
@@ -11,12 +11,12 @@ module ThrottleMachines
11
11
  )
12
12
  end
13
13
 
14
- def run(&)
14
+ def run(&block)
15
15
  @hedged.run do |attempt|
16
16
  breaker = @breakers[attempt]
17
17
  next if breaker.nil?
18
18
 
19
- breaker.call(&)
19
+ breaker.call(&block)
20
20
  end
21
21
  end
22
22
  end
@@ -94,8 +94,8 @@ module ThrottleMachines
94
94
  end
95
95
 
96
96
  # Run async version
97
- def run_async(&)
98
- Concurrent::Promises.future { run(&) }
97
+ def run_async(&block)
98
+ Concurrent::Promises.future { run(&block) }
99
99
  end
100
100
 
101
101
  # Shutdown the executor
@@ -15,7 +15,7 @@ module ThrottleMachines
15
15
  @backend ||= ActiveSupport::Notifications
16
16
  end
17
17
 
18
- def instrument(event_name, payload = {}, &)
18
+ def instrument(event_name, payload = {}, &block)
19
19
  if !enabled || backend.nil?
20
20
  return yield if block_given?
21
21
 
@@ -23,7 +23,7 @@ module ThrottleMachines
23
23
  end
24
24
 
25
25
  full_event_name = "#{event_name}.throttle_machines"
26
- backend.instrument(full_event_name, payload, &)
26
+ backend.instrument(full_event_name, payload, &block)
27
27
  end
28
28
 
29
29
  # Convenience methods for common events
@@ -66,9 +66,9 @@ module ThrottleMachines
66
66
  # Circuit breaker events
67
67
  def circuit_opened(breaker, failure_count:)
68
68
  payload = {
69
- key: breaker.key,
70
- failure_threshold: breaker.failure_threshold,
71
- timeout: breaker.timeout,
69
+ key: (breaker.respond_to?(:name) ? breaker.name : breaker.to_s),
70
+ failure_threshold: (breaker.respond_to?(:configuration) ? breaker.configuration[:failure_threshold] : nil),
71
+ timeout: (breaker.respond_to?(:configuration) ? breaker.configuration[:reset_timeout] : nil),
72
72
  failure_count: failure_count
73
73
  }
74
74
  instrument('circuit_breaker.opened', payload)
@@ -76,35 +76,35 @@ module ThrottleMachines
76
76
 
77
77
  def circuit_closed(breaker)
78
78
  payload = {
79
- key: breaker.key,
80
- failure_threshold: breaker.failure_threshold,
81
- timeout: breaker.timeout
79
+ key: (breaker.respond_to?(:name) ? breaker.name : breaker.to_s),
80
+ failure_threshold: (breaker.respond_to?(:configuration) ? breaker.configuration[:failure_threshold] : nil),
81
+ timeout: (breaker.respond_to?(:configuration) ? breaker.configuration[:reset_timeout] : nil)
82
82
  }
83
83
  instrument('circuit_breaker.closed', payload)
84
84
  end
85
85
 
86
86
  def circuit_half_opened(breaker)
87
87
  payload = {
88
- key: breaker.key,
89
- failure_threshold: breaker.failure_threshold,
90
- timeout: breaker.timeout,
91
- half_open_requests: breaker.half_open_requests
88
+ key: (breaker.respond_to?(:name) ? breaker.name : breaker.to_s),
89
+ failure_threshold: (breaker.respond_to?(:configuration) ? breaker.configuration[:failure_threshold] : nil),
90
+ timeout: (breaker.respond_to?(:configuration) ? breaker.configuration[:reset_timeout] : nil),
91
+ half_open_requests: (breaker.respond_to?(:configuration) ? breaker.configuration[:half_open_calls] : nil)
92
92
  }
93
93
  instrument('circuit_breaker.half_opened', payload)
94
94
  end
95
95
 
96
96
  def circuit_success(breaker)
97
97
  payload = {
98
- key: breaker.key,
99
- state: breaker.state
98
+ key: (breaker.respond_to?(:name) ? breaker.name : breaker.to_s),
99
+ state: (breaker.respond_to?(:to_h) ? breaker.to_h[:state] : nil)
100
100
  }
101
101
  instrument('circuit_breaker.success', payload)
102
102
  end
103
103
 
104
104
  def circuit_failure(breaker, error: nil)
105
105
  payload = {
106
- key: breaker.key,
107
- state: breaker.state,
106
+ key: (breaker.respond_to?(:name) ? breaker.name : breaker.to_s),
107
+ state: (breaker.respond_to?(:to_h) ? breaker.to_h[:state] : nil),
108
108
  error_class: error&.class&.name,
109
109
  error_message: error&.message
110
110
  }
@@ -113,9 +113,9 @@ module ThrottleMachines
113
113
 
114
114
  def circuit_rejected(breaker)
115
115
  payload = {
116
- key: breaker.key,
117
- failure_threshold: breaker.failure_threshold,
118
- timeout: breaker.timeout
116
+ key: (breaker.respond_to?(:name) ? breaker.name : breaker.to_s),
117
+ failure_threshold: (breaker.respond_to?(:configuration) ? breaker.configuration[:failure_threshold] : nil),
118
+ timeout: (breaker.respond_to?(:configuration) ? breaker.configuration[:reset_timeout] : nil)
119
119
  }
120
120
  instrument('circuit_breaker.rejected', payload)
121
121
  end
@@ -2,9 +2,8 @@
2
2
 
3
3
  module ThrottleMachines
4
4
  class Middleware
5
- def initialize(app, store: nil, &config_block)
5
+ def initialize(app, &config_block)
6
6
  @app = app
7
- @store = store || ThrottleMachines.configuration.store
8
7
  @rules = []
9
8
 
10
9
  instance_eval(&config_block) if config_block
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Native speedup for ThrottleMachines using Rust FFI.
4
+ #
5
+ # This module is automatically loaded when the native extension is available.
6
+ # It prepends optimized implementations onto Storage::Memory.
7
+ #
8
+ # Environment controls:
9
+ # DISABLE_MATRYOSHKA_NATIVE=1 - Disable all matryoshka native extensions
10
+ # DISABLE_THROTTLE_MACHINES_NATIVE=1 - Disable only throttle_machines native
11
+ # DEBUG_MATRYOSHKA=1 - Log which backend is loaded
12
+
13
+ return if ENV["DISABLE_MATRYOSHKA_NATIVE"]
14
+ return if ENV["DISABLE_THROTTLE_MACHINES_NATIVE"]
15
+
16
+ begin
17
+ require "throttle_machines_native/throttle_machines_native"
18
+
19
+ module ThrottleMachines
20
+ module Storage
21
+ # Native GCRA implementation using Rust.
22
+ module NativeGCRA
23
+ def check_gcra_limit(key, emission_interval, delay_tolerance, ttl)
24
+ with_write_lock(key) do
25
+ now = current_time
26
+ state = @gcra_states[key] || { tat: 0.0 }
27
+
28
+ # Rust does the math
29
+ ThrottleMachinesNative.gcra_check(
30
+ state[:tat], now, emission_interval, delay_tolerance
31
+ ) => { allowed:, state: new_tat, retry_after: }
32
+
33
+ @gcra_states[key] = { tat: new_tat, expires_at: now + ttl } if allowed
34
+
35
+ { allowed: allowed, retry_after: retry_after, tat: new_tat }
36
+ end
37
+ end
38
+
39
+ def peek_gcra_limit(key, _emission_interval, delay_tolerance)
40
+ with_read_lock(key) do
41
+ now = current_time
42
+ state = @gcra_states[key] || { tat: 0.0 }
43
+
44
+ # Rust does the math
45
+ ThrottleMachinesNative.gcra_peek(
46
+ state[:tat], now, delay_tolerance
47
+ ) => { allowed:, state: tat, retry_after: }
48
+
49
+ { allowed: allowed, retry_after: retry_after, tat: tat }
50
+ end
51
+ end
52
+ end
53
+
54
+ # Native Token Bucket implementation using Rust.
55
+ module NativeTokenBucket
56
+ def check_token_bucket(key, capacity, refill_rate, ttl)
57
+ with_write_lock(key) do
58
+ now = current_time
59
+ bucket = @token_buckets[key] || { tokens: capacity, last_refill: now }
60
+
61
+ # Rust does the math
62
+ ThrottleMachinesNative.token_bucket_check(
63
+ bucket[:tokens], bucket[:last_refill], now, capacity, refill_rate
64
+ ) => { allowed:, state: new_tokens, retry_after: }
65
+
66
+ if allowed
67
+ @token_buckets[key] = { tokens: new_tokens, last_refill: now, expires_at: now + ttl }
68
+ end
69
+
70
+ { allowed: allowed, retry_after: retry_after, tokens_remaining: new_tokens.floor }
71
+ end
72
+ end
73
+
74
+ def peek_token_bucket(key, capacity, refill_rate)
75
+ with_read_lock(key) do
76
+ now = current_time
77
+ bucket = @token_buckets[key] || { tokens: capacity, last_refill: now }
78
+
79
+ # Rust does the math
80
+ ThrottleMachinesNative.token_bucket_peek(
81
+ bucket[:tokens], bucket[:last_refill], now, capacity, refill_rate
82
+ ) => { allowed:, state: tokens, retry_after: }
83
+
84
+ { allowed: allowed, retry_after: retry_after, tokens_remaining: tokens.floor }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Prepend native implementations
92
+ ThrottleMachines::Storage::Memory.prepend(ThrottleMachines::Storage::NativeGCRA)
93
+ ThrottleMachines::Storage::Memory.prepend(ThrottleMachines::Storage::NativeTokenBucket)
94
+
95
+ warn "[ThrottleMachines] Native speedup loaded" if ENV["DEBUG_MATRYOSHKA"]
96
+ rescue LoadError => e
97
+ warn "[ThrottleMachines] Native speedup unavailable: #{e.message}" if ENV["DEBUG_MATRYOSHKA"]
98
+ end
@@ -2,18 +2,7 @@
2
2
 
3
3
  module ThrottleMachines
4
4
  class RackMiddleware
5
- class Allow2Ban
6
- attr_reader :name, :maxretry, :findtime, :bantime, :block
7
-
8
- def initialize(name, options, &block)
9
- @name = name
10
- @block = block
11
-
12
- @maxretry = options[:maxretry] || 5
13
- @findtime = options[:findtime] || 60
14
- @bantime = options[:bantime] || 300
15
- end
16
-
5
+ class Allow2Ban < BanFilter
17
6
  def matched_by?(request)
18
7
  discriminator = discriminator_for(request)
19
8
  return false unless discriminator
@@ -34,12 +23,18 @@ module ThrottleMachines
34
23
 
35
24
  # Check if we've had enough successful requests
36
25
  if success_limiter.remaining.zero?
37
- # Reset the fail2ban breaker
38
- storage = ThrottleMachines.storage
39
- storage.reset_breaker(fail_key)
26
+ # Reset the fail2ban breaker (use BreakerMachines circuit)
27
+ breaker = BreakerMachines::Registry.instance.get_or_create_dynamic_circuit(
28
+ fail_key,
29
+ self,
30
+ failure_threshold: @maxretry,
31
+ failure_window: @findtime,
32
+ reset_timeout: @bantime
33
+ )
34
+ breaker.hard_reset
40
35
 
41
36
  # Reset our own counter
42
- storage.reset_counter(success_key, @findtime)
37
+ ThrottleMachines.storage.reset_counter(success_key, @findtime)
43
38
  else
44
39
  # Increment success counter
45
40
  begin
@@ -51,12 +46,6 @@ module ThrottleMachines
51
46
 
52
47
  false # Allow2Ban never blocks directly
53
48
  end
54
-
55
- private
56
-
57
- def discriminator_for(request)
58
- @block.call(request)
59
- end
60
49
  end
61
50
  end
62
51
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ class RackMiddleware
5
+ # Base class for Fail2Ban and Allow2Ban filters.
6
+ #
7
+ # Both are configured with the same maxretry/findtime/bantime thresholds and
8
+ # derive a discriminator from the request via the supplied block. The ban
9
+ # logic itself lives in each subclass.
10
+ class BanFilter
11
+ attr_reader :name, :maxretry, :findtime, :bantime, :block
12
+
13
+ def initialize(name, options, &block)
14
+ @name = name
15
+ @block = block
16
+
17
+ @maxretry = options[:maxretry] || 5
18
+ @findtime = options[:findtime] || 60
19
+ @bantime = options[:bantime] || 300
20
+ end
21
+
22
+ private
23
+
24
+ def discriminator_for(request)
25
+ @block.call(request)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,25 +2,12 @@
2
2
 
3
3
  module ThrottleMachines
4
4
  class RackMiddleware
5
- class Blocklist
6
- attr_reader :name, :block
5
+ # Blocks requests matching the configured block (returns true => blocked).
6
+ class Blocklist < ListFilter
7
+ private
7
8
 
8
- def initialize(name, &block)
9
- @name = name
10
- @block = block
11
- end
12
-
13
- def matched_by?(request)
14
- return false unless @block
15
-
16
- if @block.call(request)
17
- request.env['rack.attack.matched'] = @name
18
- request.env['rack.attack.match_type'] = :blocklist
19
- ThrottleMachines::RackMiddleware.instrument(request)
20
- true
21
- else
22
- false
23
- end
9
+ def match_type
10
+ :blocklist
24
11
  end
25
12
  end
26
13
  end
@@ -32,27 +32,27 @@ module ThrottleMachines
32
32
  end
33
33
 
34
34
  # DSL Methods
35
- def throttle(name, options = {}, &)
36
- @throttles[name] = Throttle.new(name, options, &)
35
+ def throttle(name, options = {}, &block)
36
+ @throttles[name] = Throttle.new(name, options, &block)
37
37
  end
38
38
 
39
- def track(name, options = {}, &)
40
- @tracks[name] = Track.new(name, options, &)
39
+ def track(name, options = {}, &block)
40
+ @tracks[name] = Track.new(name, options, &block)
41
41
  end
42
42
 
43
- def safelist(name = nil, &)
43
+ def safelist(name = nil, &block)
44
44
  if name
45
- @safelists[name] = Safelist.new(name, &)
45
+ @safelists[name] = Safelist.new(name, &block)
46
46
  else
47
- @anonymous_safelists << Safelist.new(nil, &)
47
+ @anonymous_safelists << Safelist.new(nil, &block)
48
48
  end
49
49
  end
50
50
 
51
- def blocklist(name = nil, &)
51
+ def blocklist(name = nil, &block)
52
52
  if name
53
- @blocklists[name] = Blocklist.new(name, &)
53
+ @blocklists[name] = Blocklist.new(name, &block)
54
54
  else
55
- @anonymous_blocklists << Blocklist.new(nil, &)
55
+ @anonymous_blocklists << Blocklist.new(nil, &block)
56
56
  end
57
57
  end
58
58
 
@@ -64,12 +64,12 @@ module ThrottleMachines
64
64
  @anonymous_blocklists << Blocklist.new(nil) { |req| req.ip == ip_address }
65
65
  end
66
66
 
67
- def fail2ban(name, options = {}, &)
68
- @fail2bans[name] = Fail2Ban.new(name, options, &)
67
+ def fail2ban(name, options = {}, &block)
68
+ @fail2bans[name] = Fail2Ban.new(name, options, &block)
69
69
  end
70
70
 
71
- def allow2ban(name, options = {}, &)
72
- @allow2bans[name] = Allow2Ban.new(name, options, &)
71
+ def allow2ban(name, options = {}, &block)
72
+ @allow2bans[name] = Allow2Ban.new(name, options, &block)
73
73
  end
74
74
 
75
75
  # Check methods