stoplight 4.1.1 → 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 +288 -354
  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 -24
  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,42 +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
11
 
13
- :warning:️ You're currently browsing the documentation for Stoplight 4.x. If you're looking for
14
- the documentation of the previous version 3.x, you can find it [here](https://github.com/bolshakov/stoplight/tree/release/v3.x).
15
-
16
- Does your code use unreliable systems, like a flaky database or a spotty web
17
- service? Wrap calls to those up in stoplights to prevent them from affecting
18
- the rest of your application.
19
-
20
- Check out [stoplight-admin][] for controlling your stoplights.
21
-
22
- - [Installation](#installation)
23
- - [Basic Usage](#basic-usage)
24
- - [Custom Errors](#custom-errors)
25
- - [Custom Fallback](#custom-fallback)
26
- - [Custom Threshold](#custom-threshold)
27
- - [Custom Window Size](#custom-window-size)
28
- - [Custom Cool Off Time](#custom-cool-off-time)
29
- - [Rails](#rails)
30
- - [Setup](#setup)
31
- - [Data Store](#data-store)
32
- - [Redis](#redis)
33
- - [Notifiers](#notifiers)
34
- - [IO](#io)
35
- - [Logger](#logger)
36
- - [Community-supported Notifiers](#community-supported-notifiers)
37
- - [How to Implement Your Own Notifier?](#how-to-implement-your-own-notifier)
38
- - [Rails](#rails-1)
39
- - [Advanced Usage](#advanced-usage)
40
- - [Locking](#locking)
41
- - [Testing](#testing)
42
- - [Maintenance Policy](#maintenance-policy)
43
- - [Credits](#credits)
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.
44
21
 
45
22
  ## Installation
46
23
 
@@ -56,429 +33,394 @@ Or install it manually:
56
33
  $ gem install stoplight
57
34
  ```
58
35
 
59
- Stoplight uses [Semantic Versioning][]. Check out [the change log][] for a
60
- 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.
61
68
 
62
69
  ## Basic Usage
63
70
 
64
- To get started, create a stoplight:
71
+ Stoplight works right out of the box with sensible defaults:
65
72
 
66
73
  ```ruby
67
- 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) }
68
79
  ```
69
80
 
70
- Then you can run it with a block of code and it will return the result of calling the block. This is
71
- 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.
72
83
 
73
84
  ```ruby
74
- light.run { 22.0 / 7 }
75
- # => 3.142857142857143
76
- light.color
77
- # => "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
78
89
  ```
79
90
 
80
- If everything goes well, you shouldn't even be able to tell that you're using a
81
- 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:
93
+
94
+ ```ruby
95
+ light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: example-zero
96
+ light.color # => "red"
97
+ ```
82
98
 
83
- When you run it, the error will be recorded and passed through. After
84
- running it a few times, the stoplight will stop trying and fail fast. This is
85
- the red state. (The red state corresponds to the open state for circuit
86
- breakers.)
99
+ After one minute, the light transitions to yellow, allowing a test execution:
87
100
 
88
101
  ```ruby
89
- light = Stoplight('example-zero')
90
- # => #<Stoplight::CircuitBreaker:...>
91
- light.run { 1 / 0 }
92
- # ZeroDivisionError: divided by 0
93
- light.run { 1 / 0 }
94
- # ZeroDivisionError: divided by 0
95
- light.run { 1 / 0 }
96
- # Switching example-zero from green to red because ZeroDivisionError divided by 0
97
- # ZeroDivisionError: divided by 0
98
- light.run { 1 / 0 }
99
- # Stoplight::Error::RedLight: example-zero
100
- light.color
101
- # => "red"
102
- ```
103
-
104
- When the Stoplight changes from green to red, it will notify every configured
105
- notifier. See [the notifiers section][] to learn more about notifiers.
106
-
107
- The stoplight will move into the yellow state after being in the red state for
108
- a while. (The yellow state corresponds to the half open state for circuit
109
- breakers.) To configure how long it takes to switch into the yellow state,
110
- check out [the cool off time section][] When stoplights are yellow, they will
111
- try to run their code. If it fails, they'll switch back to red. If it succeeds,
112
- they'll switch to green.
113
-
114
- ### Custom Errors
115
-
116
- Some errors shouldn't cause your stoplight to move into the red state. Usually
117
- these are handled elsewhere in your stack and don't represent real failures. A
118
- good example is `ActiveRecord::RecordNotFound`.
119
-
120
- To prevent some errors from changing the state of your stoplight, you can
121
- provide a custom block that will be called with the error and a handler
122
- `Proc`. It can do one of three things:
123
-
124
- 1. Re-raise the error. This causes Stoplight to ignore the error. Do this for
125
- errors like `ActiveRecord::RecordNotFound` that don't represent real
126
- failures.
127
-
128
- 2. Call the handler with the error. This is the default behavior. Stoplight
129
- will only ignore the error if it shouldn't have been caught in the first
130
- place. See `Stoplight::Error::AVOID_RESCUING` for a list of errors that
131
- will be ignored.
132
-
133
- 3. Do nothing. This is **not recommended**. Doing nothing causes Stoplight to
134
- never ignore the error. That means a `NoMemoryError` could change the color
135
- 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.
136
108
 
137
109
  ```ruby
138
- light = Stoplight('example-not-found')
139
- .with_error_handler do |error, handle|
140
- if error.is_a?(ActiveRecord::RecordNotFound)
141
- raise error
142
- else
143
- handle.call(error)
144
- end
145
- end
146
- # => #<Stoplight::CircuitBreaker:...>
147
- light.run { User.find(123) }
148
- # ActiveRecord::RecordNotFound: Couldn't find User with ID=123
149
- light.run { User.find(123) }
150
- # ActiveRecord::RecordNotFound: Couldn't find User with ID=123
151
- light.run { User.find(123) }
152
- # ActiveRecord::RecordNotFound: Couldn't find User with ID=123
153
- light.color
154
- # => "green"
110
+ light.color #=> "green"
155
111
  ```
156
112
 
157
- ### Custom Fallback
113
+ ### Using Fallbacks
158
114
 
159
- By default, stoplights will re-raise errors when they're green. When they're
160
- red, they'll raise a `Stoplight::Error::RedLight` error. You can provide a
161
- fallback that will be called in both of these cases. It will be passed the
162
- error if the light was green.
115
+ Provide fallbacks to gracefully handle failures:
163
116
 
164
117
  ```ruby
118
+ fallback = ->(error) { error ? "Failed: #{error.message}" : "Service unavailable" }
119
+
165
120
  light = Stoplight('example-fallback')
166
- .with_fallback { |e| p e; 'default' }
167
- # => #<Stoplight::CircuitBreaker:..>
168
- light.run { 1 / 0 }
169
- # #<ZeroDivisionError: divided by 0>
170
- # => "default"
171
- light.run { 1 / 0 }
172
- # #<ZeroDivisionError: divided by 0>
173
- # => "default"
174
- light.run { 1 / 0 }
175
- # Switching example-fallback from green to red because ZeroDivisionError divided by 0
176
- # #<ZeroDivisionError: divided by 0>
177
- # => "default"
178
- light.run { 1 / 0 }
179
- # nil
180
- # => "default"
181
- ```
182
-
183
- ### Custom Threshold
184
-
185
- Some bits of code might be allowed to fail more or less frequently than others.
186
- 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.
187
132
 
188
133
  ```ruby
189
- light = Stoplight('example-threshold')
190
- .with_threshold(1)
191
- # => #<Stoplight::CircuitBreaker:...>
192
- light.run { fail }
193
- # Switching example-threshold from green to red because RuntimeError
194
- # RuntimeError:
195
- light.run { fail }
196
- # Stoplight::Error::RedLight: example-threshold
134
+ Rails.application.routes.draw do
135
+ # ...
136
+
137
+ mount Stoplight::Admin => '/stoplights'
138
+
139
+ # ...
140
+ end
197
141
  ```
198
142
 
199
- 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:
200
144
 
201
- ### Custom Window Size
145
+ ```ruby
146
+ gem "sinatra", require: false
147
+ gem "sinatra-contrib", require: false
148
+ ```
202
149
 
203
- By default, all recorded failures, regardless of the time these happen, will count to reach
204
- the threshold (hence turning the light to red). If needed, a window size can be set,
205
- meaning you can control how many errors per period of time will count to reach the red
206
- state.
150
+ Or install it manually:
151
+ ```ruby
152
+ gem install sinatra
153
+ gem install sinatra-contrib
154
+ ```
207
155
 
208
- By default, every recorded failure contributes to reaching the threshold, regardless of when it occurs,
209
- causing the stoplight to turn red. By configuring a custom window size, you control how errors are
210
- counted within a specified time frame. Here's how it works:
156
+ ### Standalone Admin Panel Setup
211
157
 
212
- 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.
213
159
 
214
- ```ruby
215
- window_size_in_seconds = 2
160
+ ```shell
161
+ docker run --net=host stoplight-admin:v5
162
+ ```
216
163
 
217
- light = Stoplight('example-threshold')
218
- .with_window_size(window_size_in_seconds)
219
- .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.:
220
165
 
221
- light.run { 1 / 0 } #=> #<ZeroDivisionError: divided by 0>
222
- sleep(3)
223
- light.run { 1 / 0 }
224
- ```
166
+ ```shell
167
+ docker run -e REDIS_URL=redis://localhost:6378 --net=host stoplight-admin:v5
168
+ ```
225
169
 
226
- Without the window size configuration, the second `light.run { 1 / 0 }` call will result in a
227
- `Stoplight::Error::RedLight` exception being raised, as the stoplight transitions to the red state
228
- after the first call. With a sliding window of 2 seconds, only the errors that occur within the latest
229
- 2 seconds are considered. The first error causes the stoplight to turn red, but after 3 seconds
230
- (when the second error occurs), the window has shifted, and the stoplight switches to green state
231
- causing the error to raise again. This provides a way to focus on the most recent errors.
232
170
 
233
- The default window size is infinity, so all failures counts.
171
+ ## Configuration
234
172
 
235
- ### Custom Cool Off Time
173
+ ### Global Configuration
236
174
 
237
- Stoplights will automatically attempt to recover after a certain amount of
238
- time. A light in the red state for longer than the cool off period will
239
- 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:
240
176
 
241
177
  ```ruby
242
- light = Stoplight('example-cool-off')
243
- .with_cool_off_time(1)
244
- # => #<Stoplight::CircuitBreaker:...>
245
- light.run { fail }
246
- # RuntimeError:
247
- light.run { fail }
248
- # RuntimeError:
249
- light.run { fail }
250
- # Switching example-cool-off from green to red because RuntimeError
251
- # RuntimeError:
252
- sleep(1)
253
- # => 1
254
- light.color
255
- # => "yellow"
256
- light.run { fail }
257
- # RuntimeError:
258
- ```
259
-
260
- The default cool off time is `60` seconds. To disable automatic recovery, set
261
- the cool off to `Float::INFINITY`. To make automatic recovery instantaneous,
262
- set the cool off to `0` seconds. Note that this is not recommended, as it
263
- effectively replaces the red state with yellow.
264
-
265
- ### Rails
266
-
267
- Stoplight was designed to wrap Rails actions with minimal effort. Here's an
268
- 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
+ ```
269
193
 
270
- ```ruby
271
- class ApplicationController < ActionController::Base
272
- around_action :stoplight
194
+ ### Creating Stoplights
273
195
 
274
- private
196
+ The simplest way to create a stoplight is with a name:
275
197
 
276
- def stoplight(&block)
277
- Stoplight("#{params[:controller]}##{params[:action]}")
278
- .with_fallback do |error|
279
- Rails.logger.error(error)
280
- render(nothing: true, status: :service_unavailable)
281
- end
282
- .run(&block)
283
- end
284
- end
198
+ ```ruby
199
+ light = Stoplight("Payment Service")
285
200
  ```
286
201
 
287
- ## Setup
202
+ You can also provide settings during creation:
288
203
 
289
- ### 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
+ ```
290
216
 
291
- Stoplight uses an in-memory data store out of the box.
217
+ ### Modifying Stoplights
218
+
219
+ You can create specialized versions of existing stoplights:
292
220
 
293
221
  ```ruby
294
- require 'stoplight'
295
- # => true
296
- Stoplight.default_data_store
297
- # => #<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
+ )
298
229
  ```
299
230
 
300
- If you want to use a persistent data store, you'll have to set it up. Currently
301
- 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.
302
233
 
303
- #### Redis
234
+ ## Error Handling
304
235
 
305
- 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)
306
242
 
307
243
  ```ruby
308
- require 'redis'
309
- # => true
310
- redis = Redis.new
311
- # => #<Redis client ...>
312
- data_store = Stoplight::DataStore::Redis.new(redis)
313
- # => #<Stoplight::DataStore::Redis:...>
314
- Stoplight.default_data_store = data_store
315
- # => #<Stoplight::DataStore::Redis:...>
244
+ light = Stoplight("Example API", skipped_errors: [ActiveRecord::RecordNotFound, ValidationError])
316
245
  ```
317
246
 
318
- ### Notifiers
319
-
320
- Stoplight sends notifications to standard error by default.
247
+ Only track specific errors (only these count toward failure threshold)
321
248
 
322
- ``` rb
323
- Stoplight.default_notifiers
324
- # => [#<Stoplight::Notifier::IO:...>]
249
+ ```ruby
250
+ light = Stoplight("Example API", tracked_errors: [NetworkError, Timeout::Error])
325
251
  ```
326
252
 
327
- 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`.
328
254
 
329
- #### IO
255
+ ## Advanced Configuration
330
256
 
331
- Stoplight can notify not only into STDOUT, but into any IO object. You can configure
332
- the `Stoplight::Notifier::IO` notifier for that.
257
+ ### Data Store
333
258
 
334
- ```ruby
335
- require 'stringio'
259
+ Stoplight uses an in-memory data store out of the box:
336
260
 
337
- io = StringIO.new
338
- # => #<StringIO:...>
339
- notifier = Stoplight::Notifier::IO.new(io)
340
- # => #<Stoplight::Notifier::IO:...>
341
- Stoplight.default_notifiers += [notifier]
342
- # => [#<Stoplight::Notifier::IO:...>, #<Stoplight::Notifier::IO:...>]
261
+ ```ruby
262
+ require "stoplight"
263
+ Stoplight::Default::DATA_STORE
264
+ # => #<Stoplight::DataStore::Memory:...>
343
265
  ```
344
266
 
345
- #### Logger
346
-
347
- Stoplight can be configured to use [the Logger class][] from the standard
348
- library.
267
+ For production environments, you'll likely want to use a persistent data store. Currently, [Redis] is the supported option:
349
268
 
350
269
  ```ruby
351
- require 'logger'
352
- # => true
353
- logger = Logger.new(STDERR)
354
- # => #<Logger:...>
355
- notifier = Stoplight::Notifier::Logger.new(logger)
356
- # => #<Stoplight::Notifier::Logger:...>
357
- Stoplight.default_notifiers += [notifier]
358
- # => [#<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
359
278
  ```
360
279
 
361
- #### Community-supported Notifiers
280
+ #### Connection Pooling with Redis
362
281
 
363
- * [stoplight-sentry]
364
- * [stoplight-honeybadger](https://github.com/qoqa/stoplight-honeybadger)
282
+ For high-traffic applications or when you want to control a number of opened connections to Redis:
365
283
 
366
- 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)
367
288
 
368
- Pull requests to update this section are welcome.
289
+ Stoplight.configure do |config|
290
+ config.data_store = data_store
291
+ end
292
+ ```
369
293
 
370
- #### How to implement your own notifier?
294
+ ### Notifiers
371
295
 
372
- A notifier has to implement the `Stoplight::Notifier::Base` interface:
296
+ Stoplight notifies when lights change state. Configure how these notifications are delivered:
373
297
 
374
298
  ```ruby
375
- def notify(light, from_color, to_color, error)
376
- 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]
377
306
  end
378
307
  ```
379
308
 
380
- For convenience, you can use the `Stoplight::Notifier::Generic` module. It takes care of
381
- 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`:
382
310
 
383
- ```ruby
384
- class IO < Stoplight::Notifier::Base
385
- include Generic
386
-
387
- private
388
-
389
- def put(message)
390
- @object.puts(message)
391
- end
392
- end
311
+ ```log
312
+ W, [2025-04-16T09:18:46.778447 #44233] WARN -- : Switching test-light from green to red because RuntimeError bang!
393
313
  ```
394
314
 
395
- ### 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.
396
324
 
397
- Stoplight is designed to work seamlessly with Rails. If you want to use the
398
- in-memory data store, you don't need to do anything special. If you want to use
399
- a persistent data store, you'll need to configure it. Create an initializer for
400
- 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:
401
329
 
402
330
  ```ruby
403
- # config/initializers/stoplight.rb
404
- require 'stoplight'
405
- Stoplight.default_data_store = Stoplight::DataStore::Redis.new(...)
406
- Stoplight.default_notifiers += [Stoplight::Notifier::Logger.new(Rails.logger)]
331
+ Stoplight.configure do |config|
332
+ config.error_notifier = ->(error) { Bugsnag.notify(error) }
333
+ end
407
334
  ```
408
335
 
409
- ## Advanced usage
410
-
411
336
  ### Locking
412
337
 
413
- Although stoplights can operate on their own, occasionally you may want to
414
- override the default behavior. You can lock a light using `#lock(color)` method.
415
- 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
416
345
 
417
346
  ```ruby
418
- light = Stoplight('example-locked')
419
- # => #<Stoplight::CircuitBreaker:..>
420
- light.run { true }
421
- # => true
347
+ # Force a stoplight to red state (fail all requests)
348
+ # Useful during planned maintenance or when you know a service is down
422
349
  light.lock(Stoplight::Color::RED)
423
- # => #<Stoplight::CircuitBreaker:..>
424
- light.run { true }
425
- # 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
426
357
  ```
427
358
 
428
- **Code in locked red lights may still run under certain conditions!** If you
429
- have configured a custom data store and that data store fails, Stoplight will
430
- switch over to using a blank in-memory data store. That means you will lose the
431
- locked state of any stoplights.
359
+ ## Rails Integration
432
360
 
433
- You can go back to using the default behavior by unlocking the stoplight using `#unlock`.
361
+ Wrap controller actions with minimal effort:
434
362
 
435
363
  ```ruby
436
- light.unlock
437
- # => #<Stoplight::CircuitBreaker:..>
438
- ```
364
+ class ApplicationController < ActionController::Base
365
+ around_action :stoplight
366
+
367
+ private
439
368
 
440
- ### 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
+ ```
441
375
 
442
- Stoplights typically work as expected without modification in test suites.
443
- However there are a few things you can do to make them behave better. If your
444
- stoplights are spewing messages into your test output, you can silence them
445
- with a couple configuration changes.
376
+ Configure Stoplight in an initializer:
446
377
 
447
378
  ```ruby
448
- Stoplight.default_error_notifier = -> _ {}
449
- 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
450
385
  ```
451
386
 
452
- If your tests mysteriously fail because stoplights are the wrong color, you can
453
- try resetting the data store before each test case. For example, this would
454
- give each test case a fresh data store with RSpec.
387
+ ## Testing
455
388
 
389
+ Tips for working with Stoplight in test environments:
390
+
391
+ 1. Silence notifications in tests
456
392
  ```ruby
457
- before(:each) do
458
- Stoplight.default_data_store = Stoplight::DataStore::Memory.new
393
+ Stoplight.configure do |config|
394
+ config.error_notifier = -> _ {}
395
+ config.notifiers = []
459
396
  end
460
397
  ```
461
398
 
462
- Sometimes you may want to test stoplights directly. You can avoid resetting the
463
- 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
+ ```
464
407
 
408
+ 3. Or use unique names for test Stoplights to avoid persistence between tests:
465
409
  ```ruby
466
410
  stoplight = Stoplight("test-#{rand}")
467
411
  ```
468
412
 
469
413
  ## Maintenance Policy
470
414
 
471
- Stoplight supports the latest three minor versions of Ruby, which currently are: `3.0.x`, `3.1.x`, and `3.2.x`. Changing
472
- the minimum supported Ruby version is not considered a breaking change.
473
- 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`)
474
418
 
475
419
  ## Credits
476
420
 
477
- Stoplight is brought to you by [@camdez][] and [@tfausak][] from [@OrgSync][]. [@bolshakov][] is the current
478
- maintainer of the gem. A [complete list of contributors][] is available on GitHub. We were inspired by
479
- Martin Fowler's [CircuitBreaker][] article.
480
-
481
- 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.
482
424
 
483
425
  [Stoplight]: https://github.com/bolshakov/stoplight
484
426
  [Version badge]: https://img.shields.io/gem/v/stoplight.svg?label=version
@@ -492,21 +434,13 @@ Stoplight is licensed under [the MIT License][].
492
434
  [stoplight-admin]: https://github.com/bolshakov/stoplight-admin
493
435
  [Semantic Versioning]: http://semver.org/spec/v2.0.0.html
494
436
  [the change log]: CHANGELOG.md
495
- [the notifiers section]: #notifiers
496
- [the cool off time section]: #custom-cool-off-time
497
- [the Redis gem]: https://rubygems.org/gems/redis
498
- [the Bugsnag gem]: https://rubygems.org/gems/bugsnag
499
- [the Honeybadger gem]: https://rubygems.org/gems/honeybadger
500
- [the Logger class]: http://ruby-doc.org/stdlib-2.2.3/libdoc/logger/rdoc/Logger.html
501
- [the Rollbar gem]: https://rubygems.org/gems/rollbar
502
- [the Sentry gem]: https://rubygems.org/gems/sentry-raven
503
- [the Slack gem]: https://rubygems.org/gems/slack-notifier
504
- [the Pagerduty gem]: https://rubygems.org/gems/pagerduty
505
- [@camdez]: https://github.com/camdez
506
- [@tfausak]: https://github.com/tfausak
507
- [@orgsync]: https://github.com/OrgSync
508
- [@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
509
444
  [complete list of contributors]: https://github.com/bolshakov/stoplight/graphs/contributors
510
445
  [CircuitBreaker]: http://martinfowler.com/bliki/CircuitBreaker.html
511
- [the MIT license]: LICENSE.md
512
- [stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
446
+ [Redis]: https://redis.io/