stoplight 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c9bd1ff4a7ba5920b6b1a5e98c829b88ca495ec2
4
+ data.tar.gz: 3c7698e907ec212e7072b14c9d4df4e54dd9c1b9
5
+ SHA512:
6
+ metadata.gz: 4e9fe6173c1b7524fe02ec8939512aea3c0a4cb5cf09ccf5f6cacde09ed461fc6ccfc1b4fc37eaf5008206b398d2aab9b71f5d9c1c39b94ded8ef4a57fb7b942
7
+ data.tar.gz: f6a8ffb8069abad09d6a795c5acc368b6ea954e8d9d3c6359d36b61e54625923727834a49b1ac827c55cda32d2e36a9b7d16ae9fc9939cd2f35cf14870e93a7b
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0 (2014-08-12)
4
+
5
+ - Initial release.
data/LICENSE.md ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2014 Cameron Desautels & Taylor Fausak
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # [Stoplight][1]
2
+
3
+ [![Gem version][2]][3]
4
+ [![Build status][4]][5]
5
+ [![Coverage status][6]][7]
6
+ [![Quality status][8]][9]
7
+ [![Dependency status][10]][11]
8
+
9
+ Traffic control for code. An implementation of the circuit breaker pattern in Ruby.
10
+
11
+ ## Installation
12
+
13
+ Add it to your Gemfile:
14
+
15
+ ``` rb
16
+ gem 'stoplight', '~> 0.1.0'
17
+ ```
18
+
19
+ Or install it manually:
20
+
21
+ ``` sh
22
+ $ gem install stoplight
23
+ ```
24
+
25
+ This project uses [Semantic Versioning][12].
26
+
27
+ ## Setup
28
+
29
+ Stoplight uses an in-memory data store out of the box.
30
+
31
+ ``` irb
32
+ >> require 'stoplight'
33
+ => true
34
+ >> Stoplight.data_store
35
+ => #<Stoplight::DataStore::Memory:...>
36
+ ```
37
+
38
+ If you want to use a persistent data store, you'll have to set it up. Currently
39
+ the only supported persistent data store is Redis. Make sure you have [the Redis
40
+ gem][13] installed before configuring Stoplight.
41
+
42
+ ``` irb
43
+ >> redis = Stoplight::DataStore::Redis.new(url: 'redis://127.0.0.1:6379/0')
44
+ => #<Stoplight::DataStore::Redis:...>
45
+ >> Stoplight.data_store(redis)
46
+ => #<Stoplight::DataStore::Redis:...>
47
+ ```
48
+
49
+ ### Rails
50
+
51
+ Stoplight is designed to work seamlessly with Rails. If you want to use the
52
+ in-memory data store, you don't need to do anything special. If you want to use
53
+ a persistent data store, you'll need to configure it. Create an initializer for
54
+ Stoplight:
55
+
56
+ ``` rb
57
+ # config/initializers/stoplight.rb
58
+ require 'stoplight'
59
+ Stoplight.data_store(Stoplight::DataStore::Redis.new(...))
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ To get started, create a stoplight:
65
+
66
+ ``` irb
67
+ >> light = Stoplight::Light.new('example-1') { 22.0 / 7 }
68
+ => #<Stoplight::Light:...>
69
+ ```
70
+
71
+ Then you can run it and it will return the result of calling the block. This is
72
+ the "green" state.
73
+
74
+ ``` irb
75
+ >> light.run
76
+ => 3.142857142857143
77
+ >> light.green?
78
+ => true
79
+ ```
80
+
81
+ If everything goes well, you shouldn't even be able to tell that you're using a
82
+ stoplight. That's not very interesting though. Let's create a failing stoplight:
83
+
84
+ ``` irb
85
+ >> light = Stoplight::Light.new('example-2') { 1 / 0 }
86
+ => #<Stoplight::Light:...>
87
+ ```
88
+
89
+ Now when you run it, the error will be recorded and passed through. After
90
+ running it a few times, the stoplight will stop trying and fail fast. This is
91
+ the "red" state.
92
+
93
+ ``` irb
94
+ >> light.run
95
+ ZeroDivisionError: divided by 0
96
+ >> light.run
97
+ ZeroDivisionError: divided by 0
98
+ >> light.run
99
+ ZeroDivisionError: divided by 0
100
+ >> light.run
101
+ Stoplight::Error::NoFallback: Stoplight::Error::NoFallback
102
+ >> light.red?
103
+ => true
104
+ ```
105
+
106
+ ### Custom errors
107
+
108
+ Some errors shouldn't cause your stoplight to move into the red state. Usually
109
+ these are handled elsewhere in your stack and don't represent real failures. A
110
+ good example is `ActiveRecord::RecordNotFound`.
111
+
112
+ ``` irb
113
+ >> light = Stoplight::Light.new('example-3') { User.find(123) }.
114
+ ?> with_allowed_errors([ActiveRecord::RecordNotFound])
115
+ => #<Stoplight::Light:...>
116
+ >> light.run
117
+ ActiveRecord::RecordNotFound: Couldn't find User with ID=123
118
+ >> light.run
119
+ ActiveRecord::RecordNotFound: Couldn't find User with ID=123
120
+ >> light.run
121
+ ActiveRecord::RecordNotFound: Couldn't find User with ID=123
122
+ >> light.green?
123
+ => true
124
+ ```
125
+
126
+ ### Custom fallback
127
+
128
+ Instead of raising a `Stoplight::Error::NoFallback` error when in the red state,
129
+ you can provide a block to be run. This is useful when there's a good default
130
+ value for the block.
131
+
132
+ ``` irb
133
+ >> light = Stoplight::Light.new('example-4') { fail }.
134
+ ?> with_fallback { [] }
135
+ => #<Stoplight::Light:...>
136
+ >> light.run
137
+ RuntimeError:
138
+ >> light.run
139
+ RuntimeError:
140
+ >> light.run
141
+ RuntimeError:
142
+ >> light.run
143
+ => []
144
+ ```
145
+
146
+ ### Custom threshold
147
+
148
+ Some bits of code might be allowed to fail more or less frequently than others.
149
+ You can configure this by setting a custom threshold in seconds.
150
+
151
+ ``` irb
152
+ >> light = Stoplight::Light.new('example-5') { fail }.
153
+ ?> with_threshold(1)
154
+ => #<Stoplight::Light:...>
155
+ >> light.run
156
+ RuntimeError:
157
+ >> light.run
158
+ Stoplight::Error::NoFallback: Stoplight::Error::NoFallback
159
+ ```
160
+
161
+ ### Rails
162
+
163
+ Stoplight was designed to wrap Rails actions with minimal effort. Here's an
164
+ example configuration:
165
+
166
+ ``` rb
167
+ class ApplicationController < ActionController::Base
168
+ around_action :stoplight
169
+ private
170
+ def stoplight(&block)
171
+ Stoplight::Light.new("#{params[:controller]}##{params[:action]}", &block)
172
+ .with_allowed_errors([ActiveRecord::RecordNotFound])
173
+ .with_fallback { render(nothing: true, status: :service_unavailable) }
174
+ .run
175
+ end
176
+ end
177
+ ```
178
+
179
+ ## Credits
180
+
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.
183
+
184
+ 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].
187
+
188
+ [1]: https://github.com/orgsync/stoplight
189
+ [2]: https://badge.fury.io/rb/stoplight.svg
190
+ [3]: https://rubygems.org/gems/stoplight
191
+ [4]: https://travis-ci.org/orgsync/stoplight.svg
192
+ [5]: https://travis-ci.org/orgsync/stoplight
193
+ [6]: https://img.shields.io/coveralls/orgsync/stoplight.svg
194
+ [7]: https://coveralls.io/r/orgsync/stoplight
195
+ [8]: https://codeclimate.com/github/orgsync/stoplight/badges/gpa.svg
196
+ [9]: https://codeclimate.com/github/orgsync/stoplight
197
+ [10]: https://gemnasium.com/orgsync/stoplight.svg
198
+ [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
data/lib/stoplight.rb ADDED
@@ -0,0 +1,65 @@
1
+ # coding: utf-8
2
+
3
+ require 'forwardable'
4
+ require 'stoplight/data_store'
5
+ require 'stoplight/data_store/base'
6
+ require 'stoplight/data_store/memory'
7
+ require 'stoplight/data_store/redis'
8
+ require 'stoplight/error'
9
+ require 'stoplight/failure'
10
+ require 'stoplight/light'
11
+
12
+ module Stoplight
13
+ # @return [Gem::Version]
14
+ VERSION = Gem::Version.new('0.1.0')
15
+
16
+ class << self
17
+ extend Forwardable
18
+
19
+ def_delegators :data_store, *%w(
20
+ attempts
21
+ clear_attempts
22
+ clear_failures
23
+ failures
24
+ names
25
+ record_attempt
26
+ record_failure
27
+ set_state
28
+ set_threshold
29
+ state
30
+ )
31
+
32
+ # @param data_store [DataStore::Base]
33
+ # @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
39
+
40
+ # @param name [String]
41
+ # @return [Boolean]
42
+ def green?(name)
43
+ case data_store.state(name)
44
+ when DataStore::STATE_LOCKED_GREEN
45
+ true
46
+ when DataStore::STATE_LOCKED_RED
47
+ false
48
+ else
49
+ data_store.failures(name).size < threshold(name)
50
+ end
51
+ end
52
+
53
+ # @param name [String]
54
+ # @return (see .green?)
55
+ def red?(name)
56
+ !green?(name)
57
+ end
58
+
59
+ # @param name [String]
60
+ # @return [Integer]
61
+ def threshold(name)
62
+ data_store.threshold(name) || Light::DEFAULT_THRESHOLD
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module DataStore
5
+ # @return [String]
6
+ KEY_PREFIX = 'stoplight'
7
+
8
+ # @return [Set<String>]
9
+ STATES = Set.new([
10
+ STATE_LOCKED_GREEN = 'locked_green',
11
+ STATE_LOCKED_RED = 'locked_red',
12
+ STATE_UNLOCKED = 'unlocked'
13
+ ]).freeze
14
+ end
15
+ end
@@ -0,0 +1,96 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module DataStore
5
+ class Base
6
+ # @return [Array<String>]
7
+ def names
8
+ fail NotImplementedError
9
+ end
10
+
11
+ # @param _name [String]
12
+ # @param _error [Exception]
13
+ def record_failure(_name, _error)
14
+ fail NotImplementedError
15
+ end
16
+
17
+ # @param _name [String]
18
+ def clear_failures(_name)
19
+ fail NotImplementedError
20
+ end
21
+
22
+ # @param _name [String]
23
+ # @return [Array<Failure>]
24
+ def failures(_name)
25
+ fail NotImplementedError
26
+ end
27
+
28
+ # @param _name [String]
29
+ # @return [Integer]
30
+ def threshold(_name)
31
+ fail NotImplementedError
32
+ end
33
+
34
+ # @param _name [String]
35
+ # @param _threshold [Integer]
36
+ # @return (see #threshold)
37
+ def set_threshold(_name, _threshold)
38
+ fail NotImplementedError
39
+ end
40
+
41
+ # @param _name [String]
42
+ # @return (see #attempts)
43
+ def record_attempt(_name)
44
+ fail NotImplementedError
45
+ end
46
+
47
+ # @param _name [String]
48
+ def clear_attempts(_name)
49
+ fail NotImplementedError
50
+ end
51
+
52
+ # @param _name [String]
53
+ # @return [Integer]
54
+ def attempts(_name)
55
+ fail NotImplementedError
56
+ end
57
+
58
+ # @param _name [String]
59
+ # @return [String]
60
+ def state(_name)
61
+ fail NotImplementedError
62
+ end
63
+
64
+ # @param _name [String]
65
+ # @param _state [String]
66
+ # @return [String]
67
+ def set_state(_name, _state)
68
+ # REVIEW: Should we clear failures here?
69
+ fail NotImplementedError
70
+ end
71
+
72
+ private
73
+
74
+ def validate_state!(state)
75
+ return if DataStore::STATES.include?(state)
76
+ fail ArgumentError, 'Invalid state'
77
+ end
78
+
79
+ def key(name, slug)
80
+ [DataStore::KEY_PREFIX, name, slug].join(':')
81
+ end
82
+
83
+ def attempt_key(name)
84
+ key(name, 'attempts')
85
+ end
86
+
87
+ def failure_key(name)
88
+ key(name, 'failures')
89
+ end
90
+
91
+ def settings_key(name)
92
+ key(name, 'settings')
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,67 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module DataStore
5
+ class Memory < Base
6
+ def initialize
7
+ @data = {}
8
+ end
9
+
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
15
+ end
16
+
17
+ def record_failure(name, error)
18
+ failure = Failure.new(error)
19
+ array = @data[failure_key(name)] ||= []
20
+ array.push(failure)
21
+ end
22
+
23
+ def clear_failures(name)
24
+ @data.delete(failure_key(name))
25
+ end
26
+
27
+ def failures(name)
28
+ @data[failure_key(name)] || []
29
+ end
30
+
31
+ def settings(name)
32
+ @data[settings_key(name)] ||= {}
33
+ end
34
+
35
+ def threshold(name)
36
+ settings(name)['threshold']
37
+ end
38
+
39
+ def set_threshold(name, threshold)
40
+ settings(name)['threshold'] = threshold
41
+ end
42
+
43
+ def record_attempt(name)
44
+ key = attempt_key(name)
45
+ @data[key] ||= 0
46
+ @data[key] += 1
47
+ end
48
+
49
+ def clear_attempts(name)
50
+ @data.delete(attempt_key(name))
51
+ end
52
+
53
+ def attempts(name)
54
+ @data[attempt_key(name)] || 0
55
+ end
56
+
57
+ def state(name)
58
+ settings(name)['state'] || DataStore::STATE_UNLOCKED
59
+ end
60
+
61
+ def set_state(name, state)
62
+ validate_state!(state)
63
+ settings(name)['state'] = state
64
+ end
65
+ end
66
+ end
67
+ end