stoplight 0.2.1 → 0.3.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 +6 -0
- data/README.md +73 -24
- data/lib/stoplight.rb +1 -47
- data/lib/stoplight/data_store.rb +144 -6
- data/lib/stoplight/data_store/base.rb +88 -33
- data/lib/stoplight/data_store/memory.rb +80 -36
- data/lib/stoplight/data_store/redis.rb +120 -30
- data/lib/stoplight/error.rb +16 -0
- data/lib/stoplight/failure.rb +24 -11
- data/lib/stoplight/light.rb +66 -37
- data/lib/stoplight/notifier/hip_chat.rb +7 -9
- data/lib/stoplight/notifier/standard_error.rb +7 -1
- data/spec/stoplight/data_store/base_spec.rb +18 -8
- data/spec/stoplight/data_store_spec.rb +60 -0
- data/spec/stoplight/failure_spec.rb +35 -9
- data/spec/stoplight/light_spec.rb +86 -206
- data/spec/stoplight/notifier/hip_chat_spec.rb +14 -1
- data/spec/stoplight/notifier/standard_error_spec.rb +16 -4
- data/spec/stoplight_spec.rb +0 -86
- data/spec/support/data_store.rb +36 -170
- metadata +29 -13
@@ -8,84 +8,128 @@ module Stoplight
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def names
|
11
|
-
|
11
|
+
thresholds.keys
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
14
|
+
def clear_stale
|
15
15
|
names
|
16
|
-
.select { |
|
17
|
-
.each
|
16
|
+
.select { |name| get_failures(name).empty? }
|
17
|
+
.each { |name| clear(name) }
|
18
|
+
nil
|
18
19
|
end
|
19
20
|
|
20
|
-
def
|
21
|
+
def clear(name)
|
21
22
|
clear_attempts(name)
|
22
23
|
clear_failures(name)
|
23
|
-
|
24
|
-
|
24
|
+
clear_state(name)
|
25
|
+
clear_threshold(name)
|
26
|
+
clear_timeout(name)
|
25
27
|
end
|
26
28
|
|
27
|
-
|
29
|
+
def sync(name)
|
30
|
+
set_threshold(name, get_threshold(name))
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_color(name)
|
35
|
+
state = get_state(name)
|
36
|
+
threshold = get_threshold(name)
|
37
|
+
failures = get_failures(name)
|
38
|
+
timeout = get_timeout(name)
|
39
|
+
DataStore.colorize(state, threshold, failures, timeout)
|
40
|
+
end
|
28
41
|
|
29
|
-
def
|
30
|
-
|
42
|
+
def get_attempts(name)
|
43
|
+
attempts[name]
|
31
44
|
end
|
32
45
|
|
33
46
|
def record_attempt(name)
|
34
|
-
|
35
|
-
all_attempts[name] += 1
|
47
|
+
attempts[name] += 1
|
36
48
|
end
|
37
49
|
|
38
50
|
def clear_attempts(name)
|
39
|
-
|
51
|
+
attempts.delete(name)
|
52
|
+
nil
|
40
53
|
end
|
41
54
|
|
42
|
-
|
43
|
-
|
44
|
-
def failures(name)
|
45
|
-
@data[failures_key(name)] || []
|
55
|
+
def get_failures(name)
|
56
|
+
@data.fetch(DataStore.failures_key(name), DEFAULT_FAILURES)
|
46
57
|
end
|
47
58
|
|
48
|
-
def record_failure(name,
|
49
|
-
|
59
|
+
def record_failure(name, failure)
|
60
|
+
DataStore.validate_failure!(failure)
|
61
|
+
@data[DataStore.failures_key(name)] ||= DEFAULT_FAILURES.dup
|
62
|
+
@data[DataStore.failures_key(name)].push(failure)
|
63
|
+
failure
|
50
64
|
end
|
51
65
|
|
52
66
|
def clear_failures(name)
|
53
|
-
@data.delete(failures_key(name))
|
67
|
+
@data.delete(DataStore.failures_key(name))
|
68
|
+
nil
|
54
69
|
end
|
55
70
|
|
56
|
-
|
57
|
-
|
58
|
-
def state(name)
|
59
|
-
all_states[name] || STATE_UNLOCKED
|
71
|
+
def get_state(name)
|
72
|
+
states[name]
|
60
73
|
end
|
61
74
|
|
62
75
|
def set_state(name, state)
|
63
|
-
validate_state!(state)
|
64
|
-
|
76
|
+
DataStore.validate_state!(state)
|
77
|
+
states[name] = state
|
65
78
|
end
|
66
79
|
|
67
|
-
|
80
|
+
def clear_state(name)
|
81
|
+
states.delete(name)
|
82
|
+
nil
|
83
|
+
end
|
68
84
|
|
69
|
-
def
|
70
|
-
|
85
|
+
def get_threshold(name)
|
86
|
+
thresholds[name]
|
71
87
|
end
|
72
88
|
|
73
89
|
def set_threshold(name, threshold)
|
74
|
-
|
90
|
+
DataStore.validate_threshold!(threshold)
|
91
|
+
thresholds[name] = threshold
|
92
|
+
end
|
93
|
+
|
94
|
+
def clear_threshold(name)
|
95
|
+
thresholds.delete(name)
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_timeout(name)
|
100
|
+
timeouts[name]
|
101
|
+
end
|
102
|
+
|
103
|
+
def set_timeout(name, timeout)
|
104
|
+
DataStore.validate_timeout!(timeout)
|
105
|
+
timeouts[name] = timeout
|
106
|
+
end
|
107
|
+
|
108
|
+
def clear_timeout(name)
|
109
|
+
timeouts.delete(name)
|
110
|
+
nil
|
75
111
|
end
|
76
112
|
|
77
113
|
private
|
78
114
|
|
79
|
-
|
80
|
-
|
115
|
+
# @return [Hash{String => Integer}]
|
116
|
+
def attempts
|
117
|
+
@data[DataStore.attempts_key] ||= Hash.new(DEFAULT_ATTEMPTS)
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Hash{String => String}]
|
121
|
+
def states
|
122
|
+
@data[DataStore.states_key] ||= Hash.new(DEFAULT_STATE)
|
81
123
|
end
|
82
124
|
|
83
|
-
|
84
|
-
|
125
|
+
# @return [Hash{String => Integer}]
|
126
|
+
def thresholds
|
127
|
+
@data[DataStore.thresholds_key] ||= Hash.new(DEFAULT_THRESHOLD)
|
85
128
|
end
|
86
129
|
|
87
|
-
|
88
|
-
|
130
|
+
# @return [Hash{String => Integer}]
|
131
|
+
def timeouts
|
132
|
+
@data[DataStore.timeouts_key] ||= Hash.new(DEFAULT_TIMEOUT)
|
89
133
|
end
|
90
134
|
end
|
91
135
|
end
|
@@ -1,4 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# rubocop:disable Metrics/ClassLength
|
3
|
+
|
4
|
+
require 'json'
|
2
5
|
|
3
6
|
module Stoplight
|
4
7
|
module DataStore
|
@@ -8,75 +11,162 @@ module Stoplight
|
|
8
11
|
end
|
9
12
|
|
10
13
|
def names
|
11
|
-
@redis.hkeys(thresholds_key)
|
14
|
+
@redis.hkeys(DataStore.thresholds_key)
|
12
15
|
end
|
13
16
|
|
14
|
-
def
|
17
|
+
def clear_stale
|
15
18
|
names
|
16
|
-
.select { |
|
17
|
-
.each
|
19
|
+
.select { |name| get_failures(name).empty? }
|
20
|
+
.each { |name| clear(name) }
|
21
|
+
nil
|
18
22
|
end
|
19
23
|
|
20
|
-
def
|
24
|
+
def clear(name)
|
21
25
|
@redis.pipelined do
|
22
26
|
clear_attempts(name)
|
23
27
|
clear_failures(name)
|
24
|
-
|
25
|
-
|
28
|
+
clear_state(name)
|
29
|
+
clear_threshold(name)
|
30
|
+
clear_timeout(name)
|
26
31
|
end
|
32
|
+
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def sync(name)
|
37
|
+
threshold = @redis.hget(DataStore.thresholds_key, name)
|
38
|
+
threshold = normalize_threshold(threshold)
|
39
|
+
@redis.hset(DataStore.thresholds_key, name, threshold)
|
40
|
+
nil
|
27
41
|
end
|
28
42
|
|
29
|
-
|
43
|
+
def get_color(name)
|
44
|
+
DataStore.colorize(*colorize_args(name))
|
45
|
+
end
|
30
46
|
|
31
|
-
def
|
32
|
-
@redis.hget(attempts_key, name)
|
47
|
+
def get_attempts(name)
|
48
|
+
normalize_attempts(@redis.hget(DataStore.attempts_key, name))
|
33
49
|
end
|
34
50
|
|
35
51
|
def record_attempt(name)
|
36
|
-
@redis.hincrby(attempts_key, name, 1)
|
52
|
+
@redis.hincrby(DataStore.attempts_key, name, 1)
|
37
53
|
end
|
38
54
|
|
39
55
|
def clear_attempts(name)
|
40
|
-
@redis.hdel(attempts_key, name)
|
56
|
+
@redis.hdel(DataStore.attempts_key, name)
|
57
|
+
nil
|
41
58
|
end
|
42
59
|
|
43
|
-
|
44
|
-
|
45
|
-
def failures(name)
|
46
|
-
@redis.lrange(failures_key(name), 0, -1)
|
60
|
+
def get_failures(name)
|
61
|
+
normalize_failures(@redis.lrange(DataStore.failures_key(name), 0, -1))
|
47
62
|
end
|
48
63
|
|
49
|
-
def record_failure(name,
|
50
|
-
|
64
|
+
def record_failure(name, failure)
|
65
|
+
DataStore.validate_failure!(failure)
|
66
|
+
@redis.rpush(DataStore.failures_key(name), failure.to_json)
|
67
|
+
failure
|
51
68
|
end
|
52
69
|
|
53
70
|
def clear_failures(name)
|
54
|
-
@redis.del(failures_key(name))
|
71
|
+
@redis.del(DataStore.failures_key(name))
|
72
|
+
nil
|
55
73
|
end
|
56
74
|
|
57
|
-
|
58
|
-
|
59
|
-
def state(name)
|
60
|
-
@redis.hget(states_key, name) || STATE_UNLOCKED
|
75
|
+
def get_state(name)
|
76
|
+
normalize_state(@redis.hget(DataStore.states_key, name))
|
61
77
|
end
|
62
78
|
|
63
79
|
def set_state(name, state)
|
64
|
-
validate_state!(state)
|
65
|
-
@redis.hset(states_key, name, state)
|
80
|
+
DataStore.validate_state!(state)
|
81
|
+
@redis.hset(DataStore.states_key, name, state)
|
66
82
|
state
|
67
83
|
end
|
68
84
|
|
69
|
-
|
85
|
+
def clear_state(name)
|
86
|
+
@redis.hdel(DataStore.states_key, name)
|
87
|
+
nil
|
88
|
+
end
|
70
89
|
|
71
|
-
def
|
72
|
-
|
73
|
-
Integer(value) if value
|
90
|
+
def get_threshold(name)
|
91
|
+
normalize_threshold(@redis.hget(DataStore.thresholds_key, name))
|
74
92
|
end
|
75
93
|
|
76
94
|
def set_threshold(name, threshold)
|
77
|
-
|
95
|
+
DataStore.validate_threshold!(threshold)
|
96
|
+
@redis.hset(DataStore.thresholds_key, name, threshold)
|
78
97
|
threshold
|
79
98
|
end
|
99
|
+
|
100
|
+
def clear_threshold(name)
|
101
|
+
@redis.hdel(DataStore.thresholds_key, name)
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_timeout(name)
|
106
|
+
normalize_timeout(@redis.hget(DataStore.timeouts_key, name))
|
107
|
+
end
|
108
|
+
|
109
|
+
def set_timeout(name, timeout)
|
110
|
+
DataStore.validate_timeout!(timeout)
|
111
|
+
@redis.hset(DataStore.timeouts_key, name, timeout)
|
112
|
+
timeout
|
113
|
+
end
|
114
|
+
|
115
|
+
def clear_timeout(name)
|
116
|
+
@redis.hdel(DataStore.timeouts_key, name)
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def colorize_args(name)
|
123
|
+
state, threshold, failures, timeout = @redis.pipelined do
|
124
|
+
@redis.hget(DataStore.states_key, name)
|
125
|
+
@redis.hget(DataStore.thresholds_key, name)
|
126
|
+
@redis.lrange(DataStore.failures_key(name), 0, -1)
|
127
|
+
@redis.hget(DataStore.timeouts_key, name)
|
128
|
+
end
|
129
|
+
normalize_colorize_args(state, threshold, failures, timeout)
|
130
|
+
end
|
131
|
+
|
132
|
+
def normalize_colorize_args(state, threshold, failures, timeout)
|
133
|
+
[
|
134
|
+
normalize_state(state),
|
135
|
+
normalize_threshold(threshold),
|
136
|
+
normalize_failures(failures),
|
137
|
+
normalize_timeout(timeout)
|
138
|
+
]
|
139
|
+
end
|
140
|
+
|
141
|
+
# @param attempts [String, nil]
|
142
|
+
# @return [Integer]
|
143
|
+
def normalize_attempts(attempts)
|
144
|
+
attempts ? attempts.to_i : DEFAULT_ATTEMPTS
|
145
|
+
end
|
146
|
+
|
147
|
+
# @param failures [Array<String>]
|
148
|
+
# @return [Array<Failure>]
|
149
|
+
def normalize_failures(failures)
|
150
|
+
failures.map { |json| Failure.from_json(json) }
|
151
|
+
end
|
152
|
+
|
153
|
+
# @param state [String, nil]
|
154
|
+
# @return [String]
|
155
|
+
def normalize_state(state)
|
156
|
+
state || DEFAULT_STATE
|
157
|
+
end
|
158
|
+
|
159
|
+
# @param threshold [String, nil]
|
160
|
+
# @return [Integer]
|
161
|
+
def normalize_threshold(threshold)
|
162
|
+
threshold ? threshold.to_i : DEFAULT_THRESHOLD
|
163
|
+
end
|
164
|
+
|
165
|
+
# @param timeout [String, nil]
|
166
|
+
# @return [Integer]
|
167
|
+
def normalize_timeout(timeout)
|
168
|
+
timeout ? timeout.to_i : DEFAULT_TIMEOUT
|
169
|
+
end
|
80
170
|
end
|
81
171
|
end
|
82
172
|
end
|
data/lib/stoplight/error.rb
CHANGED
@@ -4,7 +4,23 @@ module Stoplight
|
|
4
4
|
module Error
|
5
5
|
# @return [Class]
|
6
6
|
Base = Class.new(StandardError)
|
7
|
+
|
7
8
|
# @return [Class]
|
8
9
|
RedLight = Class.new(Base)
|
10
|
+
|
11
|
+
# @return [Class]
|
12
|
+
InvalidColor = Class.new(Base)
|
13
|
+
|
14
|
+
# @return [Class]
|
15
|
+
InvalidFailure = Class.new(Base)
|
16
|
+
|
17
|
+
# @return [Class]
|
18
|
+
InvalidState = Class.new(Base)
|
19
|
+
|
20
|
+
# @return [Class]
|
21
|
+
InvalidThreshold = Class.new(Base)
|
22
|
+
|
23
|
+
# @return [Class]
|
24
|
+
InvalidTimeout = Class.new(Base)
|
9
25
|
end
|
10
26
|
end
|
data/lib/stoplight/failure.rb
CHANGED
@@ -4,21 +4,34 @@ require 'json'
|
|
4
4
|
|
5
5
|
module Stoplight
|
6
6
|
class Failure
|
7
|
-
# @
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# @return [Exception]
|
8
|
+
attr_reader :error
|
9
|
+
|
10
|
+
# @return [Time]
|
11
|
+
attr_reader :time
|
12
|
+
|
13
|
+
def self.from_json(json)
|
14
|
+
h = JSON.parse(json)
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
match = /#<(.+): (.+)>/.match(h['error'])
|
17
|
+
error = Object.const_get(match[1]).new(match[2]) if match
|
18
|
+
|
19
|
+
time = Time.parse(h['time'])
|
20
|
+
|
21
|
+
new(error, time)
|
16
22
|
end
|
17
23
|
|
18
|
-
|
24
|
+
# @param error [Exception]
|
25
|
+
def initialize(error, time = nil)
|
26
|
+
@error = error
|
27
|
+
@time = time || Time.now
|
28
|
+
end
|
19
29
|
|
20
|
-
def
|
21
|
-
{
|
30
|
+
def to_json(*args)
|
31
|
+
{
|
32
|
+
error: @error.inspect,
|
33
|
+
time: time.inspect
|
34
|
+
}.to_json(*args)
|
22
35
|
end
|
23
36
|
end
|
24
37
|
end
|
data/lib/stoplight/light.rb
CHANGED
@@ -24,17 +24,15 @@ module Stoplight
|
|
24
24
|
# @see #fallback
|
25
25
|
# @see #green?
|
26
26
|
def run
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
run_fallback
|
27
|
+
Stoplight.data_store.sync(name)
|
28
|
+
|
29
|
+
case color
|
30
|
+
when DataStore::COLOR_GREEN
|
31
|
+
run_green
|
32
|
+
when DataStore::COLOR_YELLOW
|
33
|
+
run_yellow
|
34
|
+
when DataStore::COLOR_RED
|
35
|
+
run_red
|
38
36
|
end
|
39
37
|
end
|
40
38
|
|
@@ -57,7 +55,14 @@ module Stoplight
|
|
57
55
|
# @param threshold [Integer]
|
58
56
|
# @return [self]
|
59
57
|
def with_threshold(threshold)
|
60
|
-
Stoplight.set_threshold(name, threshold
|
58
|
+
Stoplight.data_store.set_threshold(name, threshold)
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param timeout [Integer]
|
63
|
+
# @return [self]
|
64
|
+
def with_timeout(timeout)
|
65
|
+
Stoplight.data_store.set_timeout(name, timeout)
|
61
66
|
self
|
62
67
|
end
|
63
68
|
|
@@ -67,51 +72,75 @@ module Stoplight
|
|
67
72
|
# @raise [Error::RedLight]
|
68
73
|
def fallback
|
69
74
|
return @fallback if defined?(@fallback)
|
70
|
-
fail Error::RedLight
|
75
|
+
fail Error::RedLight, name
|
71
76
|
end
|
72
77
|
|
73
|
-
# @return (see Stoplight
|
78
|
+
# @return (see Stoplight::DataStore::Base#threshold)
|
79
|
+
def threshold
|
80
|
+
Stoplight.data_store.get_threshold(name)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return (see Stoplight::DataStore::Base#timeout)
|
84
|
+
def timeout
|
85
|
+
Stoplight.data_store.get_timeout(name)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Colors
|
89
|
+
|
90
|
+
# @return (see Stoplight::DataStore::Base#get_color)
|
91
|
+
def color
|
92
|
+
Stoplight.data_store.get_color(name)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return (see Stoplight::DataStore::Base#green?)
|
74
96
|
def green?
|
75
|
-
Stoplight.green?(name)
|
97
|
+
Stoplight.data_store.green?(name)
|
76
98
|
end
|
77
99
|
|
78
|
-
# @return (see Stoplight
|
79
|
-
def
|
80
|
-
|
100
|
+
# @return (see Stoplight::DataStore::Base#yellow?)
|
101
|
+
def yellow?
|
102
|
+
Stoplight.data_store.yellow?(name)
|
81
103
|
end
|
82
104
|
|
83
|
-
# @return (see Stoplight
|
84
|
-
def
|
85
|
-
Stoplight.
|
105
|
+
# @return (see Stoplight::DataStore::Base#red?)
|
106
|
+
def red?
|
107
|
+
Stoplight.data_store.red?(name)
|
86
108
|
end
|
87
109
|
|
88
110
|
private
|
89
111
|
|
90
|
-
def
|
91
|
-
|
112
|
+
def run_green
|
113
|
+
code.call.tap { Stoplight.data_store.clear_failures(name) }
|
114
|
+
rescue => error
|
115
|
+
handle_error(error)
|
116
|
+
raise
|
92
117
|
end
|
93
118
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
119
|
+
def run_yellow
|
120
|
+
run_green.tap { notify("Switching #{name} from red to green.") }
|
121
|
+
end
|
122
|
+
|
123
|
+
def run_red
|
124
|
+
if Stoplight.data_store.record_attempt(name) == 1
|
125
|
+
notify("Switching #{name} from green to red.")
|
126
|
+
end
|
127
|
+
fallback.call
|
128
|
+
end
|
129
|
+
|
130
|
+
def handle_error(error)
|
99
131
|
if error_allowed?(error)
|
100
|
-
Stoplight.clear_failures(name)
|
132
|
+
Stoplight.data_store.clear_failures(name)
|
101
133
|
else
|
102
|
-
Stoplight.record_failure(name, error)
|
134
|
+
Stoplight.data_store.record_failure(name, Failure.new(error))
|
103
135
|
end
|
104
|
-
|
105
|
-
raise
|
106
136
|
end
|
107
137
|
|
108
|
-
def
|
109
|
-
|
110
|
-
fallback.call
|
138
|
+
def error_allowed?(error)
|
139
|
+
allowed_errors.any? { |klass| error.is_a?(klass) }
|
111
140
|
end
|
112
141
|
|
113
|
-
def
|
114
|
-
Stoplight.
|
142
|
+
def notify(message)
|
143
|
+
Stoplight.notifiers.each { |notifier| notifier.notify(message) }
|
115
144
|
end
|
116
145
|
end
|
117
146
|
end
|