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.
- checksums.yaml +4 -4
- data/README.md +289 -350
- data/lib/stoplight/admin/actions/action.rb +24 -0
- data/lib/stoplight/admin/actions/lock.rb +23 -0
- data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
- data/lib/stoplight/admin/actions/lock_green.rb +23 -0
- data/lib/stoplight/admin/actions/lock_red.rb +23 -0
- data/lib/stoplight/admin/actions/stats.rb +27 -0
- data/lib/stoplight/admin/actions/unlock.rb +23 -0
- data/lib/stoplight/admin/dependencies.rb +50 -0
- data/lib/stoplight/admin/helpers.rb +27 -0
- data/lib/stoplight/admin/lights_repository/light.rb +155 -0
- data/lib/stoplight/admin/lights_repository.rb +74 -0
- data/lib/stoplight/admin/lights_stats.rb +77 -0
- data/lib/stoplight/admin/views/_card.erb +120 -0
- data/lib/stoplight/admin/views/index.erb +36 -0
- data/lib/stoplight/admin/views/layout.erb +66 -0
- data/lib/stoplight/admin.rb +68 -0
- data/lib/stoplight/color.rb +3 -3
- data/lib/stoplight/config/config_provider.rb +62 -0
- data/lib/stoplight/config/library_default_config.rb +29 -0
- data/lib/stoplight/config/user_default_config.rb +83 -0
- data/lib/stoplight/data_store/base.rb +59 -33
- data/lib/stoplight/data_store/fail_safe.rb +105 -0
- data/lib/stoplight/data_store/memory.rb +257 -50
- data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
- data/lib/stoplight/data_store/redis/lua.rb +23 -0
- data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
- data/lib/stoplight/data_store/redis/record_success.lua +35 -0
- data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
- data/lib/stoplight/data_store/redis.rb +345 -106
- data/lib/stoplight/default.rb +11 -9
- data/lib/stoplight/error.rb +1 -13
- data/lib/stoplight/failure.rb +14 -13
- data/lib/stoplight/light/config.rb +118 -0
- data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
- data/lib/stoplight/light/green_run_strategy.rb +53 -0
- data/lib/stoplight/light/red_run_strategy.rb +26 -0
- data/lib/stoplight/light/run_strategy.rb +30 -0
- data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
- data/lib/stoplight/light.rb +164 -84
- data/lib/stoplight/metadata.rb +71 -0
- data/lib/stoplight/notifier/base.rb +14 -7
- data/lib/stoplight/notifier/fail_safe.rb +67 -0
- data/lib/stoplight/notifier/generic.rb +54 -5
- data/lib/stoplight/rspec/generic_notifier.rb +11 -12
- data/lib/stoplight/rspec.rb +1 -1
- data/lib/stoplight/state.rb +3 -3
- data/lib/stoplight/traffic_control/base.rb +35 -0
- data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
- data/lib/stoplight/traffic_recovery/base.rb +51 -0
- data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight.rb +111 -51
- metadata +49 -98
- data/lib/stoplight/builder.rb +0 -70
- data/lib/stoplight/circuit_breaker.rb +0 -102
- data/lib/stoplight/configurable.rb +0 -95
- data/lib/stoplight/configuration.rb +0 -126
- data/lib/stoplight/light/deprecated.rb +0 -44
- data/lib/stoplight/light/lockable.rb +0 -45
- data/lib/stoplight/light/runnable.rb +0 -127
- data/lib/stoplight/notifier.rb +0 -6
- data/spec/spec_helper.rb +0 -22
- data/spec/stoplight/builder_spec.rb +0 -165
- data/spec/stoplight/circuit_breaker_spec.rb +0 -43
- data/spec/stoplight/color_spec.rb +0 -39
- data/spec/stoplight/configurable_spec.rb +0 -25
- data/spec/stoplight/data_store/base_spec.rb +0 -71
- data/spec/stoplight/data_store/memory_spec.rb +0 -22
- data/spec/stoplight/data_store/redis_spec.rb +0 -45
- data/spec/stoplight/data_store_spec.rb +0 -9
- data/spec/stoplight/default_spec.rb +0 -80
- data/spec/stoplight/error_spec.rb +0 -39
- data/spec/stoplight/failure_spec.rb +0 -108
- data/spec/stoplight/light/lockable_spec.rb +0 -93
- data/spec/stoplight/light/runnable_spec.rb +0 -38
- data/spec/stoplight/light_spec.rb +0 -156
- data/spec/stoplight/notifier/base_spec.rb +0 -18
- data/spec/stoplight/notifier/generic_spec.rb +0 -50
- data/spec/stoplight/notifier/io_spec.rb +0 -41
- data/spec/stoplight/notifier/logger_spec.rb +0 -75
- data/spec/stoplight/notifier_spec.rb +0 -9
- data/spec/stoplight/state_spec.rb +0 -39
- data/spec/stoplight/version_spec.rb +0 -9
- data/spec/stoplight_spec.rb +0 -32
- data/spec/support/configurable.rb +0 -69
- data/spec/support/data_store/base/clear_failures.rb +0 -18
- data/spec/support/data_store/base/clear_state.rb +0 -20
- data/spec/support/data_store/base/get_all.rb +0 -44
- data/spec/support/data_store/base/get_failures.rb +0 -30
- data/spec/support/data_store/base/get_state.rb +0 -7
- data/spec/support/data_store/base/names.rb +0 -29
- data/spec/support/data_store/base/record_failures.rb +0 -70
- data/spec/support/data_store/base/set_state.rb +0 -15
- data/spec/support/data_store/base/with_notification_lock.rb +0 -27
- data/spec/support/data_store/base.rb +0 -21
- data/spec/support/database_cleaner.rb +0 -26
- data/spec/support/exception_helpers.rb +0 -9
- data/spec/support/light/runnable/color.rb +0 -79
- data/spec/support/light/runnable/run.rb +0 -247
- data/spec/support/light/runnable/state.rb +0 -31
- 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
|
-
|
13
|
-
|
14
|
-
the
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
71
|
+
Stoplight works right out of the box with sensible defaults:
|
61
72
|
|
62
73
|
```ruby
|
63
|
-
|
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
|
-
|
67
|
-
|
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
|
71
|
-
|
72
|
-
light.
|
73
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
86
|
-
|
87
|
-
light.run { 1 /
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
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
|
-
###
|
113
|
+
### Using Fallbacks
|
154
114
|
|
155
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
#
|
191
|
-
|
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
|
-
|
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
|
-
|
145
|
+
```ruby
|
146
|
+
gem "sinatra", require: false
|
147
|
+
gem "sinatra-contrib", require: false
|
148
|
+
```
|
198
149
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
150
|
+
Or install it manually:
|
151
|
+
```ruby
|
152
|
+
gem install sinatra
|
153
|
+
gem install sinatra-contrib
|
154
|
+
```
|
203
155
|
|
204
|
-
|
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
|
-
|
158
|
+
It is possible to run the Admin Panel separately from your application using the `stoplight-admin:<release-version>` docker image.
|
209
159
|
|
210
|
-
|
211
|
-
|
160
|
+
```shell
|
161
|
+
docker run --net=host stoplight-admin:v5
|
162
|
+
```
|
212
163
|
|
213
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
171
|
+
## Configuration
|
230
172
|
|
231
|
-
###
|
173
|
+
### Global Configuration
|
232
174
|
|
233
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
#
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
-
|
267
|
-
class ApplicationController < ActionController::Base
|
268
|
-
around_action :stoplight
|
194
|
+
### Creating Stoplights
|
269
195
|
|
270
|
-
|
196
|
+
The simplest way to create a stoplight is with a name:
|
271
197
|
|
272
|
-
|
273
|
-
|
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
|
-
|
202
|
+
You can also provide settings during creation:
|
284
203
|
|
285
|
-
|
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
|
-
|
217
|
+
### Modifying Stoplights
|
218
|
+
|
219
|
+
You can create specialized versions of existing stoplights:
|
288
220
|
|
289
221
|
```ruby
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
#
|
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
|
-
|
297
|
-
|
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
|
-
|
234
|
+
## Error Handling
|
300
235
|
|
301
|
-
|
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
|
-
|
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
|
-
|
315
|
-
|
316
|
-
Stoplight sends notifications to standard error by default.
|
247
|
+
Only track specific errors (only these count toward failure threshold)
|
317
248
|
|
318
|
-
```
|
319
|
-
Stoplight
|
320
|
-
# => [#<Stoplight::Notifier::IO:...>]
|
249
|
+
```ruby
|
250
|
+
light = Stoplight("Example API", tracked_errors: [NetworkError, Timeout::Error])
|
321
251
|
```
|
322
252
|
|
323
|
-
|
253
|
+
When both methods are used, `skipped_errors` takes precedence over `tracked_errors`.
|
324
254
|
|
325
|
-
|
255
|
+
## Advanced Configuration
|
326
256
|
|
327
|
-
|
328
|
-
the `Stoplight::Notifier::IO` notifier for that.
|
257
|
+
### Data Store
|
329
258
|
|
330
|
-
|
331
|
-
require 'stringio'
|
259
|
+
Stoplight uses an in-memory data store out of the box:
|
332
260
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
# => #<Stoplight::
|
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
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
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
|
-
####
|
280
|
+
#### Connection Pooling with Redis
|
358
281
|
|
359
|
-
|
282
|
+
For high-traffic applications or when you want to control a number of opened connections to Redis:
|
360
283
|
|
361
|
-
|
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
|
-
|
289
|
+
Stoplight.configure do |config|
|
290
|
+
config.data_store = data_store
|
291
|
+
end
|
292
|
+
```
|
364
293
|
|
365
|
-
|
294
|
+
### Notifiers
|
366
295
|
|
367
|
-
|
296
|
+
Stoplight notifies when lights change state. Configure how these notifications are delivered:
|
368
297
|
|
369
298
|
```ruby
|
370
|
-
|
371
|
-
|
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
|
-
|
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
|
-
```
|
379
|
-
|
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
|
-
|
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
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
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
|
-
|
399
|
-
|
400
|
-
|
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
|
-
|
409
|
-
|
410
|
-
|
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
|
-
|
414
|
-
#
|
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
|
-
|
419
|
-
|
420
|
-
#
|
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
|
-
|
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
|
-
|
361
|
+
Wrap controller actions with minimal effort:
|
429
362
|
|
430
363
|
```ruby
|
431
|
-
|
432
|
-
|
433
|
-
|
364
|
+
class ApplicationController < ActionController::Base
|
365
|
+
around_action :stoplight
|
366
|
+
|
367
|
+
private
|
434
368
|
|
435
|
-
|
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
|
-
|
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
|
-
|
444
|
-
|
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
|
-
|
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
|
-
|
453
|
-
|
393
|
+
Stoplight.configure do |config|
|
394
|
+
config.error_notifier = -> _ {}
|
395
|
+
config.notifiers = []
|
454
396
|
end
|
455
397
|
```
|
456
398
|
|
457
|
-
|
458
|
-
|
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.
|
467
|
-
the minimum supported Ruby version is not considered a breaking change.
|
468
|
-
|
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
|
473
|
-
|
474
|
-
|
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
|
+
Fowler’s [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
|
-
[
|
491
|
-
[
|
492
|
-
[
|
493
|
-
[
|
494
|
-
[
|
495
|
-
[
|
496
|
-
[
|
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
|
-
[
|
507
|
-
[stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
|
446
|
+
[Redis]: https://redis.io/
|