stoplight 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5b9db0e92d38791e2989cbd757cb1b0dcbf4e73e
4
- data.tar.gz: d43063006ea37bae49bbe203f25f059390e1c7f4
3
+ metadata.gz: e500fe8c6d34e694cfbc2c46540a445eb997f89a
4
+ data.tar.gz: ec03f4f69321194449e3e0918183c6cbea4f9e2b
5
5
  SHA512:
6
- metadata.gz: fa7c3d3d0895e885d977292a09081f0c8ba23d22e1f228c60d298ed935f8bb32557549ffb5ef13419eff16a86b8e8a3f7173e9efac29ba7b2fe919505d8eb610
7
- data.tar.gz: c7808ed8e151962b345787aaa9634a79f191fd1bc19ee8d5dd0bb04099fcf7a2765c04279b7d359afcae5bc0be84a4cf8e77fd27a2de382bc3cc92fd1342ce70
6
+ metadata.gz: 8b6f647cb561a37f14e44054b2549defcf1f72df27c0d8ba93c39d98b13d1b3f44c34b5c10070aa4d9c5d5d7c4b438983a532531bbad253605769b6a53925bdf
7
+ data.tar.gz: 75a107deca63c8fca90ec6466f66b98524af37263750d77ae06526a280331f65de4a83e1647ad41ef568c6afa7e103241d070ebe5fedd23d6b41dcbf312514e2
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0 (2014-09-11)
4
+
5
+ - Allowed formatting notifications.
6
+ - Added automatic recovery from red lights.
7
+ - Added `Stoplight::DataStore#clear_stale` for clearing stale lights.
8
+
3
9
  ## v0.2.1 (2014-08-20)
4
10
 
5
11
  - Forced times to be serialized as strings.
data/README.md CHANGED
@@ -11,12 +11,26 @@ Ruby.
11
11
 
12
12
  Check out [stoplight-admin][12] for controlling your stoplights.
13
13
 
14
+ - [Installation](#installation)
15
+ - [Setup](#setup)
16
+ - [Data store](#data-store)
17
+ - [Notifiers](#notifiers)
18
+ - [Rails](#rails)
19
+ - [Usage](#usage)
20
+ - [Mixin](#mixin)
21
+ - [Custom errors](#custom-errors)
22
+ - [Custom fallback](#custom-fallback)
23
+ - [Custom threshold](#custom-threshold)
24
+ - [Custom timeout](#custom-timeout)
25
+ - [Rails](#rails-1)
26
+ - [Credits](#credits)
27
+
14
28
  ## Installation
15
29
 
16
30
  Add it to your Gemfile:
17
31
 
18
32
  ``` rb
19
- gem 'stoplight', '~> 0.2.1'
33
+ gem 'stoplight', '~> 0.3.0'
20
34
  ```
21
35
 
22
36
  Or install it manually:
@@ -45,6 +59,8 @@ the only supported persistent data store is Redis. Make sure you have [the Redis
45
59
  gem][14] installed before configuring Stoplight.
46
60
 
47
61
  ``` irb
62
+ >> require 'redis'
63
+ => true
48
64
  >> redis = Redis.new(url: 'redis://127.0.0.1:6379/0')
49
65
  => #<Redis:...>
50
66
  >> data_store = Stoplight::DataStore::Redis.new(redis)
@@ -64,15 +80,17 @@ Stoplight sends notifications to standard error by default.
64
80
 
65
81
  If you want to send notifications elsewhere, you'll have to set them up.
66
82
  Currently the only other supported notifier is HipChat. Make sure you have [the
67
- HipChat gem][] installed before configuring Stoplight.
83
+ HipChat gem][15] installed before configuring Stoplight.
68
84
 
69
85
  ``` irb
86
+ >> require 'hipchat'
87
+ => true
70
88
  >> hipchat = HipChat::Client.new('token')
71
89
  => #<HipChat::Client:...>
72
90
  >> notifier = Stoplight::Notifier::HipChat.new(hipchat, 'room')
73
91
  => #<Stoplight::Notifier::HipChat:...>
74
- >> Stoplight.notifiers = [notifier])
75
- => [#<Stoplight::Notifier::HipChat:...>]
92
+ >> Stoplight.notifiers << notifier
93
+ => [#<Stoplight::Notifier::StandardError:...>, #<Stoplight::Notifier::HipChat:...>]
76
94
  ```
77
95
 
78
96
  ### Rails
@@ -86,6 +104,7 @@ Stoplight:
86
104
  # config/initializers/stoplight.rb
87
105
  require 'stoplight'
88
106
  Stoplight.data_store = Stoplight::DataStore::Redis.new(...)
107
+ Stoplight.notifiers << Stoplight::Notifier::HipChat.new(...)
89
108
  ```
90
109
 
91
110
  ## Usage
@@ -98,7 +117,7 @@ To get started, create a stoplight:
98
117
  ```
99
118
 
100
119
  Then you can run it and it will return the result of calling the block. This is
101
- the "green" state.
120
+ the green state.
102
121
 
103
122
  ``` irb
104
123
  >> light.run
@@ -117,7 +136,7 @@ stoplight. That's not very interesting though. Let's create a failing stoplight:
117
136
 
118
137
  Now when you run it, the error will be recorded and passed through. After
119
138
  running it a few times, the stoplight will stop trying and fail fast. This is
120
- the "red" state.
139
+ the red state.
121
140
 
122
141
  ``` irb
123
142
  >> light.run
@@ -127,7 +146,8 @@ ZeroDivisionError: divided by 0
127
146
  >> light.run
128
147
  ZeroDivisionError: divided by 0
129
148
  >> light.run
130
- Stoplight::Error::RedLight: Stoplight::Error::RedLight
149
+ Switching example-2 from green to red.
150
+ Stoplight::Error::RedLight: example-2
131
151
  >> light.red?
132
152
  => true
133
153
  ```
@@ -155,7 +175,7 @@ good example is `ActiveRecord::RecordNotFound`.
155
175
 
156
176
  ``` irb
157
177
  >> light = Stoplight::Light.new('example-4') { User.find(123) }.
158
- ?> with_allowed_errors([ActiveRecord::RecordNotFound])
178
+ .. with_allowed_errors([ActiveRecord::RecordNotFound])
159
179
  => #<Stoplight::Light:...>
160
180
  >> light.run
161
181
  ActiveRecord::RecordNotFound: Couldn't find User with ID=123
@@ -175,7 +195,7 @@ value for the block.
175
195
 
176
196
  ``` irb
177
197
  >> light = Stoplight::Light.new('example-5') { fail }.
178
- ?> with_fallback { [] }
198
+ .. with_fallback { [] }
179
199
  => #<Stoplight::Light:...>
180
200
  >> light.run
181
201
  RuntimeError:
@@ -194,14 +214,42 @@ You can configure this by setting a custom threshold in seconds.
194
214
 
195
215
  ``` irb
196
216
  >> light = Stoplight::Light.new('example-6') { fail }.
197
- ?> with_threshold(1)
217
+ .. with_threshold(1)
218
+ => #<Stoplight::Light:...>
219
+ >> light.run
220
+ RuntimeError:
221
+ >> light.run
222
+ Stoplight::Error::RedLight: example-6
223
+ ```
224
+
225
+ ### Custom timeout
226
+
227
+ Stoplights will automatically attempt to recover after a certain amount of time.
228
+ This timeout is customizable.
229
+
230
+ ``` irb
231
+ >> light = Stoplight::Light.new('example-7') { fail }.
232
+ .. with_timeout(1)
198
233
  => #<Stoplight::Light:...>
199
234
  >> light.run
200
235
  RuntimeError:
201
236
  >> light.run
202
- Stoplight::Error::RedLight: Stoplight::Error::RedLight
237
+ RuntimeError:
238
+ >> light.run
239
+ RuntimeError:
240
+ >> light.run
241
+ Switching example-7 from green to red.
242
+ Stoplight::Error::RedLight: example-7
243
+ >> sleep(1)
244
+ => 1
245
+ >> light.yellow?
246
+ => true
247
+ >> light.run
248
+ RuntimeError:
203
249
  ```
204
250
 
251
+ Set the timeout to `-1` to disable automatic recovery.
252
+
205
253
  ### Rails
206
254
 
207
255
  Stoplight was designed to wrap Rails actions with minimal effort. Here's an
@@ -222,12 +270,13 @@ end
222
270
 
223
271
  ## Credits
224
272
 
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.
273
+ Stoplight is brought to you by [@camdez][16] and [@tfausak][17] from
274
+ [@OrgSync][18]. We were inspired by Martin Fowler's [CircuitBreaker][19]
275
+ article.
227
276
 
228
277
  If this gem isn't cutting it for you, there are a few alternatives, including:
229
- [circuit_b][19], [circuit_breaker][20], [simple_circuit_breaker][21], and
230
- [ya_circuit_breaker][22].
278
+ [circuit_b][20], [circuit_breaker][21], [simple_circuit_breaker][22], and
279
+ [ya_circuit_breaker][23].
231
280
 
232
281
  [1]: https://github.com/orgsync/stoplight
233
282
  [2]: https://badge.fury.io/rb/stoplight.svg
@@ -243,12 +292,12 @@ If this gem isn't cutting it for you, there are a few alternatives, including:
243
292
  [12]: https://github.com/orgsync/stoplight-admin
244
293
  [13]: http://semver.org/spec/v2.0.0.html
245
294
  [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
295
+ [15]: https://rubygems.org/gems/hipchat
296
+ [16]: https://github.com/camdez
297
+ [17]: https://github.com/tfausak
298
+ [18]: https://github.com/OrgSync
299
+ [19]: http://martinfowler.com/bliki/CircuitBreaker.html
300
+ [20]: https://github.com/alg/circuit_b
301
+ [21]: https://github.com/wsargent/circuit_breaker
302
+ [22]: https://github.com/soundcloud/simple_circuit_breaker
303
+ [23]: https://github.com/wooga/circuit_breaker
@@ -1,6 +1,5 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'forwardable'
4
3
  require 'stoplight/data_store'
5
4
  require 'stoplight/data_store/base'
6
5
  require 'stoplight/data_store/memory'
@@ -16,61 +15,16 @@ require 'stoplight/notifier/standard_error'
16
15
 
17
16
  module Stoplight
18
17
  # @return [Gem::Version]
19
- VERSION = Gem::Version.new('0.2.1')
20
-
21
- # @return [Integer]
22
- DEFAULT_THRESHOLD = 3
18
+ VERSION = Gem::Version.new('0.3.0')
23
19
 
24
20
  @data_store = DataStore::Memory.new
25
21
  @notifiers = [Notifier::StandardError.new]
26
22
 
27
23
  class << self
28
- extend Forwardable
29
-
30
- def_delegators :data_store, *%w(
31
- attempts
32
- clear_attempts
33
- clear_failures
34
- delete
35
- failures
36
- names
37
- purge
38
- record_attempt
39
- record_failure
40
- set_state
41
- set_threshold
42
- state
43
- )
44
-
45
24
  # @return [DataStore::Base]
46
25
  attr_accessor :data_store
47
26
 
48
27
  # @return [Array<Notifier::Base>]
49
28
  attr_accessor :notifiers
50
-
51
- # @param name [String]
52
- # @return [Boolean]
53
- def green?(name)
54
- case data_store.state(name)
55
- when DataStore::STATE_LOCKED_GREEN
56
- true
57
- when DataStore::STATE_LOCKED_RED
58
- false
59
- else
60
- data_store.failures(name).size < threshold(name)
61
- end
62
- end
63
-
64
- # @param name [String]
65
- # @return (see .green?)
66
- def red?(name)
67
- !green?(name)
68
- end
69
-
70
- # @param name [String]
71
- # @return [Integer]
72
- def threshold(name)
73
- data_store.threshold(name) || DEFAULT_THRESHOLD
74
- end
75
29
  end
76
30
  end
@@ -2,14 +2,152 @@
2
2
 
3
3
  module Stoplight
4
4
  module DataStore
5
- # @return [String]
6
- KEY_PREFIX = 'stoplight'
5
+ KEY_PREFIX = 'stoplight'.freeze
6
+
7
+ COLOR_GREEN = 'green'.freeze
8
+ COLOR_YELLOW = 'yellow'.freeze
9
+ COLOR_RED = 'red'.freeze
10
+ COLORS = Set.new([
11
+ COLOR_GREEN,
12
+ COLOR_YELLOW,
13
+ COLOR_RED
14
+ ]).freeze
7
15
 
8
- # @return [Set<String>]
16
+ STATE_UNLOCKED = 'unlocked'.freeze
17
+ STATE_LOCKED_GREEN = 'locked_green'.freeze
18
+ STATE_LOCKED_RED = 'locked_red'.freeze
9
19
  STATES = Set.new([
10
- STATE_LOCKED_GREEN = 'locked_green',
11
- STATE_LOCKED_RED = 'locked_red',
12
- STATE_UNLOCKED = 'unlocked'
20
+ STATE_UNLOCKED,
21
+ STATE_LOCKED_GREEN,
22
+ STATE_LOCKED_RED
13
23
  ]).freeze
24
+
25
+ DEFAULT_ATTEMPTS = 0
26
+ DEFAULT_FAILURES = []
27
+ DEFAULT_STATE = STATE_UNLOCKED
28
+ DEFAULT_THRESHOLD = 3
29
+ DEFAULT_TIMEOUT = 60
30
+
31
+ module_function
32
+
33
+ # @group Colors
34
+
35
+ # @param state [String]
36
+ # @param threshold [Integer]
37
+ # @param failures [Array<Failure>]
38
+ # @param timeout [Integer]
39
+ # @return [String]
40
+ def colorize(state, threshold, failures, timeout)
41
+ case
42
+ when state == STATE_LOCKED_GREEN then COLOR_GREEN
43
+ when state == STATE_LOCKED_RED then COLOR_RED
44
+ when threshold < 1 then COLOR_RED
45
+ when failures.size < threshold then COLOR_GREEN
46
+ when Time.now - failures.last.time > timeout then COLOR_YELLOW
47
+ else COLOR_RED
48
+ end
49
+ end
50
+
51
+ # @group Validation
52
+
53
+ # @param color [String]
54
+ # @raise [ArgumentError]
55
+ def validate_color!(color)
56
+ return if valid_color?(color)
57
+ fail Error::InvalidColor, color.inspect
58
+ end
59
+
60
+ # @param color [String]
61
+ # @return [Boolean]
62
+ def valid_color?(color)
63
+ COLORS.include?(color)
64
+ end
65
+
66
+ # @param failure [Failure]
67
+ # @raise [ArgumentError]
68
+ def validate_failure!(failure)
69
+ return if valid_failure?(failure)
70
+ fail Error::InvalidFailure, failure.inspect
71
+ end
72
+
73
+ # @param failure [Failure]
74
+ # @return [Boolean]
75
+ def valid_failure?(failure)
76
+ failure.is_a?(Failure)
77
+ end
78
+
79
+ # @param state [String]
80
+ # @raise [ArgumentError]
81
+ def validate_state!(state)
82
+ return if valid_state?(state)
83
+ fail Error::InvalidState, state.inspect
84
+ end
85
+
86
+ # @param state [String]
87
+ # @return [Boolean]
88
+ def valid_state?(state)
89
+ STATES.include?(state)
90
+ end
91
+
92
+ # @param threshold [Integer]
93
+ # @raise [ArgumentError]
94
+ def validate_threshold!(threshold)
95
+ return if valid_threshold?(threshold)
96
+ fail Error::InvalidThreshold, threshold.inspect
97
+ end
98
+
99
+ # @param threshold [Integer]
100
+ # @return [Boolean]
101
+ def valid_threshold?(threshold)
102
+ threshold.is_a?(Integer)
103
+ end
104
+
105
+ # @param timeout [Integer]
106
+ # @raise [ArgumentError]
107
+ def validate_timeout!(timeout)
108
+ return if valid_timeout?(timeout)
109
+ fail Error::InvalidTimeout, timeout.inspect
110
+ end
111
+
112
+ # @param timeout [Integer]
113
+ # @return [Boolean]
114
+ def valid_timeout?(timeout)
115
+ timeout.is_a?(Integer)
116
+ end
117
+
118
+ # @group Keys
119
+
120
+ # @return (see #key)
121
+ def attempts_key
122
+ key('attempts')
123
+ end
124
+
125
+ # @param name [String]
126
+ # @return (see #key)
127
+ def failures_key(name)
128
+ key('failures', name)
129
+ end
130
+
131
+ # @return (see #key)
132
+ def states_key
133
+ key('states')
134
+ end
135
+
136
+ # @return (see #key)
137
+ def thresholds_key
138
+ key('thresholds')
139
+ end
140
+
141
+ # @return (see #key)
142
+ def timeouts_key
143
+ key('timeouts')
144
+ end
145
+
146
+ # @param slug [String]
147
+ # @param suffix [String, nil]
148
+ # @return [String]
149
+ def key(slug, suffix = nil)
150
+ [KEY_PREFIX, slug, suffix].compact.join(':')
151
+ end
14
152
  end
15
153
  end
@@ -8,66 +8,101 @@ module Stoplight
8
8
  fail NotImplementedError
9
9
  end
10
10
 
11
- # Deletes all green lights without failures.
12
- def purge
11
+ # @return [nil]
12
+ def clear_stale
13
13
  fail NotImplementedError
14
14
  end
15
15
 
16
16
  # @param _name [String]
17
- def delete(_name)
17
+ # @return [nil]
18
+ def clear(_name)
18
19
  fail NotImplementedError
19
20
  end
20
21
 
21
22
  # @param _name [String]
22
- # @param _error [Exception]
23
- def record_failure(_name, _error)
23
+ # @return [nil]
24
+ def sync(_name)
24
25
  fail NotImplementedError
25
26
  end
26
27
 
28
+ # @group Colors
29
+
30
+ # @param name [String]
31
+ # @return [Boolean]
32
+ def green?(name)
33
+ color = get_color(name)
34
+ DataStore.validate_color!(color)
35
+ color == COLOR_GREEN
36
+ end
37
+
38
+ # @param name [String]
39
+ # @return [Boolean]
40
+ def yellow?(name)
41
+ color = get_color(name)
42
+ DataStore.validate_color!(color)
43
+ color == COLOR_YELLOW
44
+ end
45
+
46
+ # @param name [String]
47
+ # @return [Boolean]
48
+ def red?(name)
49
+ color = get_color(name)
50
+ DataStore.validate_color!(color)
51
+ color == COLOR_RED
52
+ end
53
+
27
54
  # @param _name [String]
28
- def clear_failures(_name)
55
+ # @return [String]
56
+ def get_color(_name)
29
57
  fail NotImplementedError
30
58
  end
31
59
 
60
+ # @group Attempts
61
+
32
62
  # @param _name [String]
33
- # @return [Array<Failure>]
34
- def failures(_name)
63
+ # @return [Integer]
64
+ def get_attempts(_name)
35
65
  fail NotImplementedError
36
66
  end
37
67
 
38
68
  # @param _name [String]
39
69
  # @return [Integer]
40
- def threshold(_name)
70
+ def record_attempt(_name)
41
71
  fail NotImplementedError
42
72
  end
43
73
 
44
74
  # @param _name [String]
45
- # @param _threshold [Integer]
46
- # @return (see #threshold)
47
- def set_threshold(_name, _threshold)
75
+ # @return [nil]
76
+ def clear_attempts(_name)
48
77
  fail NotImplementedError
49
78
  end
50
79
 
80
+ # @group Failures
81
+
51
82
  # @param _name [String]
52
- # @return (see #attempts)
53
- def record_attempt(_name)
83
+ # @return [Array<Failure>]
84
+ def get_failures(_name)
54
85
  fail NotImplementedError
55
86
  end
56
87
 
57
88
  # @param _name [String]
58
- def clear_attempts(_name)
89
+ # @param _failure [Failure]
90
+ # @return [Failure]
91
+ def record_failure(_name, _failure)
59
92
  fail NotImplementedError
60
93
  end
61
94
 
62
95
  # @param _name [String]
63
- # @return [Integer]
64
- def attempts(_name)
96
+ # @return [nil]
97
+ def clear_failures(_name)
65
98
  fail NotImplementedError
66
99
  end
67
100
 
101
+ # @group States
102
+
68
103
  # @param _name [String]
69
104
  # @return [String]
70
- def state(_name)
105
+ def get_state(_name)
71
106
  fail NotImplementedError
72
107
  end
73
108
 
@@ -75,35 +110,55 @@ module Stoplight
75
110
  # @param _state [String]
76
111
  # @return [String]
77
112
  def set_state(_name, _state)
78
- # REVIEW: Should we clear failures here?
79
113
  fail NotImplementedError
80
114
  end
81
115
 
82
- private
116
+ # @param _name [String]
117
+ # @return [nil]
118
+ def clear_state(_name)
119
+ fail NotImplementedError
120
+ end
83
121
 
84
- def validate_state!(state)
85
- return if DataStore::STATES.include?(state)
86
- fail ArgumentError, 'Invalid state'
122
+ # @group Thresholds
123
+
124
+ # @param _name [String]
125
+ # @return [Integer]
126
+ def get_threshold(_name)
127
+ fail NotImplementedError
87
128
  end
88
129
 
89
- def attempts_key
90
- key('attempts')
130
+ # @param _name [String]
131
+ # @param _threshold [Integer]
132
+ # @return [Integer]
133
+ def set_threshold(_name, _threshold)
134
+ fail NotImplementedError
91
135
  end
92
136
 
93
- def failures_key(name)
94
- key('failures', name)
137
+ # @param _name [String]
138
+ # @return [nil]
139
+ def clear_threshold(_name)
140
+ fail NotImplementedError
95
141
  end
96
142
 
97
- def states_key
98
- key('states')
143
+ # @group Timeouts
144
+
145
+ # @param _name [String]
146
+ # @return [Integer]
147
+ def get_timeout(_name)
148
+ fail NotImplementedError
99
149
  end
100
150
 
101
- def thresholds_key
102
- key('thresholds')
151
+ # @param _name [String]
152
+ # @param _timeout [Integer]
153
+ # @return [Integer]
154
+ def set_timeout(_name, _timeout)
155
+ fail NotImplementedError
103
156
  end
104
157
 
105
- def key(slug, name = nil)
106
- [KEY_PREFIX, name, slug].compact.join(':')
158
+ # @param _name [String]
159
+ # @return [nil]
160
+ def clear_timeout(_name)
161
+ fail NotImplementedError
107
162
  end
108
163
  end
109
164
  end