semian 0.27.0 → 0.27.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc25bcc8e0291e3f53d1ecb6ac19e56b375ce0c16974d16c20b9a847b6d2104d
4
- data.tar.gz: 8c4ab8f6259e8118fa94801b28392738a05403d346941738f68bea258bc41f37
3
+ metadata.gz: b84efc35cf9e47382fc7ac82d69ff0bfb55028eb79c8e4d76127b1e4ee8b7053
4
+ data.tar.gz: 0e16ab39a45133600134574abd65012dae026cfa1f1020695974edf46d00d41d
5
5
  SHA512:
6
- metadata.gz: 4f0936f08aa93d042241bece483742cc03d9e50ebaf27a363aec1973026d989ee3c6c6067de13f9016a46b5a227fb40ac2136746c8da25d33895f5a4312da510
7
- data.tar.gz: 6cada0bbab474810b4bc12617e2d6e323034e6b088cebfa0d41352f0f1c8e522b4fc37d2e9dde77328c1ac5a7fc76e34e76b94f313313da9363049a72fb83859
6
+ metadata.gz: abb82539122c2b4ef05420bc996d67cbcd0da9cafd3c735cd85547dd3691af6a6e287f52034d9e2870195ee13708f94e3edf68ad11962e2ad0b6c9023e4243ec
7
+ data.tar.gz: 9041cb2e5834339f584c951558ea071dfd91afc1502971495ab55890963c0ce401ac5b7b1b51611f1d51a3d81618dafb2f2ab648ec8811d7f20f999face6653d
data/README.md CHANGED
@@ -588,6 +588,10 @@ There are four configuration parameters for circuit breakers in Semian:
588
588
  Defaults to `error_timeout` seconds if not set.
589
589
  - **error_timeout**. The amount of time in seconds until trying to query the resource
590
590
  again.
591
+ - **exponential_backoff_error_timeout**. If set to `true`, we will progress towards error_timeout exponentially, instead of committing to it directly.
592
+ This is useful to avoid rejecting too many requests if the dependency is not really degraded.
593
+ - **exponential_backoff_initial_timeout**. Where to start the exponential backoff towards `error_timeout` from. Defaults to 1 second.
594
+ - **exponential_backoff_multiplier**. The exponential multiplier to use during the exponential backoff towards the `error_timeout`. Defaults to 2.
591
595
  - **error_threshold_timeout_enabled**. If set to false it will disable
592
596
  the time window for evicting old exceptions. `error_timeout` is still used and
593
597
  will reset the circuit. Defaults to `true` if not set.
@@ -13,12 +13,16 @@ module Semian
13
13
  :state,
14
14
  :last_error,
15
15
  :error_threshold_timeout_enabled,
16
+ :exponential_backoff_error_timeout,
17
+ :exponential_backoff_initial_timeout,
18
+ :exponential_backoff_multiplier,
16
19
  )
17
20
 
18
21
  def initialize(name, exceptions:, success_threshold:, error_threshold:,
19
22
  error_timeout:, implementation:, half_open_resource_timeout: nil,
20
23
  error_threshold_timeout: nil, error_threshold_timeout_enabled: true,
21
- lumping_interval: 0)
24
+ lumping_interval: 0, exponential_backoff_error_timeout: false,
25
+ exponential_backoff_initial_timeout: 1, exponential_backoff_multiplier: 2)
22
26
  @name = name.to_sym
23
27
  @success_count_threshold = success_threshold
24
28
  @error_count_threshold = error_threshold
@@ -28,6 +32,10 @@ module Semian
28
32
  @exceptions = exceptions
29
33
  @half_open_resource_timeout = half_open_resource_timeout
30
34
  @lumping_interval = lumping_interval
35
+ @exponential_backoff_error_timeout = exponential_backoff_error_timeout
36
+ @exponential_backoff_initial_timeout = exponential_backoff_initial_timeout
37
+ @exponential_backoff_multiplier = exponential_backoff_multiplier
38
+ @current_error_timeout = exponential_backoff_error_timeout ? exponential_backoff_initial_timeout : error_timeout
31
39
 
32
40
  @errors = implementation::SlidingWindow.new(max_size: @error_count_threshold)
33
41
  @successes = implementation::Integer.new
@@ -102,6 +110,8 @@ module Semian
102
110
  log_state_transition(:closed)
103
111
  @state.close!
104
112
  @errors.clear
113
+ # Reset exponential backoff when circuit closes
114
+ @current_error_timeout = @exponential_backoff_error_timeout ? @exponential_backoff_initial_timeout : @error_timeout
105
115
  end
106
116
 
107
117
  def transition_to_open
@@ -115,6 +125,10 @@ module Semian
115
125
  log_state_transition(:half_open)
116
126
  @state.half_open!
117
127
  @successes.reset
128
+ # Multiply the backoff timeout when circuit opens (up to the max error_timeout)
129
+ if @exponential_backoff_error_timeout && @current_error_timeout < @error_timeout
130
+ @current_error_timeout = [@current_error_timeout * @exponential_backoff_multiplier, @error_timeout].min
131
+ end
118
132
  end
119
133
 
120
134
  def success_threshold_reached?
@@ -129,7 +143,7 @@ module Semian
129
143
  last_error_time = @errors.last
130
144
  return false unless last_error_time
131
145
 
132
- last_error_time + @error_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+ last_error_time + @current_error_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
133
147
  end
134
148
 
135
149
  def push_error(error)
@@ -153,6 +167,9 @@ module Semian
153
167
  str += " success_count_threshold=#{@success_count_threshold}"
154
168
  str += " error_count_threshold=#{@error_count_threshold}"
155
169
  str += " error_timeout=#{@error_timeout} error_last_at=\"#{@errors.last}\""
170
+ if @exponential_backoff_error_timeout
171
+ str += " current_error_timeout=#{@current_error_timeout}"
172
+ end
156
173
  str += " name=\"#{@name}\""
157
174
  if new_state == :open && @last_error
158
175
  str += " last_error_message=#{@last_error.message.inspect}"
@@ -103,6 +103,7 @@ module Semian
103
103
  error_threshold = @configuration[:error_threshold]
104
104
  lumping_interval = @configuration[:lumping_interval]
105
105
  half_open_resource_timeout = @configuration[:half_open_resource_timeout]
106
+ exponential_backoff_error_timeout = @configuration[:exponential_backoff_error_timeout]
106
107
 
107
108
  unless error_timeout.is_a?(Numeric) && error_timeout > 0
108
109
  err = "error_timeout must be a positive number, got #{error_timeout}"
@@ -174,6 +175,56 @@ module Semian
174
175
 
175
176
  raise_or_log_validation_required!(err)
176
177
  end
178
+
179
+ unless exponential_backoff_error_timeout.nil? || [true, false].include?(exponential_backoff_error_timeout)
180
+ err = "exponential_backoff_error_timeout must be a boolean, got #{exponential_backoff_error_timeout}"
181
+ err += hint_format("Use true to enable exponential backoff for error timeout. Use false to disable.")
182
+
183
+ raise_or_log_validation_required!(err)
184
+ end
185
+
186
+ # Validate exponential backoff initial timeout
187
+ exponential_backoff_initial_timeout = @configuration[:exponential_backoff_initial_timeout]
188
+ unless exponential_backoff_initial_timeout.nil? || (exponential_backoff_initial_timeout.is_a?(Numeric) && exponential_backoff_initial_timeout > 0)
189
+ err = "exponential_backoff_initial_timeout must be a positive number, got #{exponential_backoff_initial_timeout}"
190
+ err += hint_format("This is the initial timeout when exponential backoff is enabled. Must be less than error_timeout.")
191
+
192
+ raise_or_log_validation_required!(err)
193
+ end
194
+
195
+ # Validate exponential backoff multiplier
196
+ exponential_backoff_multiplier = @configuration[:exponential_backoff_multiplier]
197
+ unless exponential_backoff_multiplier.nil? || (exponential_backoff_multiplier.is_a?(Numeric) && exponential_backoff_multiplier > 1)
198
+ err = "exponential_backoff_multiplier must be a number greater than 1, got #{exponential_backoff_multiplier}"
199
+ err += hint_format("This is the factor by which the timeout increases on each subsequent opening. Common values are 2 (double) or 1.5.")
200
+
201
+ raise_or_log_validation_required!(err)
202
+ end
203
+
204
+ # Ensure exponential backoff parameters are only provided when exponential_backoff_error_timeout is true
205
+ unless exponential_backoff_error_timeout
206
+ if exponential_backoff_initial_timeout
207
+ err = "exponential_backoff_initial_timeout can only be specified when exponential_backoff_error_timeout is true"
208
+ err += hint_format("Set exponential_backoff_error_timeout: true to use exponential backoff features.")
209
+
210
+ raise_or_log_validation_required!(err)
211
+ end
212
+
213
+ if exponential_backoff_multiplier
214
+ err = "exponential_backoff_multiplier can only be specified when exponential_backoff_error_timeout is true"
215
+ err += hint_format("Set exponential_backoff_error_timeout: true to use exponential backoff features.")
216
+
217
+ raise_or_log_validation_required!(err)
218
+ end
219
+ end
220
+
221
+ # Ensure initial timeout is less than error_timeout when using exponential backoff
222
+ if exponential_backoff_error_timeout && exponential_backoff_initial_timeout && exponential_backoff_initial_timeout >= error_timeout
223
+ err = "exponential_backoff_initial_timeout (#{exponential_backoff_initial_timeout}) must be less than error_timeout (#{error_timeout})"
224
+ err += hint_format("The initial timeout should be smaller than the maximum timeout for exponential backoff to be effective.")
225
+
226
+ raise_or_log_validation_required!(err)
227
+ end
177
228
  end
178
229
 
179
230
  def validate_quota!(quota)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Semian
4
- VERSION = "0.27.0"
4
+ VERSION = "0.27.1"
5
5
  end
data/lib/semian.rb CHANGED
@@ -197,6 +197,18 @@ module Semian
197
197
  # +exceptions+: An array of exception classes that should be accounted as resource errors. Default [].
198
198
  # (circuit breaker)
199
199
  #
200
+ # +exponential_backoff_error_timeout+: When set to true, instead of opening the circuit for the full
201
+ # error_timeout duration, it starts with a smaller timeout and increases exponentially on each subsequent
202
+ # opening up to error_timeout. This helps avoid over-opening the circuit for temporary issues.
203
+ # Default false. (circuit breaker)
204
+ #
205
+ # +exponential_backoff_initial_timeout+: The initial timeout in seconds when exponential backoff is enabled.
206
+ # Only valid when exponential_backoff_error_timeout is true. Default 1. (circuit breaker)
207
+ #
208
+ # +exponential_backoff_multiplier+: The factor by which to multiply the timeout on each subsequent opening
209
+ # when exponential backoff is enabled. Only valid when exponential_backoff_error_timeout is true.
210
+ # Default 2. (circuit breaker)
211
+ #
200
212
  # Returns the registered resource.
201
213
  def register(name, **options)
202
214
  return UnprotectedResource.new(name) if ENV.key?("SEMIAN_DISABLED")
@@ -323,6 +335,9 @@ module Semian
323
335
  end,
324
336
  exceptions: Array(exceptions) + [::Semian::BaseError],
325
337
  half_open_resource_timeout: options[:half_open_resource_timeout],
338
+ exponential_backoff_error_timeout: options[:exponential_backoff_error_timeout] || false,
339
+ exponential_backoff_initial_timeout: options[:exponential_backoff_initial_timeout] || 1,
340
+ exponential_backoff_multiplier: options[:exponential_backoff_multiplier] || 2,
326
341
  implementation: implementation(**options),
327
342
  )
328
343
  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.27.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Francis
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
96
  requirements: []
97
- rubygems_version: 4.0.4
97
+ rubygems_version: 4.0.6
98
98
  specification_version: 4
99
99
  summary: Bulkheading for Ruby with SysV semaphores
100
100
  test_files: []