stoplight 0.1.0 → 0.2.0
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/CHANGELOG.md +14 -0
- data/README.md +73 -27
- data/lib/stoplight.rb +19 -8
- data/lib/stoplight/data_store/base.rb +22 -8
- data/lib/stoplight/data_store/memory.rb +54 -29
- data/lib/stoplight/data_store/redis.rb +46 -36
- data/lib/stoplight/error.rb +1 -3
- data/lib/stoplight/light.rb +14 -12
- data/lib/stoplight/mixin.rb +9 -0
- data/lib/stoplight/notifier.rb +6 -0
- data/lib/stoplight/notifier/base.rb +11 -0
- data/lib/stoplight/notifier/hip_chat.rb +27 -0
- data/lib/stoplight/notifier/standard_error.rb +11 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/stoplight/data_store/base_spec.rb +2 -0
- data/spec/stoplight/data_store/memory_spec.rb +1 -181
- data/spec/stoplight/data_store/redis_spec.rb +3 -182
- data/spec/stoplight/light_spec.rb +23 -3
- data/spec/stoplight/mixin_spec.rb +35 -0
- data/spec/stoplight/notifier/base_spec.rb +21 -0
- data/spec/stoplight/notifier/hip_chat_spec.rb +21 -0
- data/spec/stoplight/notifier/standard_error_spec.rb +18 -0
- data/spec/stoplight/notifier_spec.rb +6 -0
- data/spec/stoplight_spec.rb +33 -3
- data/spec/support/data_store.rb +178 -0
- metadata +23 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82aa52cf8e9479e0549ca21744fe67cbfda5aab8
|
4
|
+
data.tar.gz: fbc0abddcaa21e460473e94cd3784f6bb726c832
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a37ee7366b9d46a8a063eeb7f3541dfaa08e474869640993de0885779261bcda58be4e3eb7968e6d24fcb044dcd2c2e1f3edcf58e29383028340c80065d6c093
|
7
|
+
data.tar.gz: d0a1bf0cada95b072940372e030bf9007fedbed6a17ba109430a98a62a189d22552018e4685f1d5d0b20c7e4a87a02b47e94ec55abb3c4b48893237bd058b0e4
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.2.0 (2014-08-18)
|
4
|
+
|
5
|
+
- Switched `Stoplight.data_store` and `Stoplight.notifiers` over to using
|
6
|
+
simple accessors.
|
7
|
+
- Modified `Stoplight::DataStore::Redis` to accept an instance of `Redis`.
|
8
|
+
- Refactored `Stoplight::DataStore::Redis` to use fewer keys.
|
9
|
+
- Created `Stoplight::Notifier` and subclasses.
|
10
|
+
- Sent notifications when moving from green to red.
|
11
|
+
- Renamed `Stoplight::Light::DEFAULT_THRESHOLD` to
|
12
|
+
`Stoplight::DEFAULT_THRESHOLD`.
|
13
|
+
- Renamed `Stoplight::Error::NoFallback` to `Stoplight::Error::RedLight`.
|
14
|
+
- Created `Stoplight::Mixin#stoplight` for easily creating and running simple
|
15
|
+
stoplights.
|
16
|
+
|
3
17
|
## v0.1.0 (2014-08-12)
|
4
18
|
|
5
19
|
- Initial release.
|
data/README.md
CHANGED
@@ -6,14 +6,17 @@
|
|
6
6
|
[![Quality status][8]][9]
|
7
7
|
[![Dependency status][10]][11]
|
8
8
|
|
9
|
-
Traffic control for code. An implementation of the circuit breaker pattern in
|
9
|
+
Traffic control for code. An implementation of the circuit breaker pattern in
|
10
|
+
Ruby.
|
11
|
+
|
12
|
+
Check out [stoplight-admin][12] for controlling your stoplights.
|
10
13
|
|
11
14
|
## Installation
|
12
15
|
|
13
16
|
Add it to your Gemfile:
|
14
17
|
|
15
18
|
``` rb
|
16
|
-
gem 'stoplight', '~> 0.
|
19
|
+
gem 'stoplight', '~> 0.2.0'
|
17
20
|
```
|
18
21
|
|
19
22
|
Or install it manually:
|
@@ -22,10 +25,12 @@ Or install it manually:
|
|
22
25
|
$ gem install stoplight
|
23
26
|
```
|
24
27
|
|
25
|
-
This project uses [Semantic Versioning][
|
28
|
+
This project uses [Semantic Versioning][13].
|
26
29
|
|
27
30
|
## Setup
|
28
31
|
|
32
|
+
### Data store
|
33
|
+
|
29
34
|
Stoplight uses an in-memory data store out of the box.
|
30
35
|
|
31
36
|
``` irb
|
@@ -37,15 +42,39 @@ Stoplight uses an in-memory data store out of the box.
|
|
37
42
|
|
38
43
|
If you want to use a persistent data store, you'll have to set it up. Currently
|
39
44
|
the only supported persistent data store is Redis. Make sure you have [the Redis
|
40
|
-
gem][
|
45
|
+
gem][14] installed before configuring Stoplight.
|
41
46
|
|
42
47
|
``` irb
|
43
|
-
>> redis =
|
48
|
+
>> redis = Redis.new(url: 'redis://127.0.0.1:6379/0')
|
49
|
+
=> #<Redis:...>
|
50
|
+
>> data_store = Stoplight::DataStore::Redis.new(redis)
|
44
51
|
=> #<Stoplight::DataStore::Redis:...>
|
45
|
-
>> Stoplight.data_store
|
52
|
+
>> Stoplight.data_store = data_store
|
46
53
|
=> #<Stoplight::DataStore::Redis:...>
|
47
54
|
```
|
48
55
|
|
56
|
+
### Notifiers
|
57
|
+
|
58
|
+
Stoplight sends notifications to standard error by default.
|
59
|
+
|
60
|
+
``` irb
|
61
|
+
>> Stoplight.notifiers
|
62
|
+
=> [#<Stoplight::Notifier::StandardError:...>]
|
63
|
+
```
|
64
|
+
|
65
|
+
If you want to send notifications elsewhere, you'll have to set them up.
|
66
|
+
Currently the only other supported notifier is HipChat. Make sure you have [the
|
67
|
+
HipChat gem][] installed before configuring Stoplight.
|
68
|
+
|
69
|
+
``` irb
|
70
|
+
>> hipchat = HipChat::Client.new('token')
|
71
|
+
=> #<HipChat::Client:...>
|
72
|
+
>> notifier = Stoplight::Notifier::HipChat.new(hipchat, 'room')
|
73
|
+
=> #<Stoplight::Notifier::HipChat:...>
|
74
|
+
>> Stoplight.notifiers = [notifier])
|
75
|
+
=> [#<Stoplight::Notifier::HipChat:...>]
|
76
|
+
```
|
77
|
+
|
49
78
|
### Rails
|
50
79
|
|
51
80
|
Stoplight is designed to work seamlessly with Rails. If you want to use the
|
@@ -56,7 +85,7 @@ Stoplight:
|
|
56
85
|
``` rb
|
57
86
|
# config/initializers/stoplight.rb
|
58
87
|
require 'stoplight'
|
59
|
-
Stoplight.data_store
|
88
|
+
Stoplight.data_store = Stoplight::DataStore::Redis.new(...)
|
60
89
|
```
|
61
90
|
|
62
91
|
## Usage
|
@@ -98,11 +127,26 @@ ZeroDivisionError: divided by 0
|
|
98
127
|
>> light.run
|
99
128
|
ZeroDivisionError: divided by 0
|
100
129
|
>> light.run
|
101
|
-
Stoplight::Error::
|
130
|
+
Stoplight::Error::RedLight: Stoplight::Error::RedLight
|
102
131
|
>> light.red?
|
103
132
|
=> true
|
104
133
|
```
|
105
134
|
|
135
|
+
When the stoplight changes from green to red, it will notify every configured
|
136
|
+
notifier.
|
137
|
+
|
138
|
+
### Mixin
|
139
|
+
|
140
|
+
Since creating and running a stoplight is so common, we provide a mixin that
|
141
|
+
makes it easy.
|
142
|
+
|
143
|
+
``` irb
|
144
|
+
>> include Stoplight::Mixin
|
145
|
+
=> Object
|
146
|
+
>> stoplight('example-3') { 1.0 / 3 }
|
147
|
+
=> 0.3333333333333333
|
148
|
+
```
|
149
|
+
|
106
150
|
### Custom errors
|
107
151
|
|
108
152
|
Some errors shouldn't cause your stoplight to move into the red state. Usually
|
@@ -110,7 +154,7 @@ these are handled elsewhere in your stack and don't represent real failures. A
|
|
110
154
|
good example is `ActiveRecord::RecordNotFound`.
|
111
155
|
|
112
156
|
``` irb
|
113
|
-
>> light = Stoplight::Light.new('example-
|
157
|
+
>> light = Stoplight::Light.new('example-4') { User.find(123) }.
|
114
158
|
?> with_allowed_errors([ActiveRecord::RecordNotFound])
|
115
159
|
=> #<Stoplight::Light:...>
|
116
160
|
>> light.run
|
@@ -125,12 +169,12 @@ ActiveRecord::RecordNotFound: Couldn't find User with ID=123
|
|
125
169
|
|
126
170
|
### Custom fallback
|
127
171
|
|
128
|
-
Instead of raising a `Stoplight::Error::
|
172
|
+
Instead of raising a `Stoplight::Error::RedLight` error when in the red state,
|
129
173
|
you can provide a block to be run. This is useful when there's a good default
|
130
174
|
value for the block.
|
131
175
|
|
132
176
|
``` irb
|
133
|
-
>> light = Stoplight::Light.new('example-
|
177
|
+
>> light = Stoplight::Light.new('example-5') { fail }.
|
134
178
|
?> with_fallback { [] }
|
135
179
|
=> #<Stoplight::Light:...>
|
136
180
|
>> light.run
|
@@ -149,13 +193,13 @@ Some bits of code might be allowed to fail more or less frequently than others.
|
|
149
193
|
You can configure this by setting a custom threshold in seconds.
|
150
194
|
|
151
195
|
``` irb
|
152
|
-
>> light = Stoplight::Light.new('example-
|
196
|
+
>> light = Stoplight::Light.new('example-6') { fail }.
|
153
197
|
?> with_threshold(1)
|
154
198
|
=> #<Stoplight::Light:...>
|
155
199
|
>> light.run
|
156
200
|
RuntimeError:
|
157
201
|
>> light.run
|
158
|
-
Stoplight::Error::
|
202
|
+
Stoplight::Error::RedLight: Stoplight::Error::RedLight
|
159
203
|
```
|
160
204
|
|
161
205
|
### Rails
|
@@ -178,12 +222,12 @@ end
|
|
178
222
|
|
179
223
|
## Credits
|
180
224
|
|
181
|
-
Stoplight is brought to you by [@camdez][
|
182
|
-
inspired by Martin Fowler's [CircuitBreaker][
|
225
|
+
Stoplight is brought to you by [@camdez][15] and [@tfausak][16] from [@OrgSync][17]. We were
|
226
|
+
inspired by Martin Fowler's [CircuitBreaker][18] article.
|
183
227
|
|
184
228
|
If this gem isn't cutting it for you, there are a few alternatives, including:
|
185
|
-
[circuit_b][
|
186
|
-
[ya_circuit_breaker][
|
229
|
+
[circuit_b][19], [circuit_breaker][20], [simple_circuit_breaker][21], and
|
230
|
+
[ya_circuit_breaker][22].
|
187
231
|
|
188
232
|
[1]: https://github.com/orgsync/stoplight
|
189
233
|
[2]: https://badge.fury.io/rb/stoplight.svg
|
@@ -196,13 +240,15 @@ If this gem isn't cutting it for you, there are a few alternatives, including:
|
|
196
240
|
[9]: https://codeclimate.com/github/orgsync/stoplight
|
197
241
|
[10]: https://gemnasium.com/orgsync/stoplight.svg
|
198
242
|
[11]: https://gemnasium.com/orgsync/stoplight
|
199
|
-
[12]:
|
200
|
-
[13]:
|
201
|
-
[14]: https://
|
202
|
-
[
|
203
|
-
[
|
204
|
-
[
|
205
|
-
[
|
206
|
-
[
|
207
|
-
[
|
208
|
-
[
|
243
|
+
[12]: https://github.com/orgsync/stoplight-admin
|
244
|
+
[13]: http://semver.org/spec/v2.0.0.html
|
245
|
+
[14]: https://rubygems.org/gems/redis
|
246
|
+
[the hipchat gem]: https://rubygems.org/gems/hipchat
|
247
|
+
[15]: https://github.com/camdez
|
248
|
+
[16]: https://github.com/tfausak
|
249
|
+
[17]: https://github.com/OrgSync
|
250
|
+
[18]: http://martinfowler.com/bliki/CircuitBreaker.html
|
251
|
+
[19]: https://github.com/alg/circuit_b
|
252
|
+
[20]: https://github.com/wsargent/circuit_breaker
|
253
|
+
[21]: https://github.com/soundcloud/simple_circuit_breaker
|
254
|
+
[22]: https://github.com/wooga/circuit_breaker
|
data/lib/stoplight.rb
CHANGED
@@ -8,10 +8,21 @@ require 'stoplight/data_store/redis'
|
|
8
8
|
require 'stoplight/error'
|
9
9
|
require 'stoplight/failure'
|
10
10
|
require 'stoplight/light'
|
11
|
+
require 'stoplight/mixin'
|
12
|
+
require 'stoplight/notifier'
|
13
|
+
require 'stoplight/notifier/base'
|
14
|
+
require 'stoplight/notifier/hip_chat'
|
15
|
+
require 'stoplight/notifier/standard_error'
|
11
16
|
|
12
17
|
module Stoplight
|
13
18
|
# @return [Gem::Version]
|
14
|
-
VERSION = Gem::Version.new('0.
|
19
|
+
VERSION = Gem::Version.new('0.2.0')
|
20
|
+
|
21
|
+
# @return [Integer]
|
22
|
+
DEFAULT_THRESHOLD = 3
|
23
|
+
|
24
|
+
@data_store = DataStore::Memory.new
|
25
|
+
@notifiers = [Notifier::StandardError.new]
|
15
26
|
|
16
27
|
class << self
|
17
28
|
extend Forwardable
|
@@ -20,8 +31,10 @@ module Stoplight
|
|
20
31
|
attempts
|
21
32
|
clear_attempts
|
22
33
|
clear_failures
|
34
|
+
delete
|
23
35
|
failures
|
24
36
|
names
|
37
|
+
purge
|
25
38
|
record_attempt
|
26
39
|
record_failure
|
27
40
|
set_state
|
@@ -29,13 +42,11 @@ module Stoplight
|
|
29
42
|
state
|
30
43
|
)
|
31
44
|
|
32
|
-
# @param data_store [DataStore::Base]
|
33
45
|
# @return [DataStore::Base]
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
46
|
+
attr_accessor :data_store
|
47
|
+
|
48
|
+
# @return [Array<Notifier::Base>]
|
49
|
+
attr_accessor :notifiers
|
39
50
|
|
40
51
|
# @param name [String]
|
41
52
|
# @return [Boolean]
|
@@ -59,7 +70,7 @@ module Stoplight
|
|
59
70
|
# @param name [String]
|
60
71
|
# @return [Integer]
|
61
72
|
def threshold(name)
|
62
|
-
data_store.threshold(name) ||
|
73
|
+
data_store.threshold(name) || DEFAULT_THRESHOLD
|
63
74
|
end
|
64
75
|
end
|
65
76
|
end
|
@@ -8,6 +8,16 @@ module Stoplight
|
|
8
8
|
fail NotImplementedError
|
9
9
|
end
|
10
10
|
|
11
|
+
# Deletes all green lights without failures.
|
12
|
+
def purge
|
13
|
+
fail NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param _name [String]
|
17
|
+
def delete(_name)
|
18
|
+
fail NotImplementedError
|
19
|
+
end
|
20
|
+
|
11
21
|
# @param _name [String]
|
12
22
|
# @param _error [Exception]
|
13
23
|
def record_failure(_name, _error)
|
@@ -76,20 +86,24 @@ module Stoplight
|
|
76
86
|
fail ArgumentError, 'Invalid state'
|
77
87
|
end
|
78
88
|
|
79
|
-
def
|
80
|
-
|
89
|
+
def attempts_key
|
90
|
+
key('attempts')
|
91
|
+
end
|
92
|
+
|
93
|
+
def failures_key(name)
|
94
|
+
key('failures', name)
|
81
95
|
end
|
82
96
|
|
83
|
-
def
|
84
|
-
key(
|
97
|
+
def states_key
|
98
|
+
key('states')
|
85
99
|
end
|
86
100
|
|
87
|
-
def
|
88
|
-
key(
|
101
|
+
def thresholds_key
|
102
|
+
key('thresholds')
|
89
103
|
end
|
90
104
|
|
91
|
-
def
|
92
|
-
|
105
|
+
def key(slug, name = nil)
|
106
|
+
[KEY_PREFIX, name, slug].compact.join(':')
|
93
107
|
end
|
94
108
|
end
|
95
109
|
end
|
@@ -8,59 +8,84 @@ module Stoplight
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def names
|
11
|
-
|
12
|
-
match = /^#{DataStore::KEY_PREFIX}:(.+):([^:]+)$/.match(key)
|
13
|
-
match[1] if match
|
14
|
-
end.compact.uniq
|
11
|
+
all_thresholds.keys
|
15
12
|
end
|
16
13
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
def purge
|
15
|
+
names
|
16
|
+
.select { |l| failures(l).empty? }
|
17
|
+
.each { |l| delete(l) }
|
21
18
|
end
|
22
19
|
|
23
|
-
def
|
24
|
-
|
20
|
+
def delete(name)
|
21
|
+
clear_attempts(name)
|
22
|
+
clear_failures(name)
|
23
|
+
all_states.delete(name)
|
24
|
+
all_thresholds.delete(name)
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
@data[failure_key(name)] || []
|
29
|
-
end
|
27
|
+
# @group Attempts
|
30
28
|
|
31
|
-
def
|
32
|
-
|
29
|
+
def attempts(name)
|
30
|
+
all_attempts[name] || 0
|
33
31
|
end
|
34
32
|
|
35
|
-
def
|
36
|
-
|
33
|
+
def record_attempt(name)
|
34
|
+
all_attempts[name] ||= 0
|
35
|
+
all_attempts[name] += 1
|
37
36
|
end
|
38
37
|
|
39
|
-
def
|
40
|
-
|
38
|
+
def clear_attempts(name)
|
39
|
+
all_attempts.delete(name)
|
41
40
|
end
|
42
41
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@data[
|
42
|
+
# @group Failures
|
43
|
+
|
44
|
+
def failures(name)
|
45
|
+
@data[failures_key(name)] || []
|
47
46
|
end
|
48
47
|
|
49
|
-
def
|
50
|
-
@data.
|
48
|
+
def record_failure(name, error)
|
49
|
+
(@data[failures_key(name)] ||= []).push(Failure.new(error))
|
51
50
|
end
|
52
51
|
|
53
|
-
def
|
54
|
-
@data
|
52
|
+
def clear_failures(name)
|
53
|
+
@data.delete(failures_key(name))
|
55
54
|
end
|
56
55
|
|
56
|
+
# @group State
|
57
|
+
|
57
58
|
def state(name)
|
58
|
-
|
59
|
+
all_states[name] || STATE_UNLOCKED
|
59
60
|
end
|
60
61
|
|
61
62
|
def set_state(name, state)
|
62
63
|
validate_state!(state)
|
63
|
-
|
64
|
+
all_states[name] = state
|
65
|
+
end
|
66
|
+
|
67
|
+
# @group Threshold
|
68
|
+
|
69
|
+
def threshold(name)
|
70
|
+
all_thresholds[name]
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_threshold(name, threshold)
|
74
|
+
all_thresholds[name] = threshold
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def all_attempts
|
80
|
+
@data[attempts_key] ||= {}
|
81
|
+
end
|
82
|
+
|
83
|
+
def all_states
|
84
|
+
@data[states_key] ||= {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def all_thresholds
|
88
|
+
@data[thresholds_key] ||= {}
|
64
89
|
end
|
65
90
|
end
|
66
91
|
end
|