stoplight 0.4.1 → 0.5.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 +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
|