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
@@ -3,7 +3,7 @@
3
3
  require 'json'
4
4
  require 'timeout'
5
5
  require 'zeitwerk'
6
- require 'active_support/configurable'
6
+ require 'active_support/core_ext/class/attribute'
7
7
 
8
8
  # Ecosystem dependencies
9
9
  require 'chrono_machines'
@@ -15,27 +15,53 @@ loader.ignore("#{__dir__}/throttle_machines/engine.rb") unless defined?(Rails::E
15
15
  loader.setup
16
16
 
17
17
  module ThrottleMachines
18
- include ActiveSupport::Configurable
18
+ class Configuration
19
+ attr_accessor :default_limit, :default_period, :default_storage, :clock,
20
+ :default_algorithm, :instrumentation_enabled, :instrumentation_backend,
21
+ :_storage_instance
22
+
23
+ def initialize
24
+ @default_limit = 100
25
+ @default_period = 60 # 1 minute
26
+ @default_storage = :memory
27
+ @default_algorithm = :fixed_window
28
+ @clock = nil
29
+ @instrumentation_enabled = true
30
+ @instrumentation_backend = nil
31
+ @fiber_safe = false
32
+ @_storage_instance = nil
33
+ end
34
+
35
+ def fiber_safe
36
+ @fiber_safe
37
+ end
19
38
 
20
- # Define configuration options with defaults
21
- config_accessor :default_limit, default: 100
22
- config_accessor :default_period, default: 60 # 1 minute
23
- config_accessor :default_storage, default: :memory
24
- config_accessor :clock, default: nil
25
- config_accessor :instrumentation_enabled, default: true
26
- config_accessor :instrumentation_backend
27
- config_accessor :_storage_instance
39
+ def fiber_safe=(value)
40
+ @fiber_safe = !!value
41
+ ThrottleMachines.load_async_runtime! if @fiber_safe && ThrottleMachines.respond_to?(:load_async_runtime!)
42
+ end
43
+
44
+ alias async_enabled fiber_safe
45
+ alias async_enabled= fiber_safe=
46
+ end
47
+
48
+ @config = Configuration.new
28
49
 
29
50
  class << self
30
51
  # Delegate monotonic time to BreakerMachines for consistency
31
52
  delegate :monotonic_time, to: :BreakerMachines
32
53
 
54
+ def config
55
+ @config
56
+ end
57
+
33
58
  def configure
34
59
  yield(config) if block_given?
35
60
 
36
61
  # Apply instrumentation settings
37
62
  Instrumentation.enabled = config.instrumentation_enabled
38
63
  Instrumentation.backend = config.instrumentation_backend if config.instrumentation_backend
64
+ load_async_runtime! if config.fiber_safe
39
65
  end
40
66
 
41
67
  def storage
@@ -65,32 +91,52 @@ module ThrottleMachines
65
91
  control
66
92
  end
67
93
 
68
- def limit(key, limit:, period:, algorithm: :fixed_window, &)
94
+ def limit(key, limit:, period:, algorithm: nil, &block)
69
95
  limiter = limiter(key, limit: limit, period: period, algorithm: algorithm)
70
- limiter.throttle!(&)
96
+ limiter.throttle!(&block)
71
97
  end
72
98
 
73
- def break_circuit(key, failures:, timeout:, &)
74
- # Delegate to breaker_machines
99
+ def break_circuit(key, failures:, timeout:, &block)
100
+ # Delegate to BreakerMachines; use reset_timeout for open duration
75
101
  breaker = BreakerMachines::Circuit.new(
76
- key: key,
102
+ key,
77
103
  failure_threshold: failures,
78
- timeout: timeout
104
+ reset_timeout: timeout
79
105
  )
80
- breaker.call(&)
106
+ breaker.call(&block)
81
107
  end
82
108
 
83
- def retry_with(max_attempts: 3, backoff: :exponential, &)
109
+ def retry_with(max_attempts: 3, backoff: :exponential, &block)
84
110
  # Delegate to chrono_machines
85
111
  policy_options = {
86
112
  max_attempts: max_attempts,
87
113
  jitter_factor: backoff == :exponential ? 1.0 : 0.0
88
114
  }
89
- ChronoMachines.retry(policy_options, &)
115
+ ChronoMachines.retry(policy_options, &block)
90
116
  end
91
117
 
92
- def limiter(key, limit:, period:, algorithm: :fixed_window)
93
- Limiter.new(key, limit: limit, period: period, algorithm: algorithm, storage: storage)
118
+ def limiter(key, limit:, period:, algorithm: nil)
119
+ Limiter.new(
120
+ key,
121
+ limit: limit,
122
+ period: period,
123
+ algorithm: algorithm || config.default_algorithm,
124
+ storage: storage
125
+ )
126
+ end
127
+
128
+ def async_breaker(...)
129
+ ThrottleMachines::AsyncBreaker.new(...)
130
+ end
131
+
132
+ def load_async_runtime!
133
+ require 'async'
134
+ require 'async/task'
135
+ true
136
+ rescue LoadError => e
137
+ raise unless e.message.include?('async')
138
+
139
+ raise LoadError, "The 'async' gem is required for fiber_safe mode. Add `gem 'async'` to your Gemfile."
94
140
  end
95
141
 
96
142
  private
@@ -131,4 +177,24 @@ module ThrottleMachines
131
177
 
132
178
  CircuitOpenError = BreakerMachines::CircuitOpenError
133
179
  RetryExhaustedError = ChronoMachines::MaxRetriesExceededError
180
+
181
+ # Back-compat wrapper: use BreakerMachines as the circuit implementation
182
+ Breaker = BreakerMachines::Circuit
183
+ end
184
+
185
+ # Optional speedups (order matters: most specific first)
186
+ # These are loaded after Zeitwerk setup to allow graceful fallback.
187
+
188
+ # Native Rust extension for performance-critical algorithms
189
+ begin
190
+ require_relative "throttle_machines/native_speedup"
191
+ rescue LoadError
192
+ # Native extension not available, pure Ruby will be used
193
+ end
194
+
195
+ # Async/Fiber support when Async gem is loaded
196
+ begin
197
+ require_relative "throttle_machines/async_support"
198
+ rescue LoadError
199
+ # Async gem not available
134
200
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: throttle_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.0'
18
+ version: 8.0.4
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '7.0'
25
+ version: 8.0.4
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: concurrent-ruby
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -71,28 +71,84 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '0.4'
74
+ version: '0.19'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '0.4'
81
+ version: '0.19'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: chrono_machines
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
- - - ">="
86
+ - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '0.2'
88
+ version: '0.9'
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
- - - ">="
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.9'
96
+ - !ruby/object:Gem::Dependency
97
+ name: minitest
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '5.16'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '5.16'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rb_sys
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.9'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.9'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rake
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
94
129
  - !ruby/object:Gem::Version
95
- version: '0.2'
130
+ version: '13.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '13.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rake-compiler
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '1.3'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '1.3'
96
152
  description: ThrottleMachines provides ultra-thin, elegant rate limiting with temporal
97
153
  precision for distributed systems.
98
154
  email:
@@ -103,9 +159,10 @@ extra_rdoc_files: []
103
159
  files:
104
160
  - LICENSE
105
161
  - README.md
106
- - Rakefile
107
162
  - lib/throttle_machines.rb
163
+ - lib/throttle_machines/async_breaker.rb
108
164
  - lib/throttle_machines/async_limiter.rb
165
+ - lib/throttle_machines/async_support.rb
109
166
  - lib/throttle_machines/control.rb
110
167
  - lib/throttle_machines/controller_helpers.rb
111
168
  - lib/throttle_machines/dependency_error.rb
@@ -115,11 +172,14 @@ files:
115
172
  - lib/throttle_machines/instrumentation.rb
116
173
  - lib/throttle_machines/limiter.rb
117
174
  - lib/throttle_machines/middleware.rb
175
+ - lib/throttle_machines/native_speedup.rb
118
176
  - lib/throttle_machines/rack_middleware.rb
119
177
  - lib/throttle_machines/rack_middleware/allow2_ban.rb
178
+ - lib/throttle_machines/rack_middleware/ban_filter.rb
120
179
  - lib/throttle_machines/rack_middleware/blocklist.rb
121
180
  - lib/throttle_machines/rack_middleware/configuration.rb
122
181
  - lib/throttle_machines/rack_middleware/fail2_ban.rb
182
+ - lib/throttle_machines/rack_middleware/list_filter.rb
123
183
  - lib/throttle_machines/rack_middleware/request.rb
124
184
  - lib/throttle_machines/rack_middleware/safelist.rb
125
185
  - lib/throttle_machines/rack_middleware/throttle.rb
@@ -129,12 +189,9 @@ files:
129
189
  - lib/throttle_machines/storage/null.rb
130
190
  - lib/throttle_machines/storage/redis.rb
131
191
  - lib/throttle_machines/storage/redis/gcra.lua
132
- - lib/throttle_machines/storage/redis/get_breaker_state.lua
133
192
  - lib/throttle_machines/storage/redis/increment_counter.lua
134
193
  - lib/throttle_machines/storage/redis/peek_gcra.lua
135
194
  - lib/throttle_machines/storage/redis/peek_token_bucket.lua
136
- - lib/throttle_machines/storage/redis/record_breaker_failure.lua
137
- - lib/throttle_machines/storage/redis/record_breaker_success.lua
138
195
  - lib/throttle_machines/storage/redis/token_bucket.lua
139
196
  - lib/throttle_machines/throttled_error.rb
140
197
  - lib/throttle_machines/version.rb
@@ -146,6 +203,8 @@ metadata:
146
203
  source_code_uri: https://github.com/seuros/throttle_machines
147
204
  changelog_uri: https://github.com/seuros/throttle_machines/blob/main/CHANGELOG.md
148
205
  rubygems_mfa_required: 'true'
206
+ cargo_crate_name: throttle_machines_native
207
+ cargo_manifest_path: ext/throttle_machines_native/ffi/Cargo.toml
149
208
  rdoc_options: []
150
209
  require_paths:
151
210
  - lib
@@ -153,14 +212,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
153
212
  requirements:
154
213
  - - ">="
155
214
  - !ruby/object:Gem::Version
156
- version: 3.3.0
215
+ version: 4.0.0
157
216
  required_rubygems_version: !ruby/object:Gem::Requirement
158
217
  requirements:
159
218
  - - ">="
160
219
  - !ruby/object:Gem::Version
161
220
  version: '0'
162
221
  requirements: []
163
- rubygems_version: 3.6.9
222
+ rubygems_version: 4.0.20
164
223
  specification_version: 4
165
224
  summary: Advanced Rate limiting for Ruby applications
166
225
  test_files: []
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rake/testtask'
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << 'test'
8
- t.test_files = FileList['test/**/*_test.rb'].exclude('test/dummy/**/*')
9
- t.verbose = false
10
- end
11
-
12
- task default: :test
@@ -1,23 +0,0 @@
1
- local data = redis.call('HGETALL', KEYS[1])
2
- if #data == 0 then
3
- return {}
4
- end
5
-
6
- local state = {}
7
- for i = 1, #data, 2 do
8
- state[data[i]] = data[i + 1]
9
- end
10
-
11
- -- Auto-transition from open to half-open if timeout passed
12
- if state['state'] == 'open' and state['opens_at'] then
13
- local now = tonumber(ARGV[1])
14
- local opens_at = tonumber(state['opens_at'])
15
-
16
- if now >= opens_at then
17
- redis.call('HSET', KEYS[1], 'state', 'half_open', 'half_open_attempts', '0')
18
- state['state'] = 'half_open'
19
- state['half_open_attempts'] = '0'
20
- end
21
- end
22
-
23
- return state
@@ -1,24 +0,0 @@
1
- local state = redis.call('HGET', KEYS[1], 'state') or 'closed'
2
- local now = ARGV[3]
3
- local timeout = tonumber(ARGV[2])
4
-
5
- if state == 'half_open' then
6
- -- Failure in half-open state, just re-open the circuit
7
- redis.call('HMSET', KEYS[1],
8
- 'state', 'open',
9
- 'opens_at', tonumber(now) + timeout,
10
- 'last_failure', now
11
- )
12
- else -- state is 'closed' or nil
13
- local failures = redis.call('HINCRBY', KEYS[1], 'failures', 1)
14
- redis.call('HSET', KEYS[1], 'last_failure', now)
15
-
16
- if failures >= tonumber(ARGV[1]) then
17
- redis.call('HMSET', KEYS[1],
18
- 'state', 'open',
19
- 'opens_at', tonumber(now) + timeout
20
- )
21
- end
22
- end
23
-
24
- redis.call('EXPIRE', KEYS[1], timeout * 2)
@@ -1,16 +0,0 @@
1
- local state = redis.call('HGET', KEYS[1], 'state')
2
-
3
- if state == 'half_open' then
4
- -- Increment half-open attempts and potentially close the circuit
5
- local attempts = redis.call('HINCRBY', KEYS[1], 'half_open_attempts', 1)
6
-
7
- if attempts >= tonumber(ARGV[1]) then
8
- redis.call('DEL', KEYS[1])
9
- end
10
- elseif state == 'closed' then
11
- -- Reset failure count on success in closed state
12
- local failures = redis.call('HGET', KEYS[1], 'failures')
13
- if failures and tonumber(failures) > 0 then
14
- redis.call('HSET', KEYS[1], 'failures', 0)
15
- end
16
- end