semian 0.27.0 → 0.28.0

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.
data/lib/semian.rb CHANGED
@@ -11,6 +11,8 @@ require "semian/instrumentable"
11
11
  require "semian/platform"
12
12
  require "semian/resource"
13
13
  require "semian/circuit_breaker"
14
+ require "semian/adaptive_circuit_breaker"
15
+ require "semian/dual_circuit_breaker"
14
16
  require "semian/protected_resource"
15
17
  require "semian/unprotected_resource"
16
18
  require "semian/simple_sliding_window"
@@ -197,6 +199,32 @@ module Semian
197
199
  # +exceptions+: An array of exception classes that should be accounted as resource errors. Default [].
198
200
  # (circuit breaker)
199
201
  #
202
+ # # +exponential_backoff_error_timeout+: When set to true, instead of opening the circuit for the full
203
+ # error_timeout duration, it starts with a smaller timeout and increases exponentially on each subsequent
204
+ # opening up to error_timeout. This helps avoid over-opening the circuit for temporary issues.
205
+ # Default false. (circuit breaker)
206
+ #
207
+ # +exponential_backoff_initial_timeout+: The initial timeout in seconds when exponential backoff is enabled.
208
+ # Only valid when exponential_backoff_error_timeout is true. Default 1. (circuit breaker)
209
+ #
210
+ # +exponential_backoff_multiplier+: The factor by which to multiply the timeout on each subsequent opening
211
+ # when exponential backoff is enabled. Only valid when exponential_backoff_error_timeout is true.
212
+ # Default 2. (circuit breaker)
213
+ #
214
+ # +adaptive_circuit_breaker+: Enable adaptive circuit breaker using PID controller. Default false.
215
+ # When enabled, this replaces the traditional circuit breaker with an adaptive version
216
+ # that dynamically adjusts rejection rates based on service health. (adaptive circuit breaker)
217
+ #
218
+ # +dual_circuit_breaker+: Enable dual circuit breaker mode where both legacy and adaptive
219
+ # circuit breakers are initialized. Default false. When enabled, both circuit breakers track
220
+ # requests, but only one is used for decision-making based on use_adaptive.
221
+ # (dual circuit breaker)
222
+ #
223
+ # +use_adaptive+: A callable (Proc/lambda) that returns true to use adaptive circuit breaker
224
+ # or false to use legacy. Only used when dual_circuit_breaker is enabled. Default: ->() { false }.
225
+ # Example: ->() { MyFeatureFlag.enabled?(:adaptive_circuit_breaker) }
226
+ # (dual circuit breaker)
227
+ #
200
228
  # Returns the registered resource.
201
229
  def register(name, **options)
202
230
  return UnprotectedResource.new(name) if ENV.key?("SEMIAN_DISABLED")
@@ -204,7 +232,14 @@ module Semian
204
232
  # Validate configuration before proceeding
205
233
  ConfigurationValidator.new(name, options).validate!
206
234
 
207
- circuit_breaker = create_circuit_breaker(name, **options)
235
+ circuit_breaker = if options[:dual_circuit_breaker]
236
+ create_dual_circuit_breaker(name, **options)
237
+ elsif options[:adaptive_circuit_breaker]
238
+ create_adaptive_circuit_breaker(name, **options)
239
+ else
240
+ create_circuit_breaker(name, **options)
241
+ end
242
+
208
243
  bulkhead = create_bulkhead(name, **options)
209
244
 
210
245
  resources[name] = ProtectedResource.new(name, bulkhead, circuit_breaker)
@@ -300,12 +335,49 @@ module Semian
300
335
 
301
336
  private
302
337
 
303
- def create_circuit_breaker(name, **options)
338
+ def create_dual_circuit_breaker(name, **options)
339
+ return if ENV.key?("SEMIAN_CIRCUIT_BREAKER_DISABLED")
340
+
341
+ classic_cb = create_circuit_breaker(name, is_child: true, **options)
342
+ adaptive_cb = create_adaptive_circuit_breaker(name, is_child: true, **options)
343
+
344
+ DualCircuitBreaker.new(
345
+ name: name,
346
+ classic_circuit_breaker: classic_cb,
347
+ adaptive_circuit_breaker: adaptive_cb,
348
+ )
349
+ end
350
+
351
+ def create_adaptive_circuit_breaker(name, is_child: false, **options)
352
+ return if ENV.key?("SEMIAN_CIRCUIT_BREAKER_DISABLED")
353
+
354
+ exceptions = options[:exceptions] || []
355
+ cls = is_child ? DualCircuitBreaker::ChildAdaptiveCircuitBreaker : AdaptiveCircuitBreaker
356
+ cls.new(
357
+ name: name,
358
+ exceptions: Array(exceptions) + [::Semian::BaseError],
359
+ kp: options[:kp] || 1.0,
360
+ ki: options[:ki] || 0.2,
361
+ kd: options[:kd] || 0.0,
362
+ window_size: options[:window_size] || 10,
363
+ initial_error_rate: options[:initial_error_rate] || 0.05,
364
+ dead_zone_ratio: options[:dead_zone_ratio] || 0.25,
365
+ # We use an environment vraiable for the sliding interval because it is shared among all circuit breakers
366
+ sliding_interval: ENV.fetch("SEMIAN_ADAPTIVE_CIRCUIT_BREAKER_SLIDING_INTERVAL", 1).to_i,
367
+ ideal_error_rate_estimator_cap_value: options[:ideal_error_rate_estimator_cap_value] || 0.1,
368
+ integral_upper_cap: options[:integral_upper_cap] || 10.0,
369
+ integral_lower_cap: options[:integral_lower_cap] || -10.0,
370
+ implementation: implementation(**options),
371
+ )
372
+ end
373
+
374
+ def create_circuit_breaker(name, is_child: false, **options)
304
375
  return if ENV.key?("SEMIAN_CIRCUIT_BREAKER_DISABLED")
305
376
  return unless options.fetch(:circuit_breaker, true)
306
377
 
307
378
  exceptions = options[:exceptions] || []
308
- CircuitBreaker.new(
379
+ cls = is_child ? DualCircuitBreaker::ChildClassicCircuitBreaker : CircuitBreaker
380
+ cls.new(
309
381
  name,
310
382
  success_threshold: options[:success_threshold],
311
383
  error_threshold: options[:error_threshold],
@@ -323,6 +395,9 @@ module Semian
323
395
  end,
324
396
  exceptions: Array(exceptions) + [::Semian::BaseError],
325
397
  half_open_resource_timeout: options[:half_open_resource_timeout],
398
+ exponential_backoff_error_timeout: options[:exponential_backoff_error_timeout] || false,
399
+ exponential_backoff_initial_timeout: options[:exponential_backoff_initial_timeout] || 1,
400
+ exponential_backoff_multiplier: options[:exponential_backoff_multiplier] || 2,
326
401
  implementation: implementation(**options),
327
402
  )
328
403
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.0
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Francis
@@ -51,13 +51,18 @@ files:
51
51
  - lib/semian/activerecord_postgresql_adapter.rb
52
52
  - lib/semian/activerecord_trilogy_adapter.rb
53
53
  - lib/semian/adapter.rb
54
+ - lib/semian/adaptive_circuit_breaker.rb
54
55
  - lib/semian/circuit_breaker.rb
56
+ - lib/semian/circuit_breaker_behaviour.rb
55
57
  - lib/semian/configuration_validator.rb
58
+ - lib/semian/dual_circuit_breaker.rb
56
59
  - lib/semian/grpc.rb
57
60
  - lib/semian/instrumentable.rb
58
61
  - lib/semian/lru_hash.rb
59
62
  - lib/semian/mysql2.rb
60
63
  - lib/semian/net_http.rb
64
+ - lib/semian/pid_controller.rb
65
+ - lib/semian/pid_controller_thread.rb
61
66
  - lib/semian/platform.rb
62
67
  - lib/semian/protected_resource.rb
63
68
  - lib/semian/rails.rb
@@ -65,6 +70,7 @@ files:
65
70
  - lib/semian/redis/v5.rb
66
71
  - lib/semian/redis_client.rb
67
72
  - lib/semian/resource.rb
73
+ - lib/semian/simple_exponential_smoother.rb
68
74
  - lib/semian/simple_integer.rb
69
75
  - lib/semian/simple_sliding_window.rb
70
76
  - lib/semian/simple_state.rb
@@ -94,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
100
  - !ruby/object:Gem::Version
95
101
  version: '0'
96
102
  requirements: []
97
- rubygems_version: 4.0.4
103
+ rubygems_version: 4.0.8
98
104
  specification_version: 4
99
105
  summary: Bulkheading for Ruby with SysV semaphores
100
106
  test_files: []