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.
@@ -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