stoplight 0.3.1 → 0.4.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: e5bb148c31a3c94f65125fba1628f7399755c74d
4
- data.tar.gz: ed907f0fcfee16fbb9a760d863f47f0d89341d69
3
+ metadata.gz: 817cede2460b3309d9d6b2eed1e2eb5fd7c089e0
4
+ data.tar.gz: 7c5b0b24c68c0d4b63bdcf285dc97d0a67b2eedb
5
5
  SHA512:
6
- metadata.gz: 73b02ee787610aef6491804b789c449fc91ed49ba6c511d800e7119178b120dcc9f87341838eb0b28ce4acb880ee2d67d956cedce1883c1cd49b76feccb5c699
7
- data.tar.gz: 567b87080e6cd46487e09ea136319317aeebd6f0042f1b2e60aa5438d13f92cf99f2f42af76097609c35201903beac2c8d96f9ec912f5e7b220a25763ce8f2c1
6
+ metadata.gz: 128a58ba2d5667ca408a05696defb2cb8152a20c3f4f28e641d18e4ca91730b5209fb8f376fcff019989984fb49fdb75bdc9fbb24a8485e390e41b79799cbb37
7
+ data.tar.gz: b6766506606eda2a21886582ea626517a2f9d01d0b0967a63b323d67e58103167780ec91ff0ce018d9d341adaf07bca5261e8cc223a78dc06688391e58c1cf4c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.0 (2014-09-17)
4
+
5
+ - Made stoplights handle failing notifiers by logging the failure to standard
6
+ error.
7
+ - Made stoplights automatically fall back to a fresh in-memory data store if the
8
+ primary store is unavailable.
9
+ - Generalized `Stoplight::Notifier::StandardError` into
10
+ `Stoplight::Notifier::IO`.
11
+ - Changed notification format from a string to a lambda. It accepts the same
12
+ parameters that the format string accepted.
13
+ - Updated `Stoplight::Notifier::Base#notify` to accept three parameters (the
14
+ light, the before color, and the after color) instead of just one parameter
15
+ (the message).
16
+ - Prevented setting non-positive thresholds.
17
+ - Removed `Stoplight::Mixin`.
18
+
3
19
  ## v0.3.1 (2014-09-12)
4
20
 
5
21
  - Replaced `Stoplight::Failure#error` with `#error_class` and `#error_message`.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,7 @@
1
+ # Contributing
2
+
3
+ 1. **Fork** the repository.
4
+ 2. Create a **branch** for your feature (`git checkout -b feature`).
5
+ 3. **Commit** your changes (`git commit -a -m 'Feature'`).
6
+ 4. **Push** to your branch (`git push origin feature`).
7
+ 5. Create a **pull request**.
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![Quality status][8]][9]
7
7
  [![Dependency status][10]][11]
8
8
 
9
+ <img align="right" alt="Stoplight icon" src="https://i.imgur.com/tiuOfY9.png">
10
+
9
11
  Traffic control for code. An implementation of the circuit breaker pattern in
10
12
  Ruby.
11
13
 
@@ -16,13 +18,14 @@ Check out [stoplight-admin][12] for controlling your stoplights.
16
18
  - [Data store](#data-store)
17
19
  - [Notifiers](#notifiers)
18
20
  - [Rails](#rails)
19
- - [Usage](#usage)
20
- - [Mixin](#mixin)
21
+ - [Basic usage](#basic-usage)
21
22
  - [Custom errors](#custom-errors)
22
23
  - [Custom fallback](#custom-fallback)
23
24
  - [Custom threshold](#custom-threshold)
24
25
  - [Custom timeout](#custom-timeout)
25
26
  - [Rails](#rails-1)
27
+ - [Advanced usage](#advanced-usage)
28
+ - [Locking](#locking)
26
29
  - [Credits](#credits)
27
30
 
28
31
  ## Installation
@@ -30,7 +33,7 @@ Check out [stoplight-admin][12] for controlling your stoplights.
30
33
  Add it to your Gemfile:
31
34
 
32
35
  ``` rb
33
- gem 'stoplight', '~> 0.3.1'
36
+ gem 'stoplight', '~> 0.4.0'
34
37
  ```
35
38
 
36
39
  Or install it manually:
@@ -75,7 +78,7 @@ Stoplight sends notifications to standard error by default.
75
78
 
76
79
  ``` irb
77
80
  >> Stoplight.notifiers
78
- => [#<Stoplight::Notifier::StandardError:...>]
81
+ => [#<Stoplight::Notifier::IO:...>]
79
82
  ```
80
83
 
81
84
  If you want to send notifications elsewhere, you'll have to set them up.
@@ -90,7 +93,7 @@ HipChat gem][15] installed before configuring Stoplight.
90
93
  >> notifier = Stoplight::Notifier::HipChat.new(hipchat, 'room')
91
94
  => #<Stoplight::Notifier::HipChat:...>
92
95
  >> Stoplight.notifiers << notifier
93
- => [#<Stoplight::Notifier::StandardError:...>, #<Stoplight::Notifier::HipChat:...>]
96
+ => [#<Stoplight::Notifier::IO:...>, #<Stoplight::Notifier::HipChat:...>]
94
97
  ```
95
98
 
96
99
  ### Rails
@@ -107,7 +110,7 @@ Stoplight.data_store = Stoplight::DataStore::Redis.new(...)
107
110
  Stoplight.notifiers << Stoplight::Notifier::HipChat.new(...)
108
111
  ```
109
112
 
110
- ## Usage
113
+ ## Basic usage
111
114
 
112
115
  To get started, create a stoplight:
113
116
 
@@ -155,18 +158,6 @@ Stoplight::Error::RedLight: example-2
155
158
  When the stoplight changes from green to red, it will notify every configured
156
159
  notifier.
157
160
 
158
- ### Mixin
159
-
160
- Since creating and running a stoplight is so common, we provide a mixin that
161
- makes it easy.
162
-
163
- ``` irb
164
- >> include Stoplight::Mixin
165
- => Object
166
- >> stoplight('example-3') { 1.0 / 3 }
167
- => 0.3333333333333333
168
- ```
169
-
170
161
  ### Custom errors
171
162
 
172
163
  Some errors shouldn't cause your stoplight to move into the red state. Usually
@@ -269,6 +260,32 @@ class ApplicationController < ActionController::Base
269
260
  end
270
261
  ```
271
262
 
263
+ ## Advanced usage
264
+
265
+ ### Locking
266
+
267
+ Although stoplights can operate on their own, occasionally you may want to
268
+ override the default behavior. You can lock a light in either the green or red
269
+ state using `set_state`.
270
+
271
+ ``` irb
272
+ >> light = Stoplight::Light.new('example-8') { true }
273
+ => #<Stoplight::Light:...>
274
+ >> light.run
275
+ => true
276
+ >> Stoplight.data_store.set_state(
277
+ .. light.name, Stoplight::DataStore::STATE_LOCKED_RED)
278
+ => "locked_red"
279
+ >> light.run
280
+ Switching example-8 from green to red
281
+ Stoplight::Error::RedLight: example-8
282
+ ```
283
+
284
+ **Code in locked red lights may still run under certain conditions!** If you
285
+ have configured a custom data store and that data store fails, Stoplight will
286
+ switch over to using a blank in-memory data store. That means you will lose the
287
+ locked state of any stoplights.
288
+
272
289
  ## Credits
273
290
 
274
291
  Stoplight is brought to you by [@camdez][16] and [@tfausak][17] from
data/lib/stoplight.rb CHANGED
@@ -7,18 +7,17 @@ require 'stoplight/data_store/redis'
7
7
  require 'stoplight/error'
8
8
  require 'stoplight/failure'
9
9
  require 'stoplight/light'
10
- require 'stoplight/mixin'
11
10
  require 'stoplight/notifier'
12
11
  require 'stoplight/notifier/base'
13
12
  require 'stoplight/notifier/hip_chat'
14
- require 'stoplight/notifier/standard_error'
13
+ require 'stoplight/notifier/io'
15
14
 
16
15
  module Stoplight
17
16
  # @return [Gem::Version]
18
- VERSION = Gem::Version.new('0.3.1')
17
+ VERSION = Gem::Version.new('0.4.0')
19
18
 
20
19
  @data_store = DataStore::Memory.new
21
- @notifiers = [Notifier::StandardError.new]
20
+ @notifiers = [Notifier::IO.new($stderr)]
22
21
 
23
22
  class << self
24
23
  # @return [DataStore::Base]
@@ -41,7 +41,6 @@ module Stoplight
41
41
  case
42
42
  when state == STATE_LOCKED_GREEN then COLOR_GREEN
43
43
  when state == STATE_LOCKED_RED then COLOR_RED
44
- when threshold < 1 then COLOR_RED
45
44
  when failures.size < threshold then COLOR_GREEN
46
45
  when Time.now - failures.last.time > timeout then COLOR_YELLOW
47
46
  else COLOR_RED
@@ -99,7 +98,7 @@ module Stoplight
99
98
  # @param threshold [Integer]
100
99
  # @return [Boolean]
101
100
  def valid_threshold?(threshold)
102
- threshold.is_a?(Integer)
101
+ threshold.is_a?(Integer) && threshold > 0
103
102
  end
104
103
 
105
104
  # @param timeout [Integer]
@@ -38,6 +38,8 @@ module Stoplight
38
38
  threshold = normalize_threshold(threshold)
39
39
  @redis.hset(DataStore.thresholds_key, name, threshold)
40
40
  nil
41
+ rescue ::Redis::BaseError => error
42
+ raise Error::BadDataStore, error
41
43
  end
42
44
 
43
45
  def get_color(name)
@@ -22,5 +22,29 @@ module Stoplight
22
22
 
23
23
  # @return [Class]
24
24
  InvalidTimeout = Class.new(Base)
25
+
26
+ # @return [Class]
27
+ class BadDataStore < Base
28
+ # @return [Exception]
29
+ attr_reader :cause
30
+
31
+ # @param cause [Exception]
32
+ def initialize(cause)
33
+ super(cause.message)
34
+ @cause = cause
35
+ end
36
+ end
37
+
38
+ # @return [Class]
39
+ class BadNotifier < Base
40
+ # @return [Exception]
41
+ attr_reader :cause
42
+
43
+ # @param cause [Exception]
44
+ def initialize(cause)
45
+ super(cause.message)
46
+ @cause = cause
47
+ end
48
+ end
25
49
  end
26
50
  end
@@ -24,7 +24,7 @@ module Stoplight
24
24
  # @see #fallback
25
25
  # @see #green?
26
26
  def run
27
- Stoplight.data_store.sync(name)
27
+ sync
28
28
 
29
29
  case color
30
30
  when DataStore::COLOR_GREEN
@@ -117,12 +117,12 @@ module Stoplight
117
117
  end
118
118
 
119
119
  def run_yellow
120
- run_green.tap { notify("Switching #{name} from red to green.") }
120
+ run_green.tap { notify(DataStore::COLOR_RED, DataStore::COLOR_GREEN) }
121
121
  end
122
122
 
123
123
  def run_red
124
124
  if Stoplight.data_store.record_attempt(name) == 1
125
- notify("Switching #{name} from green to red.")
125
+ notify(DataStore::COLOR_GREEN, DataStore::COLOR_RED)
126
126
  end
127
127
  fallback.call
128
128
  end
@@ -139,8 +139,22 @@ module Stoplight
139
139
  allowed_errors.any? { |klass| error.is_a?(klass) }
140
140
  end
141
141
 
142
- def notify(message)
143
- Stoplight.notifiers.each { |notifier| notifier.notify(message) }
142
+ def notify(from_color, to_color)
143
+ Stoplight.notifiers.each do |notifier|
144
+ begin
145
+ notifier.notify(self, from_color, to_color)
146
+ rescue Error::BadNotifier => error
147
+ warn(error.cause)
148
+ end
149
+ end
150
+ end
151
+
152
+ def sync
153
+ Stoplight.data_store.sync(name)
154
+ rescue Error::BadDataStore => error
155
+ warn(error.cause)
156
+ Stoplight.data_store = Stoplight::DataStore::Memory.new
157
+ retry
144
158
  end
145
159
  end
146
160
  end
@@ -3,7 +3,10 @@
3
3
  module Stoplight
4
4
  module Notifier
5
5
  class Base
6
- def notify(_message)
6
+ # @param _light [Light]
7
+ # @param _from_color [String]
8
+ # @param _to_color [String]
9
+ def notify(_light, _from_color, _to_color)
7
10
  fail NotImplementedError
8
11
  end
9
12
  end
@@ -4,21 +4,42 @@ module Stoplight
4
4
  module Notifier
5
5
  # @note hipchat ~> 1.3.0
6
6
  class HipChat < Base
7
- DEFAULT_FORMAT = '@all %s'
7
+ DEFAULT_FORMATTER = lambda do |light, from_color, to_color|
8
+ "@all Switching #{light.name} from #{from_color} to #{to_color}"
9
+ end
8
10
  DEFAULT_OPTIONS = { color: 'red', message_format: 'text', notify: true }
9
11
 
10
12
  # @param client [HipChat::Client]
11
13
  # @param room [String]
14
+ # @param formatter [Proc, nil]
12
15
  # @param options [Hash]
13
- def initialize(client, room, format = nil, options = {})
16
+ def initialize(client, room, formatter = nil, options = {})
14
17
  @client = client
15
18
  @room = room
16
- @format = format || DEFAULT_FORMAT
19
+ @formatter = formatter || DEFAULT_FORMATTER
17
20
  @options = DEFAULT_OPTIONS.merge(options)
18
21
  end
19
22
 
20
- def notify(message)
21
- @client[@room].send('Stoplight', @format % message, @options)
23
+ def notify(light, from_color, to_color)
24
+ message = @formatter.call(light, from_color, to_color)
25
+ @client[@room].send('Stoplight', message, @options)
26
+ rescue *errors => error
27
+ raise Error::BadNotifier, error
28
+ end
29
+
30
+ private
31
+
32
+ def errors
33
+ [
34
+ ::HipChat::InvalidApiVersion,
35
+ ::HipChat::RoomMissingOwnerUserId,
36
+ ::HipChat::RoomNameTooLong,
37
+ ::HipChat::Unauthorized,
38
+ ::HipChat::UnknownResponseCode,
39
+ ::HipChat::UnknownRoom,
40
+ ::HipChat::UnknownUser,
41
+ ::HipChat::UsernameTooLong
42
+ ]
22
43
  end
23
44
  end
24
45
  end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Notifier
5
+ class IO < Base
6
+ DEFAULT_FORMATTER = lambda do |light, from_color, to_color|
7
+ "Switching #{light.name} from #{from_color} to #{to_color}"
8
+ end
9
+
10
+ # @param io [IO]
11
+ # @param formatter [Proc, nil]
12
+ def initialize(io, formatter = nil)
13
+ @io = io
14
+ @formatter = formatter || DEFAULT_FORMATTER
15
+ end
16
+
17
+ def notify(light, from_color, to_color)
18
+ message = @formatter.call(light, from_color, to_color)
19
+ @io.puts(message)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,11 +1,41 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require 'spec_helper'
4
- require 'fakeredis'
5
4
 
6
5
  describe Stoplight::DataStore::Redis do
7
6
  subject(:data_store) { described_class.new(redis) }
8
7
  let(:redis) { Redis.new }
9
8
 
10
9
  it_behaves_like 'a data store'
10
+
11
+ context 'with a failing connection' do
12
+ let(:name) { SecureRandom.hex }
13
+ let(:error) { Redis::BaseConnectionError.new(message) }
14
+ let(:message) { SecureRandom.hex }
15
+
16
+ before { allow(redis).to receive(:hget).and_raise(error) }
17
+
18
+ it 'reraises the error' do
19
+ expect { data_store.sync(name) }
20
+ .to raise_error(Stoplight::Error::BadDataStore)
21
+ end
22
+
23
+ it 'sets the message' do
24
+ begin
25
+ data_store.sync(name)
26
+ expect(false).to be(true)
27
+ rescue Stoplight::Error::BadDataStore => e
28
+ expect(e.message).to eql(message)
29
+ end
30
+ end
31
+
32
+ it 'sets the cause' do
33
+ begin
34
+ data_store.sync(name)
35
+ expect(false).to be(true)
36
+ rescue Stoplight::Error::BadDataStore => e
37
+ expect(e.cause).to eql(error)
38
+ end
39
+ end
40
+ end
11
41
  end
@@ -45,6 +45,22 @@ describe Stoplight::DataStore do
45
45
  expect { result }.to raise_error(Stoplight::Error::InvalidThreshold)
46
46
  end
47
47
  end
48
+
49
+ context 'with a negative threshold' do
50
+ let(:threshold) { -1 }
51
+
52
+ it 'raises an error' do
53
+ expect { result }.to raise_error(Stoplight::Error::InvalidThreshold)
54
+ end
55
+ end
56
+
57
+ context 'with a zero threshold' do
58
+ let(:threshold) { 0 }
59
+
60
+ it 'raises an error' do
61
+ expect { result }.to raise_error(Stoplight::Error::InvalidThreshold)
62
+ end
63
+ end
48
64
  end
49
65
 
50
66
  describe '.validate_timeout!' do
@@ -6,7 +6,7 @@ require 'spec_helper'
6
6
  describe Stoplight::Light do
7
7
  before do
8
8
  @notifiers = Stoplight.notifiers
9
- Stoplight.notifiers = []
9
+ Stoplight.notifiers = [Stoplight::Notifier::IO.new(StringIO.new)]
10
10
  end
11
11
  after { Stoplight.notifiers = @notifiers }
12
12
 
@@ -20,7 +20,7 @@ describe Stoplight::Light do
20
20
  let(:fallback) { -> { fallback_result } }
21
21
  let(:message) { SecureRandom.hex }
22
22
  let(:name) { SecureRandom.hex }
23
- let(:threshold) { rand(100) }
23
+ let(:threshold) { 1 + rand(100) }
24
24
  let(:timeout) { rand(100) }
25
25
 
26
26
  it { expect(light.run).to eql(code_result) }
@@ -97,15 +97,6 @@ describe Stoplight::Light do
97
97
  end
98
98
  end
99
99
 
100
- context 'with threshold' do
101
- before { light.with_threshold(0) }
102
-
103
- it 'stays red' do
104
- expect(light.red?).to eql(true)
105
- expect { light.run }.to raise_error(Stoplight::Error::RedLight)
106
- end
107
- end
108
-
109
100
  context 'with timeout' do
110
101
  before { light.with_timeout(-1) }
111
102
 
@@ -119,4 +110,97 @@ describe Stoplight::Light do
119
110
  end
120
111
  end
121
112
  end
113
+
114
+ context 'with Redis' do
115
+ let(:data_store) { Stoplight::DataStore::Redis.new(redis) }
116
+ let(:redis) { Redis.new }
117
+
118
+ before do
119
+ @data_store = Stoplight.data_store
120
+ Stoplight.data_store = data_store
121
+ end
122
+ after { Stoplight.data_store = @data_store }
123
+
124
+ context 'with a failing connection' do
125
+ let(:error) { Stoplight::Error::BadDataStore.new(cause) }
126
+ let(:cause) { Redis::BaseConnectionError.new(message) }
127
+ let(:message) { SecureRandom.hex }
128
+
129
+ before { allow(data_store).to receive(:sync).and_raise(error) }
130
+
131
+ before { @stderr, $stderr = $stderr, StringIO.new }
132
+ after { $stderr = @stderr }
133
+
134
+ it 'does not raise an error' do
135
+ expect { light.run }.to_not raise_error
136
+ end
137
+
138
+ it 'switches to an in-memory data store' do
139
+ light.run
140
+ expect(Stoplight.data_store).to_not eql(data_store)
141
+ expect(Stoplight.data_store).to be_a(Stoplight::DataStore::Memory)
142
+ end
143
+
144
+ it 'syncs the light in the new data store' do
145
+ expect_any_instance_of(Stoplight::DataStore::Memory)
146
+ .to receive(:sync).with(light.name)
147
+ light.run
148
+ end
149
+
150
+ it 'warns to STDERR' do
151
+ light.run
152
+ expect($stderr.string).to eql("#{cause}\n")
153
+ end
154
+ end
155
+ end
156
+
157
+ context 'with HipChat' do
158
+ let(:notifier) { Stoplight::Notifier::HipChat.new(client, room_name) }
159
+ let(:client) { double(HipChat::Client) }
160
+ let(:room_name) { SecureRandom.hex }
161
+ let(:room) { double(HipChat::Room) }
162
+
163
+ before do
164
+ @notifiers = Stoplight.notifiers
165
+ Stoplight.notifiers = [notifier]
166
+ allow(client).to receive(:[]).with(room_name).and_return(room)
167
+ end
168
+
169
+ after { Stoplight.notifiers = @notifiers }
170
+
171
+ context 'with a failing client' do
172
+ subject(:result) do
173
+ begin
174
+ light.run
175
+ rescue Stoplight::Error::RedLight
176
+ nil
177
+ end
178
+ end
179
+
180
+ let(:error_class) { HipChat::Unauthorized }
181
+
182
+ before do
183
+ Stoplight.data_store.set_state(
184
+ light.name, Stoplight::DataStore::STATE_LOCKED_RED)
185
+ allow(room).to receive(:send).with(
186
+ 'Stoplight',
187
+ /\A@all /,
188
+ hash_including(color: 'red')
189
+ ).and_raise(error)
190
+ @stderr = $stderr
191
+ $stderr = StringIO.new
192
+ end
193
+
194
+ after { $stderr = @stderr }
195
+
196
+ it 'does not raise an error' do
197
+ expect { result }.to_not raise_error
198
+ end
199
+
200
+ it 'warns to STDERR' do
201
+ result
202
+ expect($stderr.string).to eql("#{error}\n")
203
+ end
204
+ end
205
+ end
122
206
  end
@@ -3,32 +3,72 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Stoplight::Notifier::HipChat do
6
- subject(:notifier) { described_class.new(client, room, format, options) }
6
+ subject(:notifier) { described_class.new(client, room, formatter, options) }
7
7
  let(:client) { double }
8
8
  let(:room) { SecureRandom.hex }
9
- let(:format) { nil }
9
+ let(:formatter) { nil }
10
10
  let(:options) { {} }
11
11
 
12
12
  describe '#notify' do
13
- subject(:result) { notifier.notify(message) }
14
- let(:message) { SecureRandom.hex }
13
+ subject(:result) { notifier.notify(light, from_color, to_color) }
14
+ let(:light) { Stoplight::Light.new(light_name, &light_code) }
15
+ let(:light_name) { SecureRandom.hex }
16
+ let(:light_code) { -> {} }
17
+ let(:from_color) { Stoplight::DataStore::COLOR_GREEN }
18
+ let(:to_color) { Stoplight::DataStore::COLOR_RED }
15
19
 
16
20
  it 'sends the message to HipChat' do
17
21
  expect(client).to receive(:[]).with(room).and_return(client)
18
- expect(client).to receive(:send)
19
- .with('Stoplight', "@all #{message}", anything)
22
+ expect(client).to receive(:send).with(
23
+ 'Stoplight',
24
+ "@all Switching #{light.name} from #{from_color} to #{to_color}",
25
+ anything)
20
26
  result
21
27
  end
22
28
 
23
- context 'with a format' do
24
- let(:format) { '> %s <' }
29
+ context 'with a formatter' do
30
+ let(:formatter) { ->(l, f, t) { "#{l.name} #{f} #{t}" } }
25
31
 
26
32
  it 'formats the message' do
27
33
  expect(client).to receive(:[]).with(room).and_return(client)
28
- expect(client).to receive(:send)
29
- .with('Stoplight', "> #{message} <", anything)
34
+ expect(client).to receive(:send).with(
35
+ 'Stoplight',
36
+ "#{light.name} #{from_color} #{to_color}",
37
+ anything)
30
38
  result
31
39
  end
32
40
  end
41
+
42
+ context 'failing' do
43
+ let(:error) { HipChat::UnknownResponseCode.new(message) }
44
+ let(:message) { SecureRandom.hex }
45
+
46
+ before do
47
+ allow(client).to receive(:[]).with(room).and_return(client)
48
+ allow(client).to receive(:send).and_raise(error)
49
+ end
50
+
51
+ it 'reraises the error' do
52
+ expect { result }.to raise_error(Stoplight::Error::BadNotifier)
53
+ end
54
+
55
+ it 'sets the message' do
56
+ begin
57
+ result
58
+ expect(false).to be(true)
59
+ rescue Stoplight::Error::BadNotifier => e
60
+ expect(e.message).to eql(message)
61
+ end
62
+ end
63
+
64
+ it 'sets the cause' do
65
+ begin
66
+ result
67
+ expect(false).to be(true)
68
+ rescue Stoplight::Error::BadNotifier => e
69
+ expect(e.cause).to eql(error)
70
+ end
71
+ end
72
+ end
33
73
  end
34
74
  end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Stoplight::Notifier::IO do
6
+ subject(:notifier) { described_class.new(io, formatter) }
7
+ let(:io) { StringIO.new }
8
+ let(:formatter) { nil }
9
+
10
+ describe '#notify' do
11
+ subject(:result) { notifier.notify(light, from_color, to_color) }
12
+ let(:light) { Stoplight::Light.new(light_name, &light_code) }
13
+ let(:light_name) { SecureRandom.hex }
14
+ let(:light_code) { -> {} }
15
+ let(:from_color) { Stoplight::DataStore::COLOR_GREEN }
16
+ let(:to_color) { Stoplight::DataStore::COLOR_RED }
17
+
18
+ it 'emits the message as a warning' do
19
+ result
20
+ expect(io.string)
21
+ .to eql("Switching #{light.name} from #{from_color} to #{to_color}\n")
22
+ end
23
+
24
+ context 'with a formatter' do
25
+ let(:formatter) { ->(l, f, t) { "#{l.name} #{f} #{t}" } }
26
+
27
+ it 'formats the message' do
28
+ result
29
+ expect(io.string)
30
+ .to eql("#{light.name} #{from_color} #{to_color}\n")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -46,7 +46,7 @@ describe Stoplight do
46
46
  it 'uses the default notifier' do
47
47
  expect(result).to be_an(Array)
48
48
  expect(result.size).to eql(1)
49
- expect(result.first).to be_a(Stoplight::Notifier::StandardError)
49
+ expect(result.first).to be_a(Stoplight::Notifier::IO)
50
50
  end
51
51
 
52
52
  it 'memoizes the result' do
@@ -8,7 +8,7 @@ shared_examples_for 'a data store' do
8
8
  let(:error_class) { Class.new(StandardError) }
9
9
  let(:time) { Time.now }
10
10
  let(:state) { Stoplight::DataStore::STATES.to_a.sample }
11
- let(:threshold) { rand(100) }
11
+ let(:threshold) { 1 + rand(100) }
12
12
  let(:timeout) { rand(100) }
13
13
 
14
14
  it { expect(data_store.names).to eql([]) }
@@ -0,0 +1,3 @@
1
+ # coding: utf-8
2
+
3
+ require 'fakeredis/rspec'
@@ -0,0 +1,3 @@
1
+ # coding: utf-8
2
+
3
+ require 'hipchat'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stoplight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Desautels
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-09-12 00:00:00.000000000 Z
12
+ date: 2014-09-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: benchmark-ips
@@ -17,99 +17,115 @@ dependencies:
17
17
  requirements:
18
18
  - - ~>
19
19
  - !ruby/object:Gem::Version
20
- version: 2.0.0
20
+ version: '2.0'
21
21
  type: :development
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ~>
26
26
  - !ruby/object:Gem::Version
27
- version: 2.0.0
27
+ version: '2.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: coveralls
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ~>
33
33
  - !ruby/object:Gem::Version
34
- version: 0.7.1
34
+ version: '0.7'
35
35
  type: :development
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - ~>
40
40
  - !ruby/object:Gem::Version
41
- version: 0.7.1
41
+ version: '0.7'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: fakeredis
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - ~>
47
47
  - !ruby/object:Gem::Version
48
- version: 0.5.0
48
+ version: '0.5'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - ~>
54
54
  - !ruby/object:Gem::Version
55
- version: 0.5.0
55
+ version: '0.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: hipchat
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '1.3'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.3'
56
70
  - !ruby/object:Gem::Dependency
57
71
  name: rake
58
72
  requirement: !ruby/object:Gem::Requirement
59
73
  requirements:
60
74
  - - ~>
61
75
  - !ruby/object:Gem::Version
62
- version: 10.3.2
76
+ version: '10.3'
63
77
  type: :development
64
78
  prerelease: false
65
79
  version_requirements: !ruby/object:Gem::Requirement
66
80
  requirements:
67
81
  - - ~>
68
82
  - !ruby/object:Gem::Version
69
- version: 10.3.2
83
+ version: '10.3'
70
84
  - !ruby/object:Gem::Dependency
71
85
  name: rspec
72
86
  requirement: !ruby/object:Gem::Requirement
73
87
  requirements:
74
88
  - - ~>
75
89
  - !ruby/object:Gem::Version
76
- version: 3.1.0
90
+ version: '3.1'
77
91
  type: :development
78
92
  prerelease: false
79
93
  version_requirements: !ruby/object:Gem::Requirement
80
94
  requirements:
81
95
  - - ~>
82
96
  - !ruby/object:Gem::Version
83
- version: 3.1.0
97
+ version: '3.1'
84
98
  - !ruby/object:Gem::Dependency
85
99
  name: rubocop
86
100
  requirement: !ruby/object:Gem::Requirement
87
101
  requirements:
88
102
  - - ~>
89
103
  - !ruby/object:Gem::Version
90
- version: 0.26.0
104
+ version: '0.26'
91
105
  type: :development
92
106
  prerelease: false
93
107
  version_requirements: !ruby/object:Gem::Requirement
94
108
  requirements:
95
109
  - - ~>
96
110
  - !ruby/object:Gem::Version
97
- version: 0.26.0
111
+ version: '0.26'
98
112
  - !ruby/object:Gem::Dependency
99
113
  name: yard
100
114
  requirement: !ruby/object:Gem::Requirement
101
115
  requirements:
102
116
  - - ~>
103
117
  - !ruby/object:Gem::Version
104
- version: 0.8.7.4
118
+ version: '0.8'
105
119
  type: :development
106
120
  prerelease: false
107
121
  version_requirements: !ruby/object:Gem::Requirement
108
122
  requirements:
109
123
  - - ~>
110
124
  - !ruby/object:Gem::Version
111
- version: 0.8.7.4
112
- description: Traffic control for code.
125
+ version: '0.8'
126
+ description: |
127
+ Traffic control for code. An implementation of the circuit breaker pattern
128
+ in Ruby.
113
129
  email:
114
130
  - camdez@gmail.com
115
131
  - taylor@fausak.me
@@ -118,6 +134,7 @@ extensions: []
118
134
  extra_rdoc_files: []
119
135
  files:
120
136
  - CHANGELOG.md
137
+ - CONTRIBUTING.md
121
138
  - LICENSE.md
122
139
  - README.md
123
140
  - lib/stoplight.rb
@@ -128,11 +145,10 @@ files:
128
145
  - lib/stoplight/error.rb
129
146
  - lib/stoplight/failure.rb
130
147
  - lib/stoplight/light.rb
131
- - lib/stoplight/mixin.rb
132
148
  - lib/stoplight/notifier.rb
133
149
  - lib/stoplight/notifier/base.rb
134
150
  - lib/stoplight/notifier/hip_chat.rb
135
- - lib/stoplight/notifier/standard_error.rb
151
+ - lib/stoplight/notifier/io.rb
136
152
  - spec/spec_helper.rb
137
153
  - spec/stoplight/data_store/base_spec.rb
138
154
  - spec/stoplight/data_store/memory_spec.rb
@@ -140,14 +156,15 @@ files:
140
156
  - spec/stoplight/data_store_spec.rb
141
157
  - spec/stoplight/failure_spec.rb
142
158
  - spec/stoplight/light_spec.rb
143
- - spec/stoplight/mixin_spec.rb
144
159
  - spec/stoplight/notifier/base_spec.rb
145
160
  - spec/stoplight/notifier/hip_chat_spec.rb
146
- - spec/stoplight/notifier/standard_error_spec.rb
161
+ - spec/stoplight/notifier/io_spec.rb
147
162
  - spec/stoplight/notifier_spec.rb
148
163
  - spec/stoplight_spec.rb
149
164
  - spec/support/data_store.rb
150
- homepage: https://github.com/orgsync/stoplight
165
+ - spec/support/fakeredis.rb
166
+ - spec/support/hipchat.rb
167
+ homepage: http://orgsync.github.io/stoplight
151
168
  licenses:
152
169
  - MIT
153
170
  metadata: {}
@@ -179,11 +196,12 @@ test_files:
179
196
  - spec/stoplight/data_store_spec.rb
180
197
  - spec/stoplight/failure_spec.rb
181
198
  - spec/stoplight/light_spec.rb
182
- - spec/stoplight/mixin_spec.rb
183
199
  - spec/stoplight/notifier/base_spec.rb
184
200
  - spec/stoplight/notifier/hip_chat_spec.rb
185
- - spec/stoplight/notifier/standard_error_spec.rb
201
+ - spec/stoplight/notifier/io_spec.rb
186
202
  - spec/stoplight/notifier_spec.rb
187
203
  - spec/stoplight_spec.rb
188
204
  - spec/support/data_store.rb
205
+ - spec/support/fakeredis.rb
206
+ - spec/support/hipchat.rb
189
207
  has_rdoc:
@@ -1,9 +0,0 @@
1
- # coding: utf-8
2
-
3
- module Stoplight
4
- module Mixin
5
- def stoplight(name, &block)
6
- Stoplight::Light.new(name, &block).run
7
- end
8
- end
9
- end
@@ -1,17 +0,0 @@
1
- # coding: utf-8
2
-
3
- module Stoplight
4
- module Notifier
5
- class StandardError < Base
6
- DEFAULT_FORMAT = '%s'
7
-
8
- def initialize(format = nil)
9
- @format = format || DEFAULT_FORMAT
10
- end
11
-
12
- def notify(message)
13
- warn(@format % message)
14
- end
15
- end
16
- end
17
- end
@@ -1,35 +0,0 @@
1
- # coding: utf-8
2
-
3
- require 'spec_helper'
4
-
5
- describe Stoplight::Mixin do
6
- subject(:klass) { Class.new.extend(described_class) }
7
-
8
- describe '#stoplight' do
9
- subject(:result) { klass.stoplight(name, &code) }
10
- let(:name) { SecureRandom.hex }
11
- let(:code) { proc { code_result } }
12
- let(:code_result) { double }
13
-
14
- let(:light) { double }
15
-
16
- before do
17
- allow(Stoplight::Light).to receive(:new).and_return(light)
18
- allow(light).to receive(:run).and_return(code.call)
19
- end
20
-
21
- it 'calls .new' do
22
- expect(Stoplight::Light).to receive(:new)
23
- result
24
- end
25
-
26
- it 'calls #run' do
27
- expect(light).to receive(:run)
28
- result
29
- end
30
-
31
- it 'returns the result of #run' do
32
- expect(result).to eql(code_result)
33
- end
34
- end
35
- end
@@ -1,30 +0,0 @@
1
- # coding: utf-8
2
-
3
- require 'spec_helper'
4
-
5
- describe Stoplight::Notifier::StandardError do
6
- subject(:notifier) { described_class.new(format) }
7
- let(:format) { nil }
8
-
9
- before { @stderr, $stderr = $stderr, StringIO.new }
10
- after { $stderr = @stderr }
11
-
12
- describe '#notify' do
13
- subject(:result) { notifier.notify(message) }
14
- let(:message) { SecureRandom.hex }
15
-
16
- it 'emits the message as a warning' do
17
- result
18
- expect($stderr.string).to eql("#{message}\n")
19
- end
20
-
21
- context 'with a format' do
22
- let(:format) { '> %s <' }
23
-
24
- it 'formats the message' do
25
- result
26
- expect($stderr.string).to eql("> #{message} <\n")
27
- end
28
- end
29
- end
30
- end