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.
- checksums.yaml +4 -4
- data/README.md +288 -354
- 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 -24
- 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,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
|
14
|
-
the documentation of the previous version
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|
-
|
71
|
+
Stoplight works right out of the box with sensible defaults:
|
65
72
|
|
66
73
|
```ruby
|
67
|
-
|
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
|
-
|
71
|
-
|
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
|
75
|
-
|
76
|
-
light.
|
77
|
-
|
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
|
-
|
81
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
91
|
-
light.run { 1 /
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
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
|
-
###
|
113
|
+
### Using Fallbacks
|
158
114
|
|
159
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
#
|
195
|
-
|
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
|
-
|
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
|
-
|
145
|
+
```ruby
|
146
|
+
gem "sinatra", require: false
|
147
|
+
gem "sinatra-contrib", require: false
|
148
|
+
```
|
202
149
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
150
|
+
Or install it manually:
|
151
|
+
```ruby
|
152
|
+
gem install sinatra
|
153
|
+
gem install sinatra-contrib
|
154
|
+
```
|
207
155
|
|
208
|
-
|
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
|
-
|
158
|
+
It is possible to run the Admin Panel separately from your application using the `stoplight-admin:<release-version>` docker image.
|
213
159
|
|
214
|
-
|
215
|
-
|
160
|
+
```shell
|
161
|
+
docker run --net=host stoplight-admin:v5
|
162
|
+
```
|
216
163
|
|
217
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
171
|
+
## Configuration
|
234
172
|
|
235
|
-
###
|
173
|
+
### Global Configuration
|
236
174
|
|
237
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
#
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
271
|
-
class ApplicationController < ActionController::Base
|
272
|
-
around_action :stoplight
|
194
|
+
### Creating Stoplights
|
273
195
|
|
274
|
-
|
196
|
+
The simplest way to create a stoplight is with a name:
|
275
197
|
|
276
|
-
|
277
|
-
|
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
|
-
|
202
|
+
You can also provide settings during creation:
|
288
203
|
|
289
|
-
|
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
|
-
|
217
|
+
### Modifying Stoplights
|
218
|
+
|
219
|
+
You can create specialized versions of existing stoplights:
|
292
220
|
|
293
221
|
```ruby
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
#
|
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
|
-
|
301
|
-
|
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
|
-
|
234
|
+
## Error Handling
|
304
235
|
|
305
|
-
|
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
|
-
|
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
|
-
|
319
|
-
|
320
|
-
Stoplight sends notifications to standard error by default.
|
247
|
+
Only track specific errors (only these count toward failure threshold)
|
321
248
|
|
322
|
-
```
|
323
|
-
Stoplight
|
324
|
-
# => [#<Stoplight::Notifier::IO:...>]
|
249
|
+
```ruby
|
250
|
+
light = Stoplight("Example API", tracked_errors: [NetworkError, Timeout::Error])
|
325
251
|
```
|
326
252
|
|
327
|
-
|
253
|
+
When both methods are used, `skipped_errors` takes precedence over `tracked_errors`.
|
328
254
|
|
329
|
-
|
255
|
+
## Advanced Configuration
|
330
256
|
|
331
|
-
|
332
|
-
the `Stoplight::Notifier::IO` notifier for that.
|
257
|
+
### Data Store
|
333
258
|
|
334
|
-
|
335
|
-
require 'stringio'
|
259
|
+
Stoplight uses an in-memory data store out of the box:
|
336
260
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
# => #<Stoplight::
|
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
|
-
|
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
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
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
|
-
####
|
280
|
+
#### Connection Pooling with Redis
|
362
281
|
|
363
|
-
|
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
|
-
|
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
|
-
|
289
|
+
Stoplight.configure do |config|
|
290
|
+
config.data_store = data_store
|
291
|
+
end
|
292
|
+
```
|
369
293
|
|
370
|
-
|
294
|
+
### Notifiers
|
371
295
|
|
372
|
-
|
296
|
+
Stoplight notifies when lights change state. Configure how these notifications are delivered:
|
373
297
|
|
374
298
|
```ruby
|
375
|
-
|
376
|
-
|
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
|
-
|
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
|
-
```
|
384
|
-
|
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
|
-
|
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
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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
|
-
|
404
|
-
|
405
|
-
|
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
|
-
|
414
|
-
|
415
|
-
|
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
|
-
|
419
|
-
#
|
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
|
-
|
424
|
-
|
425
|
-
#
|
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
|
-
|
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
|
-
|
361
|
+
Wrap controller actions with minimal effort:
|
434
362
|
|
435
363
|
```ruby
|
436
|
-
|
437
|
-
|
438
|
-
|
364
|
+
class ApplicationController < ActionController::Base
|
365
|
+
around_action :stoplight
|
366
|
+
|
367
|
+
private
|
439
368
|
|
440
|
-
|
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
|
-
|
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
|
-
|
449
|
-
|
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
|
-
|
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
|
-
|
458
|
-
|
393
|
+
Stoplight.configure do |config|
|
394
|
+
config.error_notifier = -> _ {}
|
395
|
+
config.notifiers = []
|
459
396
|
end
|
460
397
|
```
|
461
398
|
|
462
|
-
|
463
|
-
|
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.
|
472
|
-
the minimum supported Ruby version is not considered a breaking change.
|
473
|
-
|
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
|
478
|
-
|
479
|
-
|
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
|
+
Fowler’s [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
|
-
[
|
496
|
-
[
|
497
|
-
[
|
498
|
-
[
|
499
|
-
[
|
500
|
-
[
|
501
|
-
[
|
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
|
-
[
|
512
|
-
[stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
|
446
|
+
[Redis]: https://redis.io/
|