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 +4 -4
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +7 -0
- data/README.md +35 -18
- data/lib/stoplight.rb +3 -4
- data/lib/stoplight/data_store.rb +1 -2
- data/lib/stoplight/data_store/redis.rb +2 -0
- data/lib/stoplight/error.rb +24 -0
- data/lib/stoplight/light.rb +19 -5
- data/lib/stoplight/notifier/base.rb +4 -1
- data/lib/stoplight/notifier/hip_chat.rb +26 -5
- data/lib/stoplight/notifier/io.rb +23 -0
- data/spec/stoplight/data_store/redis_spec.rb +31 -1
- data/spec/stoplight/data_store_spec.rb +16 -0
- data/spec/stoplight/light_spec.rb +95 -11
- data/spec/stoplight/notifier/hip_chat_spec.rb +50 -10
- data/spec/stoplight/notifier/io_spec.rb +34 -0
- data/spec/stoplight_spec.rb +1 -1
- data/spec/support/data_store.rb +1 -1
- data/spec/support/fakeredis.rb +3 -0
- data/spec/support/hipchat.rb +3 -0
- metadata +42 -24
- data/lib/stoplight/mixin.rb +0 -9
- data/lib/stoplight/notifier/standard_error.rb +0 -17
- data/spec/stoplight/mixin_spec.rb +0 -35
- data/spec/stoplight/notifier/standard_error_spec.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 817cede2460b3309d9d6b2eed1e2eb5fd7c089e0
|
4
|
+
data.tar.gz: 7c5b0b24c68c0d4b63bdcf285dc97d0a67b2eedb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
- [
|
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.
|
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::
|
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::
|
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
|
-
##
|
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/
|
13
|
+
require 'stoplight/notifier/io'
|
15
14
|
|
16
15
|
module Stoplight
|
17
16
|
# @return [Gem::Version]
|
18
|
-
VERSION = Gem::Version.new('0.
|
17
|
+
VERSION = Gem::Version.new('0.4.0')
|
19
18
|
|
20
19
|
@data_store = DataStore::Memory.new
|
21
|
-
@notifiers = [Notifier::
|
20
|
+
@notifiers = [Notifier::IO.new($stderr)]
|
22
21
|
|
23
22
|
class << self
|
24
23
|
# @return [DataStore::Base]
|
data/lib/stoplight/data_store.rb
CHANGED
@@ -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]
|
data/lib/stoplight/error.rb
CHANGED
@@ -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
|
data/lib/stoplight/light.rb
CHANGED
@@ -24,7 +24,7 @@ module Stoplight
|
|
24
24
|
# @see #fallback
|
25
25
|
# @see #green?
|
26
26
|
def run
|
27
|
-
|
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(
|
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(
|
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(
|
143
|
-
Stoplight.notifiers.each
|
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
|
@@ -4,21 +4,42 @@ module Stoplight
|
|
4
4
|
module Notifier
|
5
5
|
# @note hipchat ~> 1.3.0
|
6
6
|
class HipChat < Base
|
7
|
-
|
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,
|
16
|
+
def initialize(client, room, formatter = nil, options = {})
|
14
17
|
@client = client
|
15
18
|
@room = room
|
16
|
-
@
|
19
|
+
@formatter = formatter || DEFAULT_FORMATTER
|
17
20
|
@options = DEFAULT_OPTIONS.merge(options)
|
18
21
|
end
|
19
22
|
|
20
|
-
def notify(
|
21
|
-
@
|
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,
|
6
|
+
subject(:notifier) { described_class.new(client, room, formatter, options) }
|
7
7
|
let(:client) { double }
|
8
8
|
let(:room) { SecureRandom.hex }
|
9
|
-
let(:
|
9
|
+
let(:formatter) { nil }
|
10
10
|
let(:options) { {} }
|
11
11
|
|
12
12
|
describe '#notify' do
|
13
|
-
subject(:result) { notifier.notify(
|
14
|
-
let(:
|
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
|
-
|
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
|
24
|
-
let(:
|
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
|
-
|
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
|
data/spec/stoplight_spec.rb
CHANGED
@@ -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::
|
49
|
+
expect(result.first).to be_a(Stoplight::Notifier::IO)
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'memoizes the result' do
|
data/spec/support/data_store.rb
CHANGED
@@ -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([]) }
|
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.
|
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
|
+
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
112
|
-
description:
|
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/
|
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/
|
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
|
-
|
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/
|
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:
|
data/lib/stoplight/mixin.rb
DELETED
@@ -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
|