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 +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
|