stoplight 0.3.1 → 0.4.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: 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