stoplight 0.1.0

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