stoplight 0.4.1 → 0.5.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 +15 -0
- data/LICENSE.md +1 -1
- data/README.md +66 -63
- data/lib/stoplight.rb +10 -15
- data/lib/stoplight/color.rb +9 -0
- data/lib/stoplight/data_store.rb +0 -146
- data/lib/stoplight/data_store/base.rb +7 -130
- data/lib/stoplight/data_store/memory.rb +25 -100
- data/lib/stoplight/data_store/redis.rb +61 -119
- data/lib/stoplight/default.rb +34 -0
- data/lib/stoplight/error.rb +0 -42
- data/lib/stoplight/failure.rb +21 -25
- data/lib/stoplight/light.rb +42 -127
- data/lib/stoplight/light/runnable.rb +97 -0
- data/lib/stoplight/notifier/base.rb +1 -4
- data/lib/stoplight/notifier/hip_chat.rb +17 -32
- data/lib/stoplight/notifier/io.rb +9 -9
- data/lib/stoplight/state.rb +9 -0
- data/spec/spec_helper.rb +2 -3
- data/spec/stoplight/color_spec.rb +39 -0
- data/spec/stoplight/data_store/base_spec.rb +56 -36
- data/spec/stoplight/data_store/memory_spec.rb +120 -2
- data/spec/stoplight/data_store/redis_spec.rb +123 -24
- data/spec/stoplight/data_store_spec.rb +2 -69
- data/spec/stoplight/default_spec.rb +86 -0
- data/spec/stoplight/error_spec.rb +29 -0
- data/spec/stoplight/failure_spec.rb +61 -51
- data/spec/stoplight/light/runnable_spec.rb +234 -0
- data/spec/stoplight/light_spec.rb +143 -191
- data/spec/stoplight/notifier/base_spec.rb +8 -11
- data/spec/stoplight/notifier/hip_chat_spec.rb +66 -55
- data/spec/stoplight/notifier/io_spec.rb +49 -21
- data/spec/stoplight/notifier_spec.rb +3 -0
- data/spec/stoplight/state_spec.rb +39 -0
- data/spec/stoplight_spec.rb +2 -65
- metadata +55 -19
- data/spec/support/data_store.rb +0 -36
- data/spec/support/fakeredis.rb +0 -3
- data/spec/support/hipchat.rb +0 -3
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
module Default
|
5
|
+
ALLOWED_ERRORS = [
|
6
|
+
NoMemoryError,
|
7
|
+
ScriptError,
|
8
|
+
SecurityError,
|
9
|
+
SignalException,
|
10
|
+
SystemExit,
|
11
|
+
SystemStackError
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
DATA_STORE = DataStore::Memory.new
|
15
|
+
|
16
|
+
ERROR_NOTIFIER = -> error { warn error }
|
17
|
+
|
18
|
+
FALLBACK = nil
|
19
|
+
|
20
|
+
FORMATTER = lambda do |light, from_color, to_color, error|
|
21
|
+
words = ['Switching', light.name, 'from', from_color, 'to', to_color]
|
22
|
+
words += ['because', error.class, error.message] if error
|
23
|
+
words.join(' ')
|
24
|
+
end
|
25
|
+
|
26
|
+
NOTIFIERS = [
|
27
|
+
Notifier::IO.new($stderr)
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
THRESHOLD = 3
|
31
|
+
|
32
|
+
TIMEOUT = 60.0
|
33
|
+
end
|
34
|
+
end
|
data/lib/stoplight/error.rb
CHANGED
@@ -2,49 +2,7 @@
|
|
2
2
|
|
3
3
|
module Stoplight
|
4
4
|
module Error
|
5
|
-
# @return [Class]
|
6
5
|
Base = Class.new(StandardError)
|
7
|
-
|
8
|
-
# @return [Class]
|
9
6
|
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)
|
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
|
49
7
|
end
|
50
8
|
end
|
data/lib/stoplight/failure.rb
CHANGED
@@ -5,48 +5,44 @@ require 'time'
|
|
5
5
|
|
6
6
|
module Stoplight
|
7
7
|
class Failure
|
8
|
-
# @return [String]
|
9
8
|
attr_reader :error_class
|
10
|
-
|
11
|
-
# @return [String]
|
12
9
|
attr_reader :error_message
|
13
|
-
|
14
|
-
# @return [Time]
|
15
10
|
attr_reader :time
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
new(error.class.name, error.message)
|
12
|
+
def self.from_error(error)
|
13
|
+
new(error.class.name, error.message, Time.new)
|
20
14
|
end
|
21
15
|
|
22
|
-
# @param json [String]
|
23
16
|
def self.from_json(json)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
new(
|
17
|
+
object = JSON.parse(json)
|
18
|
+
|
19
|
+
error_class = object['error']['class']
|
20
|
+
error_message = object['error']['message']
|
21
|
+
time = Time.parse(object['time'])
|
22
|
+
|
23
|
+
new(error_class, error_message, time)
|
31
24
|
end
|
32
25
|
|
33
|
-
|
34
|
-
# @param error_message [String]
|
35
|
-
# @param time [Time, nil]
|
36
|
-
def initialize(error_class, error_message, time = nil)
|
26
|
+
def initialize(error_class, error_message, time)
|
37
27
|
@error_class = error_class
|
38
28
|
@error_message = error_message
|
39
|
-
@time = time
|
29
|
+
@time = time
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
error_class == other.error_class &&
|
34
|
+
error_message == other.error_message &&
|
35
|
+
time == other.time
|
40
36
|
end
|
41
37
|
|
42
|
-
def to_json
|
43
|
-
|
38
|
+
def to_json
|
39
|
+
JSON.generate(
|
44
40
|
error: {
|
45
41
|
class: error_class,
|
46
42
|
message: error_message
|
47
43
|
},
|
48
|
-
time: time.
|
49
|
-
|
44
|
+
time: time.strftime('%Y-%m-%dT%H:%M:%S.%N%:z')
|
45
|
+
)
|
50
46
|
end
|
51
47
|
end
|
52
48
|
end
|
data/lib/stoplight/light.rb
CHANGED
@@ -2,159 +2,74 @@
|
|
2
2
|
|
3
3
|
module Stoplight
|
4
4
|
class Light
|
5
|
-
|
6
|
-
attr_reader :allowed_errors
|
5
|
+
include Runnable
|
7
6
|
|
8
|
-
|
7
|
+
attr_reader :allowed_errors
|
9
8
|
attr_reader :code
|
10
|
-
|
11
|
-
|
9
|
+
attr_reader :data_store
|
10
|
+
attr_reader :error_notifier
|
11
|
+
attr_reader :fallback
|
12
12
|
attr_reader :name
|
13
|
+
attr_reader :notifiers
|
14
|
+
attr_reader :threshold
|
15
|
+
attr_reader :timeout
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@code = code.to_proc
|
19
|
-
@name = name.to_s
|
17
|
+
class << self
|
18
|
+
attr_accessor :default_data_store
|
19
|
+
attr_accessor :default_error_notifier
|
20
|
+
attr_accessor :default_notifiers
|
20
21
|
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
# @see #green?
|
26
|
-
def run
|
27
|
-
sync
|
23
|
+
@default_data_store = Default::DATA_STORE
|
24
|
+
@default_error_notifier = Default::ERROR_NOTIFIER
|
25
|
+
@default_notifiers = Default::NOTIFIERS
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
when DataStore::COLOR_YELLOW
|
33
|
-
run_yellow
|
34
|
-
when DataStore::COLOR_RED
|
35
|
-
run_red
|
36
|
-
end
|
37
|
-
end
|
27
|
+
def initialize(name, &code)
|
28
|
+
@name = name
|
29
|
+
@code = code
|
38
30
|
|
39
|
-
|
31
|
+
@allowed_errors = Default::ALLOWED_ERRORS
|
32
|
+
@data_store = self.class.default_data_store
|
33
|
+
@error_notifier = self.class.default_error_notifier
|
34
|
+
@fallback = Default::FALLBACK
|
35
|
+
@notifiers = self.class.default_notifiers
|
36
|
+
@threshold = Default::THRESHOLD
|
37
|
+
@timeout = Default::TIMEOUT
|
38
|
+
end
|
40
39
|
|
41
|
-
# @param allowed_errors [Array<Exception>]
|
42
|
-
# @return [self]
|
43
40
|
def with_allowed_errors(allowed_errors)
|
44
|
-
@allowed_errors = allowed_errors
|
41
|
+
@allowed_errors = Default::ALLOWED_ERRORS + allowed_errors
|
45
42
|
self
|
46
43
|
end
|
47
44
|
|
48
|
-
|
49
|
-
|
50
|
-
def with_fallback(&fallback)
|
51
|
-
@fallback = fallback.to_proc
|
45
|
+
def with_data_store(data_store)
|
46
|
+
@data_store = data_store
|
52
47
|
self
|
53
48
|
end
|
54
49
|
|
55
|
-
|
56
|
-
|
57
|
-
def with_threshold(threshold)
|
58
|
-
Stoplight.data_store.set_threshold(name, threshold)
|
50
|
+
def with_error_notifier(&error_notifier)
|
51
|
+
@error_notifier = error_notifier
|
59
52
|
self
|
60
53
|
end
|
61
54
|
|
62
|
-
|
63
|
-
|
64
|
-
def with_timeout(timeout)
|
65
|
-
Stoplight.data_store.set_timeout(name, timeout)
|
55
|
+
def with_fallback(&fallback)
|
56
|
+
@fallback = fallback
|
66
57
|
self
|
67
58
|
end
|
68
59
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
# @raise [Error::RedLight]
|
73
|
-
def fallback
|
74
|
-
return @fallback if defined?(@fallback)
|
75
|
-
fail Error::RedLight, name
|
76
|
-
end
|
77
|
-
|
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?)
|
96
|
-
def green?
|
97
|
-
Stoplight.data_store.green?(name)
|
98
|
-
end
|
99
|
-
|
100
|
-
# @return (see Stoplight::DataStore::Base#yellow?)
|
101
|
-
def yellow?
|
102
|
-
Stoplight.data_store.yellow?(name)
|
103
|
-
end
|
104
|
-
|
105
|
-
# @return (see Stoplight::DataStore::Base#red?)
|
106
|
-
def red?
|
107
|
-
Stoplight.data_store.red?(name)
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
def run_green
|
113
|
-
code.call.tap { Stoplight.data_store.greenify(name) }
|
114
|
-
rescue => error
|
115
|
-
handle_error(error)
|
116
|
-
raise
|
117
|
-
end
|
118
|
-
|
119
|
-
def run_yellow
|
120
|
-
run_green.tap { notify(DataStore::COLOR_RED, DataStore::COLOR_GREEN) }
|
121
|
-
end
|
122
|
-
|
123
|
-
def run_red
|
124
|
-
if Stoplight.data_store.record_attempt(name) == 1
|
125
|
-
notify(DataStore::COLOR_GREEN, DataStore::COLOR_RED)
|
126
|
-
end
|
127
|
-
fallback.call
|
128
|
-
end
|
129
|
-
|
130
|
-
def handle_error(error)
|
131
|
-
if error_allowed?(error)
|
132
|
-
Stoplight.data_store.greenify(name)
|
133
|
-
else
|
134
|
-
Stoplight.data_store.record_failure(name, Failure.create(error))
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def error_allowed?(error)
|
139
|
-
allowed_errors.any? { |klass| error.is_a?(klass) }
|
60
|
+
def with_notifiers(notifiers)
|
61
|
+
@notifiers = notifiers
|
62
|
+
self
|
140
63
|
end
|
141
64
|
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
notifier.notify(self, from_color, to_color)
|
146
|
-
rescue Error::BadNotifier => error
|
147
|
-
warn(error.cause)
|
148
|
-
end
|
149
|
-
end
|
65
|
+
def with_threshold(threshold)
|
66
|
+
@threshold = threshold
|
67
|
+
self
|
150
68
|
end
|
151
69
|
|
152
|
-
def
|
153
|
-
|
154
|
-
|
155
|
-
warn(error.cause)
|
156
|
-
Stoplight.data_store = Stoplight::DataStore::Memory.new
|
157
|
-
retry
|
70
|
+
def with_timeout(timeout)
|
71
|
+
@timeout = timeout
|
72
|
+
self
|
158
73
|
end
|
159
74
|
end
|
160
75
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
class Light
|
5
|
+
module Runnable
|
6
|
+
def color
|
7
|
+
failures, state = failures_and_state
|
8
|
+
now = Time.new
|
9
|
+
|
10
|
+
case
|
11
|
+
when state == State::LOCKED_GREEN then Color::GREEN
|
12
|
+
when state == State::LOCKED_RED then Color::RED
|
13
|
+
when failures.size < threshold then Color::GREEN
|
14
|
+
when failures.any? { |f| now - f.time >= timeout } then Color::YELLOW
|
15
|
+
else Color::RED
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
case color
|
21
|
+
when Color::GREEN then run_green
|
22
|
+
when Color::YELLOW then run_yellow
|
23
|
+
else run_red
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def run_green
|
30
|
+
on_failure = lambda do |size, error|
|
31
|
+
notify(Color::GREEN, Color::RED, error) if size == threshold
|
32
|
+
end
|
33
|
+
run_code(nil, on_failure)
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_yellow
|
37
|
+
on_success = lambda do |failures|
|
38
|
+
notify(Color::RED, Color::GREEN) unless failures.empty?
|
39
|
+
end
|
40
|
+
run_code(on_success, nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_red
|
44
|
+
fail Error::RedLight, name unless fallback
|
45
|
+
fallback.call(nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_code(on_success, on_failure)
|
49
|
+
result = code.call
|
50
|
+
failures = clear_failures
|
51
|
+
on_success.call(failures) if on_success
|
52
|
+
result
|
53
|
+
rescue => error
|
54
|
+
handle_error(error, on_failure)
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_error(error, on_failure)
|
58
|
+
fail error if allowed_errors.any? { |klass| error.is_a?(klass) }
|
59
|
+
size = record_failure(error)
|
60
|
+
on_failure.call(size, error) if on_failure
|
61
|
+
fail error unless fallback
|
62
|
+
fallback.call(error)
|
63
|
+
end
|
64
|
+
|
65
|
+
def clear_failures
|
66
|
+
safely([]) { data_store.clear_failures(self) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def failures_and_state
|
70
|
+
safely([[], State::UNLOCKED]) { data_store.get_all(self) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def notify(from_color, to_color, error = nil)
|
74
|
+
notifiers.each do |notifier|
|
75
|
+
safely { notifier.notify(self, from_color, to_color, error) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def record_failure(error)
|
80
|
+
failure = Failure.from_error(error)
|
81
|
+
safely(0) { data_store.record_failure(self, failure) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def safely(default = nil, &code)
|
85
|
+
return code.call if data_store == Default::DATA_STORE
|
86
|
+
|
87
|
+
self.class.new("#{name}-safely", &code)
|
88
|
+
.with_data_store(Default::DATA_STORE)
|
89
|
+
.with_fallback do |error|
|
90
|
+
error_notifier.call(error) if error
|
91
|
+
default
|
92
|
+
end
|
93
|
+
.run
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|