stoplight 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|