status_lib 0.0.2
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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/bin/guard +16 -0
- data/lib/status_lib/cache.rb +33 -0
- data/lib/status_lib/config.rb +61 -0
- data/lib/status_lib/null_object.rb +6 -0
- data/lib/status_lib/null_stats_handler.rb +3 -0
- data/lib/status_lib/status_api.rb +62 -0
- data/lib/status_lib/status_info.rb +72 -0
- data/lib/status_lib/version.rb +3 -0
- data/lib/status_lib.rb +119 -0
- data/spec/lib/status_lib/cache_spec.rb +45 -0
- data/spec/lib/status_lib/config_spec.rb +100 -0
- data/spec/lib/status_lib/null_object_spec.rb +28 -0
- data/spec/lib/status_lib/null_stats_handler_spec.rb +9 -0
- data/spec/lib/status_lib/status_api_spec.rb +122 -0
- data/spec/lib/status_lib/status_info_spec.rb +146 -0
- data/spec/lib/status_lib_spec.rb +195 -0
- data/spec/spec_helper.rb +17 -0
- data/status_lib.gemspec +30 -0
- metadata +231 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe StatusLib::StatusApi do
|
4
|
+
let(:api) { StatusLib::StatusApi.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
StatusLib.configure(server: "http://status.example.com")
|
8
|
+
end
|
9
|
+
|
10
|
+
context "HTTP requests when the service is crashing" do
|
11
|
+
shared_examples_for "an unavailable API" do
|
12
|
+
it "raises an ApiDownError for #get_status_list" do
|
13
|
+
expect {
|
14
|
+
api.get_status_list
|
15
|
+
}.to raise_exception StatusLib::ApiDownError
|
16
|
+
end
|
17
|
+
|
18
|
+
it "raises an ApiDownError for #send_status_update" do
|
19
|
+
expect {
|
20
|
+
api.send_status_update('test', :down, 20)
|
21
|
+
}.to raise_exception StatusLib::ApiDownError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when the API has errors" do
|
26
|
+
before do
|
27
|
+
stub_request(:get, "status.example.com/api/status").
|
28
|
+
to_return(status: 500)
|
29
|
+
stub_request(:put, "status.example.com/api/status/test").
|
30
|
+
to_return(status: 500)
|
31
|
+
end
|
32
|
+
|
33
|
+
it_behaves_like "an unavailable API"
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when the API is timing out" do
|
37
|
+
before do
|
38
|
+
stub_request(:get, "status.example.com/api/status").to_return do
|
39
|
+
raise Timeout::Error
|
40
|
+
end
|
41
|
+
stub_request(:put, "status.example.com/api/status/test").to_return do
|
42
|
+
raise Timeout::Error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it_behaves_like "an unavailable API"
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when we crash trying to call the API" do
|
50
|
+
before do
|
51
|
+
stub_request(:get, "status.example.com/api/status").to_return do
|
52
|
+
raise RuntimeError
|
53
|
+
end
|
54
|
+
stub_request(:put, "status.example.com/api/status/test").to_return do
|
55
|
+
raise RuntimeError
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it_behaves_like "an unavailable API"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "HTTP requests" do
|
64
|
+
context "#get_status_list" do
|
65
|
+
before do
|
66
|
+
body = { one: "up", two: "down" }.to_json
|
67
|
+
stub_request(:get, "status.example.com/api/status").
|
68
|
+
to_return(body: body)
|
69
|
+
end
|
70
|
+
|
71
|
+
let!(:result) { api.get_status_list }
|
72
|
+
|
73
|
+
it "calls the service" do
|
74
|
+
expect(WebMock).to have_requested(:get, "status.example.com/api/status")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "returns the status of each service" do
|
78
|
+
expect(result[:one]).to eq(:up)
|
79
|
+
expect(result[:two]).to eq(:down)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "#send_status_update" do
|
84
|
+
before do
|
85
|
+
stub_request(:put, "status.example.com/api/status/test")
|
86
|
+
end
|
87
|
+
|
88
|
+
context "on a valid service" do
|
89
|
+
it "calls the service" do
|
90
|
+
api.send_status_update('test', :down, 20)
|
91
|
+
|
92
|
+
expect(WebMock).
|
93
|
+
to have_requested(:put, "status.example.com/api/status/test").
|
94
|
+
with(body: { status: 'down', expires: 20 }.to_json)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "when passing a Time instance" do
|
99
|
+
it "calls the service with an epoch time in UTC" do
|
100
|
+
api.send_status_update('test', :down, Time.utc(1972, 1, 22, 17, 30))
|
101
|
+
|
102
|
+
expect(WebMock).
|
103
|
+
to have_requested(:put, "status.example.com/api/status/test").
|
104
|
+
with(body: { status: 'down', expires: 64949400 }.to_json)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "on an unknown service" do
|
109
|
+
before do
|
110
|
+
stub_request(:put, "status.example.com/api/status/test").
|
111
|
+
to_return(status: 404)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "raises an ArgumentError" do
|
115
|
+
expect {
|
116
|
+
api.send_status_update('test', :down, 20)
|
117
|
+
}.to raise_exception ArgumentError
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe StatusLib::StatusInfo do
|
4
|
+
let(:info) { StatusLib::StatusInfo.new(api) }
|
5
|
+
let(:api) { api_stub.new }
|
6
|
+
|
7
|
+
before do
|
8
|
+
StatusLib::Cache.clear!
|
9
|
+
StatusLib.configure(server: "http://status.example.com")
|
10
|
+
end
|
11
|
+
|
12
|
+
context "calling the API" do
|
13
|
+
let(:api_stub) { double(:api_stub, new: double(:api)) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(api).to receive(:send_status_update)
|
17
|
+
|
18
|
+
Timecop.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
it "passes the :for duration as an expiry time" do
|
22
|
+
info.switch(:one, :down, for: 99)
|
23
|
+
|
24
|
+
expect(api).to have_received(:send_status_update).
|
25
|
+
with(:one, :down, Time.now + 99)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "passes a nil :for duration as a nil expiry time" do
|
29
|
+
info.switch(:one, :down, for: nil)
|
30
|
+
|
31
|
+
expect(api).to have_received(:send_status_update).
|
32
|
+
with(:one, :down, nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "passes a default expiry time if no :for duration supplied" do
|
36
|
+
info.switch(:one, :down)
|
37
|
+
|
38
|
+
expect(api).to have_received(:send_status_update).
|
39
|
+
with(:one, :down, Time.now + StatusLib.config.down_duration)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
shared_examples_for "a circuit breaker" do
|
44
|
+
it "disables and enables services" do
|
45
|
+
info.switch(:one, :down, for: 30)
|
46
|
+
expect(info.up?(:one)).to be_false
|
47
|
+
|
48
|
+
info.switch(:one, :up)
|
49
|
+
expect(info.up?(:one)).to be_true
|
50
|
+
end
|
51
|
+
|
52
|
+
it "caches the results from the API" do
|
53
|
+
allow(api).to receive(:get_status_list).and_call_original
|
54
|
+
|
55
|
+
5.times do
|
56
|
+
expect(info.up?(:one)).to be_true
|
57
|
+
end
|
58
|
+
|
59
|
+
expect(api).to have_received(:get_status_list).once
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can switch items down for an interval of time" do
|
63
|
+
Timecop.freeze 0
|
64
|
+
info.switch(:one, :down, for: 30)
|
65
|
+
|
66
|
+
expect(info.up?(:one)).to be_false
|
67
|
+
|
68
|
+
Timecop.freeze(15)
|
69
|
+
expect(info.up?(:one)).to be_false
|
70
|
+
|
71
|
+
Timecop.freeze(60)
|
72
|
+
expect(info.up?(:one)).to be_true
|
73
|
+
end
|
74
|
+
|
75
|
+
it "records a change of status in the configured Stats object" do
|
76
|
+
allow(StatusLib.config.stats_handler).to receive(:increment)
|
77
|
+
|
78
|
+
info.switch(:one, :down)
|
79
|
+
|
80
|
+
expect(StatusLib.config.stats_handler).to have_received(:increment).
|
81
|
+
with('status.change.one.down')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "when the status API is up" do
|
86
|
+
let(:api_stub) do
|
87
|
+
Class.new do
|
88
|
+
def initialize
|
89
|
+
@statuses = { one: :up, two: :down }
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_status_list
|
93
|
+
return @statuses
|
94
|
+
end
|
95
|
+
|
96
|
+
def send_status_update(name, status, duration)
|
97
|
+
@statuses[name] = status
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
it_behaves_like "a circuit breaker"
|
103
|
+
end
|
104
|
+
|
105
|
+
context "when the status API is broken" do
|
106
|
+
let(:api_stub) do
|
107
|
+
Class.new do
|
108
|
+
def get_status_list
|
109
|
+
raise StatusLib::ApiDownError
|
110
|
+
end
|
111
|
+
|
112
|
+
def send_status_update(name, status, duration)
|
113
|
+
raise StatusLib::ApiDownError
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it_behaves_like "a circuit breaker"
|
119
|
+
|
120
|
+
context "after the API comes back" do
|
121
|
+
it "sends the last update that happened while it was down" do
|
122
|
+
Timecop.freeze
|
123
|
+
|
124
|
+
info.switch(:one, :up)
|
125
|
+
info.switch(:one, :down, for: 30)
|
126
|
+
info.switch(:one, :up)
|
127
|
+
info.switch(:one, :down, for: 45)
|
128
|
+
|
129
|
+
allow(api).to receive(:get_status_list).and_return({})
|
130
|
+
allow(api).to receive(:send_status_update)
|
131
|
+
|
132
|
+
info.up?(:one)
|
133
|
+
|
134
|
+
expect(api).to have_received(:send_status_update).
|
135
|
+
with(:one, :down, Time.now + 45).
|
136
|
+
once
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "when there is a null status API configured" do
|
142
|
+
let(:api_stub) { StatusLib::NullStatusApi }
|
143
|
+
|
144
|
+
it_behaves_like "a circuit breaker"
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe StatusLib::ServiceDownError do
|
4
|
+
it "provides access to an original exception" do
|
5
|
+
original = ArgumentError.new
|
6
|
+
error = StatusLib::ServiceDownError.new(original)
|
7
|
+
|
8
|
+
expect(error.original_exception).to eq(original)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "has no original exception by default" do
|
12
|
+
error = StatusLib::ServiceDownError.new
|
13
|
+
|
14
|
+
expect(error.original_exception).to be_nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe StatusLib do
|
19
|
+
let(:status_info) { StatusLib.send(:status_info) }
|
20
|
+
|
21
|
+
it 'should have a version number' do
|
22
|
+
expect(StatusLib::VERSION).to_not be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
context "#up?" do
|
26
|
+
it "calls the StatusInfo method" do
|
27
|
+
expect(status_info).to receive(:up?).with(:test)
|
28
|
+
|
29
|
+
StatusLib.up?(:test)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "#switch" do
|
34
|
+
it "calls the StatusInfo method" do
|
35
|
+
expect(status_info).to receive(:switch).with(:test, :down, for: 30)
|
36
|
+
|
37
|
+
StatusLib.switch(:test, :down, for: 30)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "#with_circuit_breaker" do
|
42
|
+
before do
|
43
|
+
StatusLib::Cache.clear!
|
44
|
+
StatusLib.configure(server: "http://status.example.com")
|
45
|
+
end
|
46
|
+
|
47
|
+
class TestException < RuntimeError; end
|
48
|
+
|
49
|
+
def invoke_test(should_fail)
|
50
|
+
StatusLib.with_circuit_breaker(:test, for: 20) do
|
51
|
+
raise TestException if should_fail
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def invoke_test_and_handle_exception(should_fail)
|
56
|
+
invoke_test(should_fail)
|
57
|
+
rescue StatusLib::ServiceDownError
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when the service is up" do
|
61
|
+
before do
|
62
|
+
allow(status_info).to receive(:up?).with(:test).and_return(true)
|
63
|
+
allow(status_info).to receive(:switch)
|
64
|
+
end
|
65
|
+
|
66
|
+
context "when the block works" do
|
67
|
+
it "does not mark the service down" do
|
68
|
+
expect(status_info).to_not receive(:switch)
|
69
|
+
invoke_test(false)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "does not raise an exception" do
|
73
|
+
expect { invoke_test(false) }.to_not raise_exception
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when the block crashes" do
|
78
|
+
it "marks the service as down" do
|
79
|
+
invoke_test_and_handle_exception(true)
|
80
|
+
|
81
|
+
expect(status_info).to have_received(:switch).
|
82
|
+
with(:test, :down, for: 20)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "raises a ServiceDownError" do
|
86
|
+
expect {
|
87
|
+
invoke_test(true)
|
88
|
+
}.to raise_exception StatusLib::ServiceDownError
|
89
|
+
end
|
90
|
+
|
91
|
+
it "provides access to the original exception" do
|
92
|
+
begin
|
93
|
+
invoke_test(true)
|
94
|
+
rescue StatusLib::ServiceDownError => ex
|
95
|
+
expect(ex.original_exception).to be_an_instance_of(TestException)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "when the block times out" do
|
100
|
+
it "marks the service as down" do
|
101
|
+
begin
|
102
|
+
StatusLib.with_circuit_breaker(:test, for: 20, timeout: 0.25) do
|
103
|
+
sleep 0.5
|
104
|
+
end
|
105
|
+
rescue Timeout::Error
|
106
|
+
end
|
107
|
+
|
108
|
+
expect(status_info).to have_received(:switch).
|
109
|
+
with(:test, :down, for: 20)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "raises a Timeout::Error" do
|
113
|
+
expect {
|
114
|
+
StatusLib.with_circuit_breaker(:test, timeout: 0.25) do
|
115
|
+
sleep 0.5
|
116
|
+
end
|
117
|
+
}.to raise_exception Timeout::Error
|
118
|
+
end
|
119
|
+
|
120
|
+
it "raises a ServiceDownError" do
|
121
|
+
expect {
|
122
|
+
StatusLib.with_circuit_breaker(:test, timeout: 0.25) do
|
123
|
+
raise StandardError.new
|
124
|
+
end
|
125
|
+
}.to raise_exception StatusLib::ServiceDownError
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "when an exception handler has been configured" do
|
130
|
+
let(:mock_handler) { double(:handler, call: nil) }
|
131
|
+
|
132
|
+
before do
|
133
|
+
allow(StatusLib.config).to receive(:exception_handler).
|
134
|
+
and_return(mock_handler)
|
135
|
+
|
136
|
+
invoke_test_and_handle_exception(true)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "invokes the supplied exception handler on failure" do
|
140
|
+
expect(mock_handler).to have_received(:call).
|
141
|
+
with(an_instance_of(TestException))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context "when the block crashes but we have a zero downtime" do
|
147
|
+
before do
|
148
|
+
begin
|
149
|
+
StatusLib.with_circuit_breaker(:test, for: 0) do
|
150
|
+
raise TestException
|
151
|
+
end
|
152
|
+
rescue StatusLib::ServiceDownError
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "does not mark the service down" do
|
157
|
+
expect(status_info).to_not have_received(:switch)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when the block crashes but we have a nil downtime" do
|
162
|
+
before do
|
163
|
+
begin
|
164
|
+
StatusLib.with_circuit_breaker(:test, for: nil) do
|
165
|
+
raise TestException
|
166
|
+
end
|
167
|
+
rescue StatusLib::ServiceDownError
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
it "does not mark the service down" do
|
172
|
+
expect(status_info).to_not have_received(:switch)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "when the service is down" do
|
178
|
+
before do
|
179
|
+
allow(status_info).to receive(:up?).with(:test).and_return(false)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "raises a ServiceDownError" do
|
183
|
+
expect {
|
184
|
+
invoke_test(false)
|
185
|
+
}.to raise_exception StatusLib::ServiceDownError
|
186
|
+
end
|
187
|
+
|
188
|
+
it "does not execute the block" do
|
189
|
+
expect { |b|
|
190
|
+
invoke_test_and_handle_exception(true, &b)
|
191
|
+
}.to_not yield_control
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'status_lib'
|
3
|
+
require 'rspec'
|
4
|
+
require 'webmock/rspec'
|
5
|
+
require 'timecop'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
# Only use the newer-style syntax
|
10
|
+
config.expect_with :rspec do |c|
|
11
|
+
c.syntax = :expect
|
12
|
+
end
|
13
|
+
config.mock_with :rspec do |c|
|
14
|
+
c.syntax = :expect
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
data/status_lib.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'status_lib/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "status_lib"
|
8
|
+
spec.version = StatusLib::VERSION
|
9
|
+
spec.authors = ["Kevin McConnell"]
|
10
|
+
spec.email = ["kevin.mcconnell@livingsocial.com"]
|
11
|
+
spec.description = "Wrapper library for access to service status service"
|
12
|
+
spec.summary = ""
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "ls-gem_tasks"
|
23
|
+
spec.add_development_dependency "rb-fsevent"
|
24
|
+
spec.add_development_dependency "guard"
|
25
|
+
spec.add_development_dependency "guard-rspec"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
28
|
+
spec.add_development_dependency "timecop"
|
29
|
+
spec.add_development_dependency "webmock"
|
30
|
+
end
|