status_lib 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
+
@@ -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