stoplight 4.1.0 → 5.0.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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +289 -350
  3. data/lib/stoplight/admin/actions/action.rb +24 -0
  4. data/lib/stoplight/admin/actions/lock.rb +23 -0
  5. data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
  6. data/lib/stoplight/admin/actions/lock_green.rb +23 -0
  7. data/lib/stoplight/admin/actions/lock_red.rb +23 -0
  8. data/lib/stoplight/admin/actions/stats.rb +27 -0
  9. data/lib/stoplight/admin/actions/unlock.rb +23 -0
  10. data/lib/stoplight/admin/dependencies.rb +50 -0
  11. data/lib/stoplight/admin/helpers.rb +27 -0
  12. data/lib/stoplight/admin/lights_repository/light.rb +155 -0
  13. data/lib/stoplight/admin/lights_repository.rb +74 -0
  14. data/lib/stoplight/admin/lights_stats.rb +77 -0
  15. data/lib/stoplight/admin/views/_card.erb +120 -0
  16. data/lib/stoplight/admin/views/index.erb +36 -0
  17. data/lib/stoplight/admin/views/layout.erb +66 -0
  18. data/lib/stoplight/admin.rb +68 -0
  19. data/lib/stoplight/color.rb +3 -3
  20. data/lib/stoplight/config/config_provider.rb +62 -0
  21. data/lib/stoplight/config/library_default_config.rb +29 -0
  22. data/lib/stoplight/config/user_default_config.rb +83 -0
  23. data/lib/stoplight/data_store/base.rb +59 -33
  24. data/lib/stoplight/data_store/fail_safe.rb +105 -0
  25. data/lib/stoplight/data_store/memory.rb +257 -50
  26. data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
  27. data/lib/stoplight/data_store/redis/lua.rb +23 -0
  28. data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
  29. data/lib/stoplight/data_store/redis/record_success.lua +35 -0
  30. data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
  31. data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
  32. data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
  33. data/lib/stoplight/data_store/redis.rb +345 -106
  34. data/lib/stoplight/default.rb +11 -9
  35. data/lib/stoplight/error.rb +1 -13
  36. data/lib/stoplight/failure.rb +14 -13
  37. data/lib/stoplight/light/config.rb +118 -0
  38. data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
  39. data/lib/stoplight/light/green_run_strategy.rb +53 -0
  40. data/lib/stoplight/light/red_run_strategy.rb +26 -0
  41. data/lib/stoplight/light/run_strategy.rb +30 -0
  42. data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
  43. data/lib/stoplight/light.rb +164 -84
  44. data/lib/stoplight/metadata.rb +71 -0
  45. data/lib/stoplight/notifier/base.rb +14 -7
  46. data/lib/stoplight/notifier/fail_safe.rb +67 -0
  47. data/lib/stoplight/notifier/generic.rb +54 -5
  48. data/lib/stoplight/rspec/generic_notifier.rb +11 -12
  49. data/lib/stoplight/rspec.rb +1 -1
  50. data/lib/stoplight/state.rb +3 -3
  51. data/lib/stoplight/traffic_control/base.rb +35 -0
  52. data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
  53. data/lib/stoplight/traffic_recovery/base.rb +51 -0
  54. data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
  55. data/lib/stoplight/version.rb +1 -1
  56. data/lib/stoplight.rb +111 -51
  57. metadata +49 -98
  58. data/lib/stoplight/builder.rb +0 -70
  59. data/lib/stoplight/circuit_breaker.rb +0 -102
  60. data/lib/stoplight/configurable.rb +0 -95
  61. data/lib/stoplight/configuration.rb +0 -126
  62. data/lib/stoplight/light/deprecated.rb +0 -44
  63. data/lib/stoplight/light/lockable.rb +0 -45
  64. data/lib/stoplight/light/runnable.rb +0 -127
  65. data/lib/stoplight/notifier.rb +0 -6
  66. data/spec/spec_helper.rb +0 -22
  67. data/spec/stoplight/builder_spec.rb +0 -165
  68. data/spec/stoplight/circuit_breaker_spec.rb +0 -43
  69. data/spec/stoplight/color_spec.rb +0 -39
  70. data/spec/stoplight/configurable_spec.rb +0 -25
  71. data/spec/stoplight/data_store/base_spec.rb +0 -71
  72. data/spec/stoplight/data_store/memory_spec.rb +0 -22
  73. data/spec/stoplight/data_store/redis_spec.rb +0 -45
  74. data/spec/stoplight/data_store_spec.rb +0 -9
  75. data/spec/stoplight/default_spec.rb +0 -80
  76. data/spec/stoplight/error_spec.rb +0 -39
  77. data/spec/stoplight/failure_spec.rb +0 -108
  78. data/spec/stoplight/light/lockable_spec.rb +0 -93
  79. data/spec/stoplight/light/runnable_spec.rb +0 -38
  80. data/spec/stoplight/light_spec.rb +0 -156
  81. data/spec/stoplight/notifier/base_spec.rb +0 -18
  82. data/spec/stoplight/notifier/generic_spec.rb +0 -50
  83. data/spec/stoplight/notifier/io_spec.rb +0 -41
  84. data/spec/stoplight/notifier/logger_spec.rb +0 -75
  85. data/spec/stoplight/notifier_spec.rb +0 -9
  86. data/spec/stoplight/state_spec.rb +0 -39
  87. data/spec/stoplight/version_spec.rb +0 -9
  88. data/spec/stoplight_spec.rb +0 -32
  89. data/spec/support/configurable.rb +0 -69
  90. data/spec/support/data_store/base/clear_failures.rb +0 -18
  91. data/spec/support/data_store/base/clear_state.rb +0 -20
  92. data/spec/support/data_store/base/get_all.rb +0 -44
  93. data/spec/support/data_store/base/get_failures.rb +0 -30
  94. data/spec/support/data_store/base/get_state.rb +0 -7
  95. data/spec/support/data_store/base/names.rb +0 -29
  96. data/spec/support/data_store/base/record_failures.rb +0 -70
  97. data/spec/support/data_store/base/set_state.rb +0 -15
  98. data/spec/support/data_store/base/with_notification_lock.rb +0 -27
  99. data/spec/support/data_store/base.rb +0 -21
  100. data/spec/support/database_cleaner.rb +0 -26
  101. data/spec/support/exception_helpers.rb +0 -9
  102. data/spec/support/light/runnable/color.rb +0 -79
  103. data/spec/support/light/runnable/run.rb +0 -247
  104. data/spec/support/light/runnable/state.rb +0 -31
  105. data/spec/support/light/runnable.rb +0 -5
data/README.md CHANGED
@@ -5,38 +5,19 @@
5
5
  [![Coverage badge][]][coverage]
6
6
  [![Climate badge][]][climate]
7
7
 
8
- Stoplight is traffic control for code. It's an implementation of the circuit
9
- breaker pattern in Ruby.
8
+ Stoplight is traffic control for code. It's an implementation of the circuit breaker pattern in Ruby.
10
9
 
11
10
  ---
12
- Does your code use unreliable systems, like a flaky database or a spotty web
13
- service? Wrap calls to those up in stoplights to prevent them from affecting
14
- the rest of your application.
15
-
16
- Check out [stoplight-admin][] for controlling your stoplights.
17
-
18
- - [Installation](#installation)
19
- - [Basic Usage](#basic-usage)
20
- - [Custom Errors](#custom-errors)
21
- - [Custom Fallback](#custom-fallback)
22
- - [Custom Threshold](#custom-threshold)
23
- - [Custom Window Size](#custom-window-size)
24
- - [Custom Cool Off Time](#custom-cool-off-time)
25
- - [Rails](#rails)
26
- - [Setup](#setup)
27
- - [Data Store](#data-store)
28
- - [Redis](#redis)
29
- - [Notifiers](#notifiers)
30
- - [IO](#io)
31
- - [Logger](#logger)
32
- - [Community-supported Notifiers](#community-supported-notifiers)
33
- - [How to Implement Your Own Notifier?](#how-to-implement-your-own-notifier)
34
- - [Rails](#rails-1)
35
- - [Advanced Usage](#advanced-usage)
36
- - [Locking](#locking)
37
- - [Testing](#testing)
38
- - [Maintenance Policy](#maintenance-policy)
39
- - [Credits](#credits)
11
+
12
+ :warning:️ You're currently browsing the documentation for Stoplight 5.x. If you're looking for
13
+ the documentation of the previous version 4.x, you can find it [here](https://github.com/bolshakov/stoplight/tree/v4.1.1).
14
+
15
+ Stoplight helps your application gracefully handle failures in external dependencies
16
+ (like flaky databases, unreliable APIs, or spotty web services). By wrapping these unreliable
17
+ calls, Stoplight prevents cascading failures from affecting your entire application.
18
+
19
+ **The best part?** Stoplight works with zero configuration out of the box, while offering deep customization when you
20
+ need it.
40
21
 
41
22
  ## Installation
42
23
 
@@ -52,428 +33,394 @@ Or install it manually:
52
33
  $ gem install stoplight
53
34
  ```
54
35
 
55
- Stoplight uses [Semantic Versioning][]. Check out [the change log][] for a
56
- detailed list of changes.
36
+ Stoplight uses [Semantic Versioning][]. Check out [the change log][] for a detailed list of changes.
37
+
38
+ ## Core Concepts
39
+
40
+ Stoplight operates like a traffic light with three states:
41
+
42
+ ```mermaid
43
+ stateDiagram
44
+ Green --> Red: Failures reach threshold
45
+ Red --> Yellow: After cool_off_time
46
+ Yellow --> Green: Successful attempt
47
+ Yellow --> Red: Failed attempt
48
+ Green --> Green: Success
49
+
50
+ classDef greenState fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff
51
+ classDef redState fill:#dc3545,stroke:#c82333,stroke-width:2px,color:#fff
52
+ classDef yellowState fill:#ffc107,stroke:#e0a800,stroke-width:2px,color:#000
53
+
54
+ class Green greenState
55
+ class Red redState
56
+ class Yellow yellowState
57
+ ```
58
+
59
+ - **Green**: Normal operation. Code runs as expected. (Circuit closed)
60
+ - **Red**: Failure state. Fast-fails without running the code. (Circuit open)
61
+ - **Yellow**: Recovery state. Allows a test execution to see if the problem is resolved. (Circuit half-open)
62
+
63
+ Stoplight's behavior is controlled by three primary parameters:
64
+
65
+ 1. **Threshold** (default: `3`): Number of failures required to transition from green to red.
66
+ 2. **Cool Off Time** (default: `60` seconds): Time to wait in the red state before transitioning to yellow.
67
+ 3. **Window Size** (default: `nil`): Time window in which failures are counted toward the threshold. By default, all failures are counted.
57
68
 
58
69
  ## Basic Usage
59
70
 
60
- To get started, create a stoplight:
71
+ Stoplight works right out of the box with sensible defaults:
61
72
 
62
73
  ```ruby
63
- light = Stoplight('example-pi')
74
+ # Create a stoplight with default settings
75
+ light = Stoplight("Payment Service")
76
+
77
+ # Use it to wrap code that might fail
78
+ result = light.run { payment_gateway.process(order) }
64
79
  ```
65
80
 
66
- Then you can run it with a block of code and it will return the result of calling the block. This is
67
- the green state. (The green state corresponds to the closed state for circuit breakers.)
81
+ When everything works, the light stays green and your code runs normally. If the code fails repeatedly, the
82
+ light turns red and raises a `Stoplight::Error::RedLight` exception to prevent further calls.
68
83
 
69
84
  ```ruby
70
- light.run { 22.0 / 7 }
71
- # => 3.142857142857143
72
- light.color
73
- # => "green"
85
+ light = Stoplight("Example")
86
+ light.run { 1 / 0 } #=> raises ZeroDivisionError: divided by 0
87
+ light.run { 1 / 0 } #=> raises ZeroDivisionError: divided by 0
88
+ light.run { 1 / 0 } #=> raises ZeroDivisionError: divided by 0
74
89
  ```
75
90
 
76
- If everything goes well, you shouldn't even be able to tell that you're using a
77
- stoplight. That's not very interesting though, so let's make stoplight fail.
91
+ After the last failure, the light turns red. The next call will raise a `Stoplight::Error::RedLight` exception without
92
+ executing the block:
78
93
 
79
- When you run it, the error will be recorded and passed through. After
80
- running it a few times, the stoplight will stop trying and fail fast. This is
81
- the red state. (The red state corresponds to the open state for circuit
82
- breakers.)
94
+ ```ruby
95
+ light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: example-zero
96
+ light.color # => "red"
97
+ ```
98
+
99
+ After one minute, the light transitions to yellow, allowing a test execution:
83
100
 
84
101
  ```ruby
85
- light = Stoplight('example-zero')
86
- # => #<Stoplight::CircuitBreaker:...>
87
- light.run { 1 / 0 }
88
- # ZeroDivisionError: divided by 0
89
- light.run { 1 / 0 }
90
- # ZeroDivisionError: divided by 0
91
- light.run { 1 / 0 }
92
- # Switching example-zero from green to red because ZeroDivisionError divided by 0
93
- # ZeroDivisionError: divided by 0
94
- light.run { 1 / 0 }
95
- # Stoplight::Error::RedLight: example-zero
96
- light.color
97
- # => "red"
98
- ```
99
-
100
- When the Stoplight changes from green to red, it will notify every configured
101
- notifier. See [the notifiers section][] to learn more about notifiers.
102
-
103
- The stoplight will move into the yellow state after being in the red state for
104
- a while. (The yellow state corresponds to the half open state for circuit
105
- breakers.) To configure how long it takes to switch into the yellow state,
106
- check out [the cool off time section][] When stoplights are yellow, they will
107
- try to run their code. If it fails, they'll switch back to red. If it succeeds,
108
- they'll switch to green.
109
-
110
- ### Custom Errors
111
-
112
- Some errors shouldn't cause your stoplight to move into the red state. Usually
113
- these are handled elsewhere in your stack and don't represent real failures. A
114
- good example is `ActiveRecord::RecordNotFound`.
115
-
116
- To prevent some errors from changing the state of your stoplight, you can
117
- provide a custom block that will be called with the error and a handler
118
- `Proc`. It can do one of three things:
119
-
120
- 1. Re-raise the error. This causes Stoplight to ignore the error. Do this for
121
- errors like `ActiveRecord::RecordNotFound` that don't represent real
122
- failures.
123
-
124
- 2. Call the handler with the error. This is the default behavior. Stoplight
125
- will only ignore the error if it shouldn't have been caught in the first
126
- place. See `Stoplight::Error::AVOID_RESCUING` for a list of errors that
127
- will be ignored.
128
-
129
- 3. Do nothing. This is **not recommended**. Doing nothing causes Stoplight to
130
- never ignore the error. That means a `NoMemoryError` could change the color
131
- of your stoplights.
102
+ # Wait for the cool off time
103
+ sleep 60
104
+ light.run { 1 / 1 } #=> 1
105
+ ```
106
+
107
+ If the test probe succeeds, the light turns green again. If it fails, the light turns red again.
132
108
 
133
109
  ```ruby
134
- light = Stoplight('example-not-found')
135
- .with_error_handler do |error, handle|
136
- if error.is_a?(ActiveRecord::RecordNotFound)
137
- raise error
138
- else
139
- handle.call(error)
140
- end
141
- end
142
- # => #<Stoplight::CircuitBreaker:...>
143
- light.run { User.find(123) }
144
- # ActiveRecord::RecordNotFound: Couldn't find User with ID=123
145
- light.run { User.find(123) }
146
- # ActiveRecord::RecordNotFound: Couldn't find User with ID=123
147
- light.run { User.find(123) }
148
- # ActiveRecord::RecordNotFound: Couldn't find User with ID=123
149
- light.color
150
- # => "green"
110
+ light.color #=> "green"
151
111
  ```
152
112
 
153
- ### Custom Fallback
113
+ ### Using Fallbacks
154
114
 
155
- By default, stoplights will re-raise errors when they're green. When they're
156
- red, they'll raise a `Stoplight::Error::RedLight` error. You can provide a
157
- fallback that will be called in both of these cases. It will be passed the
158
- error if the light was green.
115
+ Provide fallbacks to gracefully handle failures:
159
116
 
160
117
  ```ruby
118
+ fallback = ->(error) { error ? "Failed: #{error.message}" : "Service unavailable" }
119
+
161
120
  light = Stoplight('example-fallback')
162
- .with_fallback { |e| p e; 'default' }
163
- # => #<Stoplight::CircuitBreaker:..>
164
- light.run { 1 / 0 }
165
- # #<ZeroDivisionError: divided by 0>
166
- # => "default"
167
- light.run { 1 / 0 }
168
- # #<ZeroDivisionError: divided by 0>
169
- # => "default"
170
- light.run { 1 / 0 }
171
- # Switching example-fallback from green to red because ZeroDivisionError divided by 0
172
- # #<ZeroDivisionError: divided by 0>
173
- # => "default"
174
- light.run { 1 / 0 }
175
- # nil
176
- # => "default"
177
- ```
178
-
179
- ### Custom Threshold
180
-
181
- Some bits of code might be allowed to fail more or less frequently than others.
182
- You can configure this by setting a custom threshold.
121
+ result = light.run(fallback) { external_service.call }
122
+ ```
123
+
124
+ If the light is green but the call fails, the fallback receives the `error`. If the light is red, the fallback
125
+ receives `nil`. In both cases, the return value of the fallback becomes the return value of the `run` method.
126
+
127
+ ## Admin Panel
128
+
129
+ Stoplight goes with a built-in Admin Panel that can track all active Lights and manually lock them in the desired state (`Green` or `Red`). Locking lights in certain states might be helpful in scenarios like E2E testing.
130
+
131
+ To add Admin Panel to your Rails project, add this configuration to your `config/routes.rb` file.
183
132
 
184
133
  ```ruby
185
- light = Stoplight('example-threshold')
186
- .with_threshold(1)
187
- # => #<Stoplight::CircuitBreaker:...>
188
- light.run { fail }
189
- # Switching example-threshold from green to red because RuntimeError
190
- # RuntimeError:
191
- light.run { fail }
192
- # Stoplight::Error::RedLight: example-threshold
134
+ Rails.application.routes.draw do
135
+ # ...
136
+
137
+ mount Stoplight::Admin => '/stoplights'
138
+
139
+ # ...
140
+ end
193
141
  ```
194
142
 
195
- The default threshold is `3`.
143
+ **IMPORTANT:** Stoplight Admin Panel requires you to have `sinatra` and `sinatra-contrib` gems installed. You can either add them to your Gemfile:
196
144
 
197
- ### Custom Window Size
145
+ ```ruby
146
+ gem "sinatra", require: false
147
+ gem "sinatra-contrib", require: false
148
+ ```
198
149
 
199
- By default, all recorded failures, regardless of the time these happen, will count to reach
200
- the threshold (hence turning the light to red). If needed, a window size can be set,
201
- meaning you can control how many errors per period of time will count to reach the red
202
- state.
150
+ Or install it manually:
151
+ ```ruby
152
+ gem install sinatra
153
+ gem install sinatra-contrib
154
+ ```
203
155
 
204
- By default, every recorded failure contributes to reaching the threshold, regardless of when it occurs,
205
- causing the stoplight to turn red. By configuring a custom window size, you control how errors are
206
- counted within a specified time frame. Here's how it works:
156
+ ### Standalone Admin Panel Setup
207
157
 
208
- Let's say you set the window size to 2 seconds:
158
+ It is possible to run the Admin Panel separately from your application using the `stoplight-admin:<release-version>` docker image.
209
159
 
210
- ```ruby
211
- window_size_in_seconds = 2
160
+ ```shell
161
+ docker run --net=host stoplight-admin:v5
162
+ ```
212
163
 
213
- light = Stoplight('example-threshold')
214
- .with_window_size(window_size_in_seconds)
215
- .with_threshold(1) #=> #<Stoplight::CircuitBreaker:...>
164
+ **IMPORTANT:** Standalone Admin Panel should use the same Redis your application uses. To achieve this, set the `REDIS_URL` ENV variable via `-e REDIS_URL=<url-to-your-redis-servier>.` E.g.:
216
165
 
217
- light.run { 1 / 0 } #=> #<ZeroDivisionError: divided by 0>
218
- sleep(3)
219
- light.run { 1 / 0 }
220
- ```
166
+ ```shell
167
+ docker run -e REDIS_URL=redis://localhost:6378 --net=host stoplight-admin:v5
168
+ ```
221
169
 
222
- Without the window size configuration, the second `light.run { 1 / 0 }` call will result in a
223
- `Stoplight::Error::RedLight` exception being raised, as the stoplight transitions to the red state
224
- after the first call. With a sliding window of 2 seconds, only the errors that occur within the latest
225
- 2 seconds are considered. The first error causes the stoplight to turn red, but after 3 seconds
226
- (when the second error occurs), the window has shifted, and the stoplight switches to green state
227
- causing the error to raise again. This provides a way to focus on the most recent errors.
228
170
 
229
- The default window size is infinity, so all failures counts.
171
+ ## Configuration
230
172
 
231
- ### Custom Cool Off Time
173
+ ### Global Configuration
232
174
 
233
- Stoplights will automatically attempt to recover after a certain amount of
234
- time. A light in the red state for longer than the cool off period will
235
- transition to the yellow state. This cool off time is customizable.
175
+ Stoplight allows you to set default values for all lights in your application:
236
176
 
237
177
  ```ruby
238
- light = Stoplight('example-cool-off')
239
- .with_cool_off_time(1)
240
- # => #<Stoplight::CircuitBreaker:...>
241
- light.run { fail }
242
- # RuntimeError:
243
- light.run { fail }
244
- # RuntimeError:
245
- light.run { fail }
246
- # Switching example-cool-off from green to red because RuntimeError
247
- # RuntimeError:
248
- sleep(1)
249
- # => 1
250
- light.color
251
- # => "yellow"
252
- light.run { fail }
253
- # RuntimeError:
254
- ```
255
-
256
- The default cool off time is `60` seconds. To disable automatic recovery, set
257
- the cool off to `Float::INFINITY`. To make automatic recovery instantaneous,
258
- set the cool off to `0` seconds. Note that this is not recommended, as it
259
- effectively replaces the red state with yellow.
260
-
261
- ### Rails
262
-
263
- Stoplight was designed to wrap Rails actions with minimal effort. Here's an
264
- example configuration:
178
+ Stoplight.configure do |config|
179
+ # Set default behavior for all stoplights
180
+ config.threshold = 5
181
+ config.cool_off_time = 30
182
+ config.window_size = 60
183
+
184
+ # Set up default data store and notifiers
185
+ config.data_store = Stoplight::DataStore::Redis.new(redis)
186
+ config.notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)]
187
+
188
+ # Configure error handling defaults
189
+ config.tracked_errors = [StandardError, CustomError]
190
+ config.skipped_errors = [ActiveRecord::RecordNotFound]
191
+ end
192
+ ```
265
193
 
266
- ```ruby
267
- class ApplicationController < ActionController::Base
268
- around_action :stoplight
194
+ ### Creating Stoplights
269
195
 
270
- private
196
+ The simplest way to create a stoplight is with a name:
271
197
 
272
- def stoplight(&block)
273
- Stoplight("#{params[:controller]}##{params[:action]}")
274
- .with_fallback do |error|
275
- Rails.logger.error(error)
276
- render(nothing: true, status: :service_unavailable)
277
- end
278
- .run(&block)
279
- end
280
- end
198
+ ```ruby
199
+ light = Stoplight("Payment Service")
281
200
  ```
282
201
 
283
- ## Setup
202
+ You can also provide settings during creation:
284
203
 
285
- ### Data store
204
+ ```ruby
205
+ data_store = Stoplight::DataStore::Redis.new(Redis.new)
206
+
207
+ light = Stoplight("Payment Service",
208
+ threshold: 5, # 5 failures before turning red
209
+ cool_off_time: 60, # Wait 60 seconds before attempting recovery
210
+ window_size: 300, # Only count failures in the last five minutes
211
+ data_store: data_store, # Use Redis for persistence
212
+ tracked_errors: [TimeoutError], # Only count TimeoutError
213
+ skipped_errors: [ValidationError] # Ignore ValidationError
214
+ )
215
+ ```
286
216
 
287
- Stoplight uses an in-memory data store out of the box.
217
+ ### Modifying Stoplights
218
+
219
+ You can create specialized versions of existing stoplights:
288
220
 
289
221
  ```ruby
290
- require 'stoplight'
291
- # => true
292
- Stoplight.default_data_store
293
- # => #<Stoplight::DataStore::Memory:...>
222
+ # Base configuration for API calls
223
+ base_api = Stoplight("Service API")
224
+
225
+ # Create specialized version for the users endpoint
226
+ users_api = base_api.with(
227
+ tracked_errors: [TimeoutError] # Only track timeouts
228
+ )
294
229
  ```
295
230
 
296
- If you want to use a persistent data store, you'll have to set it up. Currently
297
- the only supported persistent data store is Redis.
231
+ The `#with` method creates a new stoplight instance without modifying the original, making it ideal for creating
232
+ specialized stoplights from a common configuration.
298
233
 
299
- #### Redis
234
+ ## Error Handling
300
235
 
301
- Make sure you have [the Redis gem][] installed before configuring Stoplight.
236
+ By default, Stoplight tracks all `StandardError` exceptions.
237
+ Note: System-level exceptions (e.g., `NoMemoryError`, `SignalException`) are not tracked, as they are not subclasses of `StandardError`.
238
+
239
+ ### Custom Error Configuration
240
+
241
+ Control which errors affect your stoplight state. Skip specific errors (will not count toward failure threshold)
302
242
 
303
243
  ```ruby
304
- require 'redis'
305
- # => true
306
- redis = Redis.new
307
- # => #<Redis client ...>
308
- data_store = Stoplight::DataStore::Redis.new(redis)
309
- # => #<Stoplight::DataStore::Redis:...>
310
- Stoplight.default_data_store = data_store
311
- # => #<Stoplight::DataStore::Redis:...>
244
+ light = Stoplight("Example API", skipped_errors: [ActiveRecord::RecordNotFound, ValidationError])
312
245
  ```
313
246
 
314
- ### Notifiers
315
-
316
- Stoplight sends notifications to standard error by default.
247
+ Only track specific errors (only these count toward failure threshold)
317
248
 
318
- ``` rb
319
- Stoplight.default_notifiers
320
- # => [#<Stoplight::Notifier::IO:...>]
249
+ ```ruby
250
+ light = Stoplight("Example API", tracked_errors: [NetworkError, Timeout::Error])
321
251
  ```
322
252
 
323
- If you want to send notifications elsewhere, you'll have to set them up.
253
+ When both methods are used, `skipped_errors` takes precedence over `tracked_errors`.
324
254
 
325
- #### IO
255
+ ## Advanced Configuration
326
256
 
327
- Stoplight can notify not only into STDOUT, but into any IO object. You can configure
328
- the `Stoplight::Notifier::IO` notifier for that.
257
+ ### Data Store
329
258
 
330
- ```ruby
331
- require 'stringio'
259
+ Stoplight uses an in-memory data store out of the box:
332
260
 
333
- io = StringIO.new
334
- # => #<StringIO:...>
335
- notifier = Stoplight::Notifier::IO.new(io)
336
- # => #<Stoplight::Notifier::IO:...>
337
- Stoplight.default_notifiers += [notifier]
338
- # => [#<Stoplight::Notifier::IO:...>, #<Stoplight::Notifier::IO:...>]
261
+ ```ruby
262
+ require "stoplight"
263
+ Stoplight::Default::DATA_STORE
264
+ # => #<Stoplight::DataStore::Memory:...>
339
265
  ```
340
266
 
341
- #### Logger
342
-
343
- Stoplight can be configured to use [the Logger class][] from the standard
344
- library.
267
+ For production environments, you'll likely want to use a persistent data store. Currently, [Redis] is the supported option:
345
268
 
346
269
  ```ruby
347
- require 'logger'
348
- # => true
349
- logger = Logger.new(STDERR)
350
- # => #<Logger:...>
351
- notifier = Stoplight::Notifier::Logger.new(logger)
352
- # => #<Stoplight::Notifier::Logger:...>
353
- Stoplight.default_notifiers += [notifier]
354
- # => [#<Stoplight::Notifier::IO:...>, #<Stoplight::Notifier::Logger:...>]
270
+ # Configure Redis as the data store
271
+ require "redis"
272
+ redis = Redis.new
273
+ data_store = Stoplight::DataStore::Redis.new(redis)
274
+
275
+ Stoplight.configure do |config|
276
+ config.data_store = data_store
277
+ end
355
278
  ```
356
279
 
357
- #### Community-supported Notifiers
280
+ #### Connection Pooling with Redis
358
281
 
359
- * [stoplight-sentry]
282
+ For high-traffic applications or when you want to control a number of opened connections to Redis:
360
283
 
361
- You you want to implement your own notifier, the following section contains all the required information.
284
+ ```ruby
285
+ require "connection_pool"
286
+ pool = ConnectionPool.new(size: 5, timeout: 3) { Redis.new }
287
+ data_store = Stoplight::DataStore::Redis.new(pool)
362
288
 
363
- Pull requests to update this section are welcome.
289
+ Stoplight.configure do |config|
290
+ config.data_store = data_store
291
+ end
292
+ ```
364
293
 
365
- #### How to implement your own notifier?
294
+ ### Notifiers
366
295
 
367
- A notifier has to implement the `Stoplight::Notifier::Base` interface:
296
+ Stoplight notifies when lights change state. Configure how these notifications are delivered:
368
297
 
369
298
  ```ruby
370
- def notify(light, from_color, to_color, error)
371
- raise NotImplementedError
299
+ # Log to a specific logger
300
+ logger = Logger.new("stoplight.log")
301
+ notifier = Stoplight::Notifier::Logger.new(logger)
302
+
303
+ # Configure globally
304
+ Stoplight.configure do |config|
305
+ config.notifiers = [notifier]
372
306
  end
373
307
  ```
374
308
 
375
- For convenience, you can use the `Stoplight::Notifier::Generic` module. It takes care of
376
- the message formatting, and you have to implement only the `put` method, which takes message sting as an argument:
309
+ In this example, when Stoplight fails three times in a row, it will log the error to `stoplight.log`:
377
310
 
378
- ```ruby
379
- class IO < Stoplight::Notifier::Base
380
- include Generic
381
-
382
- private
383
-
384
- def put(message)
385
- @object.puts(message)
386
- end
387
- end
311
+ ```log
312
+ W, [2025-04-16T09:18:46.778447 #44233] WARN -- : Switching test-light from green to red because RuntimeError bang!
388
313
  ```
389
314
 
390
- ### Rails
315
+ By default, Stoplight logs state transitions to STDERR.
316
+
317
+ #### Community-supported Notifiers
318
+
319
+ * [stoplight-sentry]
320
+ * [stoplight-honeybadger]
321
+
322
+ Pull requests to update this section are welcome. If you want to implement your own notifier, refer to
323
+ the [notifier interface documentation] for detailed instructions. Pull requests to update this section are welcome.
391
324
 
392
- Stoplight is designed to work seamlessly with Rails. If you want to use the
393
- in-memory data store, you don't need to do anything special. If you want to use
394
- a persistent data store, you'll need to configure it. Create an initializer for
395
- Stoplight:
325
+ ### Error Notifiers
326
+
327
+ Stoplight is built for resilience. If the Redis data store fails, Stoplight automatically falls back to the in-memory
328
+ data store. To get notified about such errors, you can configure an error notifier:
396
329
 
397
330
  ```ruby
398
- # config/initializers/stoplight.rb
399
- require 'stoplight'
400
- Stoplight.default_data_store = Stoplight::DataStore::Redis.new(...)
401
- Stoplight.default_notifiers += [Stoplight::Notifier::Logger.new(Rails.logger)]
331
+ Stoplight.configure do |config|
332
+ config.error_notifier = ->(error) { Bugsnag.notify(error) }
333
+ end
402
334
  ```
403
335
 
404
- ## Advanced usage
405
-
406
336
  ### Locking
407
337
 
408
- Although stoplights can operate on their own, occasionally you may want to
409
- override the default behavior. You can lock a light using `#lock(color)` method.
410
- Color should be either `Stoplight::Color::GREEN` or ``Stoplight::Color::RED``.
338
+ Sometimes you need to override Stoplight's automatic behavior. Locking allows you to manually control the state of
339
+ a stoplight, which is useful for:
340
+
341
+ * **Maintenance periods**: Lock to red when a service is known to be unavailable
342
+ * **Emergency overrides**: Lock to green to force traffic through during critical operations
343
+ * **Testing scenarios**: Control circuit state without waiting for failures
344
+ * **Gradual rollouts**: Manually control which stoplights are active during deployments
411
345
 
412
346
  ```ruby
413
- light = Stoplight('example-locked')
414
- # => #<Stoplight::CircuitBreaker:..>
415
- light.run { true }
416
- # => true
347
+ # Force a stoplight to red state (fail all requests)
348
+ # Useful during planned maintenance or when you know a service is down
417
349
  light.lock(Stoplight::Color::RED)
418
- # => #<Stoplight::CircuitBreaker:..>
419
- light.run { true }
420
- # Stoplight::Error::RedLight: example-locked
350
+
351
+ # Force a stoplight to green state (allow all requests)
352
+ # Useful for critical operations that must attempt to proceed
353
+ light.lock(Stoplight::Color::GREEN)
354
+
355
+ # Return to normal operation (automatic state transitions)
356
+ light.unlock
421
357
  ```
422
358
 
423
- **Code in locked red lights may still run under certain conditions!** If you
424
- have configured a custom data store and that data store fails, Stoplight will
425
- switch over to using a blank in-memory data store. That means you will lose the
426
- locked state of any stoplights.
359
+ ## Rails Integration
427
360
 
428
- You can go back to using the default behavior by unlocking the stoplight using `#unlock`.
361
+ Wrap controller actions with minimal effort:
429
362
 
430
363
  ```ruby
431
- light.unlock
432
- # => #<Stoplight::CircuitBreaker:..>
433
- ```
364
+ class ApplicationController < ActionController::Base
365
+ around_action :stoplight
366
+
367
+ private
434
368
 
435
- ### Testing
369
+ def stoplight(&block)
370
+ Stoplight("#{params[:controller]}##{params[:action]}")
371
+ .run(-> { render(nothing: true, status: :service_unavailable) }, &block)
372
+ end
373
+ end
374
+ ```
436
375
 
437
- Stoplights typically work as expected without modification in test suites.
438
- However there are a few things you can do to make them behave better. If your
439
- stoplights are spewing messages into your test output, you can silence them
440
- with a couple configuration changes.
376
+ Configure Stoplight in an initializer:
441
377
 
442
378
  ```ruby
443
- Stoplight.default_error_notifier = -> _ {}
444
- Stoplight.default_notifiers = []
379
+ # config/initializers/stoplight.rb
380
+ require "stoplight"
381
+ Stoplight.configure do |config|
382
+ config.data_store = Stoplight::DataStore::Redis.new(Redis.new)
383
+ config.notifiers += [Stoplight::Notifier::Logger.new(Rails.logger)]
384
+ end
445
385
  ```
446
386
 
447
- If your tests mysteriously fail because stoplights are the wrong color, you can
448
- try resetting the data store before each test case. For example, this would
449
- give each test case a fresh data store with RSpec.
387
+ ## Testing
450
388
 
389
+ Tips for working with Stoplight in test environments:
390
+
391
+ 1. Silence notifications in tests
451
392
  ```ruby
452
- before(:each) do
453
- Stoplight.default_data_store = Stoplight::DataStore::Memory.new
393
+ Stoplight.configure do |config|
394
+ config.error_notifier = -> _ {}
395
+ config.notifiers = []
454
396
  end
455
397
  ```
456
398
 
457
- Sometimes you may want to test stoplights directly. You can avoid resetting the
458
- data store by giving each stoplight a unique name.
399
+ 2. Reset data store between tests
400
+ ```ruby
401
+ before(:each) do
402
+ Stoplight.configure do |config|
403
+ config.data_store = Stoplight::DataStore::Memory.new
404
+ end
405
+ end
406
+ ```
459
407
 
408
+ 3. Or use unique names for test Stoplights to avoid persistence between tests:
460
409
  ```ruby
461
410
  stoplight = Stoplight("test-#{rand}")
462
411
  ```
463
412
 
464
413
  ## Maintenance Policy
465
414
 
466
- Stoplight supports the latest three minor versions of Ruby, which currently are: `3.0.x`, `3.1.x`, and `3.2.x`. Changing
467
- the minimum supported Ruby version is not considered a breaking change.
468
- We support the current stable Redis version (`7.2`) and the latest release of the previous major version (`6.2.9`)
415
+ Stoplight supports the latest three minor versions of Ruby, which currently are: `3.2.x`, `3.3.x`, and `3.4.x`. Changing
416
+ the minimum supported Ruby version is not considered a breaking change. We support the current stable Redis
417
+ version (`7.4.x`) and the latest release of the previous major version (`6.2.x`)
469
418
 
470
419
  ## Credits
471
420
 
472
- Stoplight is brought to you by [@camdez][] and [@tfausak][] from [@OrgSync][]. [@bolshakov][] is the current
473
- maintainer of the gem. A [complete list of contributors][] is available on GitHub. We were inspired by
474
- Martin Fowler's [CircuitBreaker][] article.
475
-
476
- Stoplight is licensed under [the MIT License][].
421
+ Stoplight was originally created by [camdez][] and [tfausak][]. It is currently maintained by [bolshakov][] and
422
+ [Lokideos][]. You can find a [complete list of contributors][] on GitHub. The project was inspired by Martin
423
+ Fowlers [CircuitBreaker][] article.
477
424
 
478
425
  [Stoplight]: https://github.com/bolshakov/stoplight
479
426
  [Version badge]: https://img.shields.io/gem/v/stoplight.svg?label=version
@@ -487,21 +434,13 @@ Stoplight is licensed under [the MIT License][].
487
434
  [stoplight-admin]: https://github.com/bolshakov/stoplight-admin
488
435
  [Semantic Versioning]: http://semver.org/spec/v2.0.0.html
489
436
  [the change log]: CHANGELOG.md
490
- [the notifiers section]: #notifiers
491
- [the cool off time section]: #custom-cool-off-time
492
- [the Redis gem]: https://rubygems.org/gems/redis
493
- [the Bugsnag gem]: https://rubygems.org/gems/bugsnag
494
- [the Honeybadger gem]: https://rubygems.org/gems/honeybadger
495
- [the Logger class]: http://ruby-doc.org/stdlib-2.2.3/libdoc/logger/rdoc/Logger.html
496
- [the Rollbar gem]: https://rubygems.org/gems/rollbar
497
- [the Sentry gem]: https://rubygems.org/gems/sentry-raven
498
- [the Slack gem]: https://rubygems.org/gems/slack-notifier
499
- [the Pagerduty gem]: https://rubygems.org/gems/pagerduty
500
- [@camdez]: https://github.com/camdez
501
- [@tfausak]: https://github.com/tfausak
502
- [@orgsync]: https://github.com/OrgSync
503
- [@bolshakov]: https://github.com/bolshakov
437
+ [stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
438
+ [stoplight-honeybadger]: https://github.com/qoqa/stoplight-honeybadger
439
+ [notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/master/lib/stoplight/notifier/generic.rb
440
+ [camdez]: https://github.com/camdez
441
+ [tfausak]: https://github.com/tfausak
442
+ [bolshakov]: https://github.com/bolshakov
443
+ [Lokideos]: https://github.com/Lokideos
504
444
  [complete list of contributors]: https://github.com/bolshakov/stoplight/graphs/contributors
505
445
  [CircuitBreaker]: http://martinfowler.com/bliki/CircuitBreaker.html
506
- [the MIT license]: LICENSE.md
507
- [stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
446
+ [Redis]: https://redis.io/