stoplight 0.1.0 → 0.2.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 +14 -0
- data/README.md +73 -27
- data/lib/stoplight.rb +19 -8
- data/lib/stoplight/data_store/base.rb +22 -8
- data/lib/stoplight/data_store/memory.rb +54 -29
- data/lib/stoplight/data_store/redis.rb +46 -36
- data/lib/stoplight/error.rb +1 -3
- data/lib/stoplight/light.rb +14 -12
- data/lib/stoplight/mixin.rb +9 -0
- data/lib/stoplight/notifier.rb +6 -0
- data/lib/stoplight/notifier/base.rb +11 -0
- data/lib/stoplight/notifier/hip_chat.rb +27 -0
- data/lib/stoplight/notifier/standard_error.rb +11 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/stoplight/data_store/base_spec.rb +2 -0
- data/spec/stoplight/data_store/memory_spec.rb +1 -181
- data/spec/stoplight/data_store/redis_spec.rb +3 -182
- data/spec/stoplight/light_spec.rb +23 -3
- data/spec/stoplight/mixin_spec.rb +35 -0
- data/spec/stoplight/notifier/base_spec.rb +21 -0
- data/spec/stoplight/notifier/hip_chat_spec.rb +21 -0
- data/spec/stoplight/notifier/standard_error_spec.rb +18 -0
- data/spec/stoplight/notifier_spec.rb +6 -0
- data/spec/stoplight_spec.rb +33 -3
- data/spec/support/data_store.rb +178 -0
- metadata +23 -6
@@ -1,72 +1,82 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
begin
|
4
|
-
require 'redis'
|
5
|
-
REDIS_LOADED = true
|
6
|
-
rescue LoadError
|
7
|
-
REDIS_LOADED = false
|
8
|
-
end
|
9
|
-
|
10
3
|
module Stoplight
|
11
4
|
module DataStore
|
12
5
|
class Redis < Base
|
13
|
-
def initialize(
|
14
|
-
|
15
|
-
|
16
|
-
@redis = ::Redis.new(*args)
|
6
|
+
def initialize(redis)
|
7
|
+
@redis = redis
|
17
8
|
end
|
18
9
|
|
19
10
|
def names
|
20
|
-
@redis.
|
21
|
-
key[/^#{DataStore::KEY_PREFIX}:(.+):[^:]+$/o, 1]
|
22
|
-
end.uniq
|
11
|
+
@redis.hkeys(thresholds_key)
|
23
12
|
end
|
24
13
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
14
|
+
def purge
|
15
|
+
names
|
16
|
+
.select { |l| failures(l).empty? }
|
17
|
+
.each { |l| delete(l) }
|
29
18
|
end
|
30
19
|
|
31
|
-
def
|
32
|
-
@redis.
|
20
|
+
def delete(name)
|
21
|
+
@redis.pipelined do
|
22
|
+
clear_attempts(name)
|
23
|
+
clear_failures(name)
|
24
|
+
@redis.hdel(states_key, name)
|
25
|
+
@redis.hdel(thresholds_key, name)
|
26
|
+
end
|
33
27
|
end
|
34
28
|
|
35
|
-
|
36
|
-
|
29
|
+
# @group Attempts
|
30
|
+
|
31
|
+
def attempts(name)
|
32
|
+
@redis.hget(attempts_key, name).to_i
|
37
33
|
end
|
38
34
|
|
39
|
-
def
|
40
|
-
|
41
|
-
Integer(value) if value
|
35
|
+
def record_attempt(name)
|
36
|
+
@redis.hincrby(attempts_key, name, 1)
|
42
37
|
end
|
43
38
|
|
44
|
-
def
|
45
|
-
@redis.
|
46
|
-
threshold
|
39
|
+
def clear_attempts(name)
|
40
|
+
@redis.hdel(attempts_key, name)
|
47
41
|
end
|
48
42
|
|
49
|
-
|
50
|
-
|
43
|
+
# @group Failures
|
44
|
+
|
45
|
+
def failures(name)
|
46
|
+
@redis.lrange(failures_key(name), 0, -1)
|
51
47
|
end
|
52
48
|
|
53
|
-
def
|
54
|
-
@redis.
|
49
|
+
def record_failure(name, error)
|
50
|
+
@redis.rpush(failures_key(name), Failure.new(error).to_json)
|
55
51
|
end
|
56
52
|
|
57
|
-
def
|
58
|
-
@redis.
|
53
|
+
def clear_failures(name)
|
54
|
+
@redis.del(failures_key(name))
|
59
55
|
end
|
60
56
|
|
57
|
+
# @group State
|
58
|
+
|
61
59
|
def state(name)
|
62
|
-
@redis.hget(
|
60
|
+
@redis.hget(states_key, name) || STATE_UNLOCKED
|
63
61
|
end
|
64
62
|
|
65
63
|
def set_state(name, state)
|
66
64
|
validate_state!(state)
|
67
|
-
@redis.hset(
|
65
|
+
@redis.hset(states_key, name, state)
|
68
66
|
state
|
69
67
|
end
|
68
|
+
|
69
|
+
# @group Threshold
|
70
|
+
|
71
|
+
def threshold(name)
|
72
|
+
value = @redis.hget(thresholds_key, name)
|
73
|
+
Integer(value) if value
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_threshold(name, threshold)
|
77
|
+
@redis.hset(thresholds_key, name, threshold)
|
78
|
+
threshold
|
79
|
+
end
|
70
80
|
end
|
71
81
|
end
|
72
82
|
end
|
data/lib/stoplight/error.rb
CHANGED
data/lib/stoplight/light.rb
CHANGED
@@ -2,9 +2,6 @@
|
|
2
2
|
|
3
3
|
module Stoplight
|
4
4
|
class Light
|
5
|
-
# @return [Integer]
|
6
|
-
DEFAULT_THRESHOLD = 3
|
7
|
-
|
8
5
|
# @return [Array<Exception>]
|
9
6
|
attr_reader :allowed_errors
|
10
7
|
|
@@ -23,7 +20,7 @@ module Stoplight
|
|
23
20
|
end
|
24
21
|
|
25
22
|
# @return [Object]
|
26
|
-
# @raise [Error::
|
23
|
+
# @raise [Error::RedLight]
|
27
24
|
# @see #fallback
|
28
25
|
# @see #green?
|
29
26
|
def run
|
@@ -32,6 +29,11 @@ module Stoplight
|
|
32
29
|
if green?
|
33
30
|
run_code
|
34
31
|
else
|
32
|
+
if Stoplight.attempts(name).zero?
|
33
|
+
message = "Switching #{name} stoplight from green to red."
|
34
|
+
Stoplight.notifiers.each { |notifier| notifier.notify(message) }
|
35
|
+
end
|
36
|
+
|
35
37
|
run_fallback
|
36
38
|
end
|
37
39
|
end
|
@@ -55,17 +57,17 @@ module Stoplight
|
|
55
57
|
# @param threshold [Integer]
|
56
58
|
# @return [self]
|
57
59
|
def with_threshold(threshold)
|
58
|
-
Stoplight.
|
60
|
+
Stoplight.set_threshold(name, threshold.to_i)
|
59
61
|
self
|
60
62
|
end
|
61
63
|
|
62
64
|
# Attribute readers
|
63
65
|
|
64
66
|
# @return [Object]
|
65
|
-
# @raise [Error::
|
67
|
+
# @raise [Error::RedLight]
|
66
68
|
def fallback
|
67
69
|
return @fallback if defined?(@fallback)
|
68
|
-
fail Error::
|
70
|
+
fail Error::RedLight
|
69
71
|
end
|
70
72
|
|
71
73
|
# @return (see Stoplight.green?)
|
@@ -91,25 +93,25 @@ module Stoplight
|
|
91
93
|
|
92
94
|
def run_code
|
93
95
|
result = code.call
|
94
|
-
Stoplight.
|
96
|
+
Stoplight.clear_failures(name)
|
95
97
|
result
|
96
98
|
rescue => error
|
97
99
|
if error_allowed?(error)
|
98
|
-
Stoplight.
|
100
|
+
Stoplight.clear_failures(name)
|
99
101
|
else
|
100
|
-
Stoplight.
|
102
|
+
Stoplight.record_failure(name, error)
|
101
103
|
end
|
102
104
|
|
103
105
|
raise
|
104
106
|
end
|
105
107
|
|
106
108
|
def run_fallback
|
107
|
-
Stoplight.
|
109
|
+
Stoplight.record_attempt(name)
|
108
110
|
fallback.call
|
109
111
|
end
|
110
112
|
|
111
113
|
def sync_settings
|
112
|
-
Stoplight.
|
114
|
+
Stoplight.set_threshold(name, threshold)
|
113
115
|
end
|
114
116
|
end
|
115
117
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
module Notifier
|
5
|
+
# @note hipchat ~> 1.3.0
|
6
|
+
class HipChat < Base
|
7
|
+
# @param client [HipChat::Client]
|
8
|
+
# @param room [String]
|
9
|
+
# @param options [Hash]
|
10
|
+
def initialize(client, room, options = {})
|
11
|
+
@client = client
|
12
|
+
@room = room
|
13
|
+
@options = default_options.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def notify(message)
|
17
|
+
@client[@room].send('Stoplight', "@all #{message}", @options)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def default_options
|
23
|
+
{ color: 'red', notify: true }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,187 +3,7 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
describe Stoplight::DataStore::Memory do
|
6
|
-
let(:error) { error_klass.new }
|
7
|
-
let(:error_klass) { Class.new(StandardError) }
|
8
|
-
let(:name) { SecureRandom.hex }
|
9
|
-
let(:state) { Stoplight::DataStore::STATE_LOCKED_GREEN }
|
10
|
-
let(:threshold) { rand(10) }
|
11
|
-
|
12
6
|
subject(:data_store) { described_class.new }
|
13
7
|
|
14
|
-
|
15
|
-
subject(:result) { data_store.attempts(name) }
|
16
|
-
|
17
|
-
it 'is zero' do
|
18
|
-
expect(result).to eql(0)
|
19
|
-
end
|
20
|
-
|
21
|
-
context 'with an attempt' do
|
22
|
-
before { data_store.record_attempt(name) }
|
23
|
-
|
24
|
-
it 'is one' do
|
25
|
-
expect(result).to eql(1)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
describe '#clear_attempts' do
|
31
|
-
subject(:result) { data_store.clear_attempts(name) }
|
32
|
-
|
33
|
-
it 'returns nil' do
|
34
|
-
expect(result).to be_nil
|
35
|
-
end
|
36
|
-
|
37
|
-
context 'with an attempt' do
|
38
|
-
before { data_store.record_attempt(name) }
|
39
|
-
|
40
|
-
it 'returns one' do
|
41
|
-
expect(result).to eql(1)
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'clears attempts' do
|
45
|
-
result
|
46
|
-
expect(data_store.attempts(name)).to eql(0)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
describe '#clear_failures' do
|
52
|
-
subject(:result) { data_store.clear_failures(name) }
|
53
|
-
|
54
|
-
it 'returns nil' do
|
55
|
-
expect(result).to be nil
|
56
|
-
end
|
57
|
-
|
58
|
-
context 'with a failure' do
|
59
|
-
before { data_store.record_failure(name, error) }
|
60
|
-
|
61
|
-
it 'returns the failures' do
|
62
|
-
failures = data_store.failures(name)
|
63
|
-
expect(result).to eql(failures)
|
64
|
-
end
|
65
|
-
|
66
|
-
it 'clears the failures' do
|
67
|
-
result
|
68
|
-
expect(data_store.failures(name)).to be_empty
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe '#threshold' do
|
74
|
-
subject(:result) { data_store.threshold(name) }
|
75
|
-
|
76
|
-
it 'is nil' do
|
77
|
-
expect(result).to be nil
|
78
|
-
end
|
79
|
-
|
80
|
-
context 'with a threshold' do
|
81
|
-
before { data_store.set_threshold(name, threshold) }
|
82
|
-
|
83
|
-
it 'returns the threshold' do
|
84
|
-
expect(result).to eql(threshold)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
describe '#failures' do
|
90
|
-
subject(:result) { data_store.failures(name) }
|
91
|
-
|
92
|
-
it 'is an array' do
|
93
|
-
expect(result).to be_an(Array)
|
94
|
-
end
|
95
|
-
|
96
|
-
it 'is empty' do
|
97
|
-
expect(result).to be_empty
|
98
|
-
end
|
99
|
-
|
100
|
-
context 'with a failure' do
|
101
|
-
before { data_store.record_failure(name, error) }
|
102
|
-
|
103
|
-
it 'returns the failures' do
|
104
|
-
expect(result.size).to eql(1)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
describe '#names' do
|
110
|
-
subject(:result) { data_store.names }
|
111
|
-
|
112
|
-
it 'is an array' do
|
113
|
-
expect(result).to be_an(Array)
|
114
|
-
end
|
115
|
-
|
116
|
-
it 'is empty' do
|
117
|
-
expect(result).to be_empty
|
118
|
-
end
|
119
|
-
|
120
|
-
context 'with a name' do
|
121
|
-
before { data_store.settings(name) }
|
122
|
-
|
123
|
-
it 'includes the name' do
|
124
|
-
expect(result).to include(name)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
describe '#record_attempt' do
|
130
|
-
subject(:result) { data_store.record_attempt(name) }
|
131
|
-
|
132
|
-
it 'records the attempt' do
|
133
|
-
result
|
134
|
-
expect(data_store.attempts(name)).to eql(1)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
describe '#record_failure' do
|
139
|
-
subject(:result) { data_store.record_failure(name, error) }
|
140
|
-
|
141
|
-
it 'returns the failures' do
|
142
|
-
expect(result).to eql(data_store.failures(name))
|
143
|
-
end
|
144
|
-
|
145
|
-
it 'logs the failure' do
|
146
|
-
expect(result.size).to eql(1)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
describe '#set_threshold' do
|
151
|
-
subject(:result) { data_store.set_threshold(name, threshold) }
|
152
|
-
|
153
|
-
it 'returns the threshold' do
|
154
|
-
expect(result).to eql(threshold)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
describe '#set_state' do
|
159
|
-
subject(:result) { data_store.set_state(name, state) }
|
160
|
-
|
161
|
-
it 'returns the state' do
|
162
|
-
expect(result).to eql(state)
|
163
|
-
end
|
164
|
-
|
165
|
-
context 'with an invalid state' do
|
166
|
-
let(:state) { SecureRandom.hex }
|
167
|
-
|
168
|
-
it 'raises an error' do
|
169
|
-
expect { result }.to raise_error(ArgumentError)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
describe '#state' do
|
175
|
-
subject(:result) { data_store.state(name) }
|
176
|
-
|
177
|
-
it 'returns the default state' do
|
178
|
-
expect(result).to eql(Stoplight::DataStore::STATE_UNLOCKED)
|
179
|
-
end
|
180
|
-
|
181
|
-
context 'with a custom state' do
|
182
|
-
before { data_store.set_state(name, state) }
|
183
|
-
|
184
|
-
it 'returns the state' do
|
185
|
-
expect(result).to eql(state)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
8
|
+
it_behaves_like 'a data store'
|
189
9
|
end
|