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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c9bd1ff4a7ba5920b6b1a5e98c829b88ca495ec2
4
- data.tar.gz: 3c7698e907ec212e7072b14c9d4df4e54dd9c1b9
3
+ metadata.gz: 82aa52cf8e9479e0549ca21744fe67cbfda5aab8
4
+ data.tar.gz: fbc0abddcaa21e460473e94cd3784f6bb726c832
5
5
  SHA512:
6
- metadata.gz: 4e9fe6173c1b7524fe02ec8939512aea3c0a4cb5cf09ccf5f6cacde09ed461fc6ccfc1b4fc37eaf5008206b398d2aab9b71f5d9c1c39b94ded8ef4a57fb7b942
7
- data.tar.gz: f6a8ffb8069abad09d6a795c5acc368b6ea954e8d9d3c6359d36b61e54625923727834a49b1ac827c55cda32d2e36a9b7d16ae9fc9939cd2f35cf14870e93a7b
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 Ruby.
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.1.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][12].
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][13] installed before configuring Stoplight.
45
+ gem][14] installed before configuring Stoplight.
41
46
 
42
47
  ``` irb
43
- >> redis = Stoplight::DataStore::Redis.new(url: 'redis://127.0.0.1:6379/0')
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(redis)
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(Stoplight::DataStore::Redis.new(...))
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::NoFallback: Stoplight::Error::NoFallback
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-3') { User.find(123) }.
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::NoFallback` error when in the red state,
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-4') { fail }.
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-5') { fail }.
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::NoFallback: Stoplight::Error::NoFallback
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][14] and [@tfausak][15] from [@OrgSync][16]. We were
182
- inspired by Martin Fowler's [CircuitBreaker][17] article.
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][18], [circuit_breaker][19], [simple_circuit_breaker][20], and
186
- [ya_circuit_breaker][21].
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]: http://semver.org/spec/v2.0.0.html
200
- [13]: https://rubygems.org/gems/redis
201
- [14]: https://github.com/camdez
202
- [15]: https://github.com/tfausak
203
- [16]: https://github.com/OrgSync
204
- [17]: http://martinfowler.com/bliki/CircuitBreaker.html
205
- [18]: https://github.com/alg/circuit_b
206
- [19]: https://github.com/wsargent/circuit_breaker
207
- [20]: https://github.com/soundcloud/simple_circuit_breaker
208
- [21]: https://github.com/wooga/circuit_breaker
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.1.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
- def data_store(data_store = nil)
35
- @data_store = data_store if data_store
36
- @data_store = DataStore::Memory.new unless defined?(@data_store)
37
- @data_store
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) || Light::DEFAULT_THRESHOLD
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 key(name, slug)
80
- [DataStore::KEY_PREFIX, name, slug].join(':')
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 attempt_key(name)
84
- key(name, 'attempts')
97
+ def states_key
98
+ key('states')
85
99
  end
86
100
 
87
- def failure_key(name)
88
- key(name, 'failures')
101
+ def thresholds_key
102
+ key('thresholds')
89
103
  end
90
104
 
91
- def settings_key(name)
92
- key(name, 'settings')
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
- @data.keys.map do |key|
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 record_failure(name, error)
18
- failure = Failure.new(error)
19
- array = @data[failure_key(name)] ||= []
20
- array.push(failure)
14
+ def purge
15
+ names
16
+ .select { |l| failures(l).empty? }
17
+ .each { |l| delete(l) }
21
18
  end
22
19
 
23
- def clear_failures(name)
24
- @data.delete(failure_key(name))
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
- def failures(name)
28
- @data[failure_key(name)] || []
29
- end
27
+ # @group Attempts
30
28
 
31
- def settings(name)
32
- @data[settings_key(name)] ||= {}
29
+ def attempts(name)
30
+ all_attempts[name] || 0
33
31
  end
34
32
 
35
- def threshold(name)
36
- settings(name)['threshold']
33
+ def record_attempt(name)
34
+ all_attempts[name] ||= 0
35
+ all_attempts[name] += 1
37
36
  end
38
37
 
39
- def set_threshold(name, threshold)
40
- settings(name)['threshold'] = threshold
38
+ def clear_attempts(name)
39
+ all_attempts.delete(name)
41
40
  end
42
41
 
43
- def record_attempt(name)
44
- key = attempt_key(name)
45
- @data[key] ||= 0
46
- @data[key] += 1
42
+ # @group Failures
43
+
44
+ def failures(name)
45
+ @data[failures_key(name)] || []
47
46
  end
48
47
 
49
- def clear_attempts(name)
50
- @data.delete(attempt_key(name))
48
+ def record_failure(name, error)
49
+ (@data[failures_key(name)] ||= []).push(Failure.new(error))
51
50
  end
52
51
 
53
- def attempts(name)
54
- @data[attempt_key(name)] || 0
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
- settings(name)['state'] || DataStore::STATE_UNLOCKED
59
+ all_states[name] || STATE_UNLOCKED
59
60
  end
60
61
 
61
62
  def set_state(name, state)
62
63
  validate_state!(state)
63
- settings(name)['state'] = state
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