right_agent 2.0.8-x86-mingw32 → 2.1.0-x86-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/right_agent.rb +1 -2
- data/lib/right_agent/actors/agent_manager.rb +3 -3
- data/lib/right_agent/agent.rb +14 -4
- data/lib/right_agent/clients.rb +10 -0
- data/lib/right_agent/clients/balanced_http_client.rb +210 -122
- data/lib/right_agent/clients/base_retry_client.rb +25 -6
- data/lib/right_agent/clients/blocking_client.rb +155 -0
- data/lib/right_agent/clients/non_blocking_client.rb +198 -0
- data/lib/right_agent/clients/right_http_client.rb +2 -2
- data/lib/right_agent/clients/router_client.rb +205 -80
- data/lib/right_agent/connectivity_checker.rb +1 -1
- data/lib/right_agent/eventmachine_spawn.rb +70 -0
- data/lib/right_agent/exceptions.rb +4 -20
- data/lib/right_agent/http_exceptions.rb +87 -0
- data/lib/right_agent/minimal.rb +1 -0
- data/lib/right_agent/retryable_request.rb +1 -1
- data/lib/right_agent/scripts/agent_controller.rb +12 -6
- data/lib/right_agent/scripts/agent_deployer.rb +6 -0
- data/lib/right_agent/sender.rb +31 -6
- data/right_agent.gemspec +2 -2
- data/spec/agent_spec.rb +9 -6
- data/spec/clients/balanced_http_client_spec.rb +349 -244
- data/spec/clients/base_retry_client_spec.rb +31 -15
- data/spec/clients/blocking_client_spec.rb +265 -0
- data/spec/clients/non_blocking_client_spec.rb +387 -0
- data/spec/clients/router_client_spec.rb +390 -171
- data/spec/http_exceptions_spec.rb +106 -0
- data/spec/retryable_request_spec.rb +13 -13
- data/spec/sender_spec.rb +48 -22
- data/spec/spec_helper.rb +0 -26
- metadata +10 -3
@@ -103,7 +103,7 @@ describe RightScale::BaseRetryClient do
|
|
103
103
|
|
104
104
|
it "sets up for reconnect if not connected" do
|
105
105
|
flexmock(RightScale::BalancedHttpClient).should_receive(:new).and_return(@http_client).once
|
106
|
-
e = RightScale::BalancedHttpClient::NotResponding.new(nil,
|
106
|
+
e = RightScale::BalancedHttpClient::NotResponding.new(nil, RightScale::HttpExceptions.create(503))
|
107
107
|
@http_client.should_receive(:check_health).and_raise(e)
|
108
108
|
@log.should_receive(:error)
|
109
109
|
flexmock(@client).should_receive(:enable_use).never
|
@@ -291,9 +291,9 @@ describe RightScale::BaseRetryClient do
|
|
291
291
|
end
|
292
292
|
|
293
293
|
it "sets state to :disconnected and logs if server not responding" do
|
294
|
-
e = RightScale::BalancedHttpClient::NotResponding.new("not responding",
|
294
|
+
e = RightScale::BalancedHttpClient::NotResponding.new("not responding", RightScale::HttpExceptions.create(503))
|
295
295
|
@http_client.should_receive(:check_health).and_raise(e).once
|
296
|
-
@log.should_receive(:error).with("Failed test health check",
|
296
|
+
@log.should_receive(:error).with("Failed test health check", RightScale::HttpException).once
|
297
297
|
@client.send(:check_health).should == :disconnected
|
298
298
|
@client.state.should == :disconnected
|
299
299
|
end
|
@@ -490,7 +490,7 @@ describe RightScale::BaseRetryClient do
|
|
490
490
|
context "when redirect" do
|
491
491
|
[301, 302].each do |http_code|
|
492
492
|
it "handles #{http_code} redirect" do
|
493
|
-
e =
|
493
|
+
e = RightScale::HttpExceptions.create(http_code, "redirect")
|
494
494
|
flexmock(@client).should_receive(:handle_redirect).with(e, @type, @request_uuid).once
|
495
495
|
@client.send(:handle_exception, e, @type, @request_uuid, @now, 1)
|
496
496
|
end
|
@@ -498,27 +498,27 @@ describe RightScale::BaseRetryClient do
|
|
498
498
|
end
|
499
499
|
|
500
500
|
it "raises if unauthorized" do
|
501
|
-
e =
|
501
|
+
e = RightScale::HttpExceptions.create(401, "unauthorized")
|
502
502
|
lambda { @client.send(:handle_exception, e, @type, @request_uuid, @now, 1) }.should \
|
503
503
|
raise_error(RightScale::Exceptions::Unauthorized, "unauthorized")
|
504
504
|
end
|
505
505
|
|
506
506
|
it "notifies auth client and raises retryable if session expired" do
|
507
|
-
e =
|
507
|
+
e = RightScale::HttpExceptions.create(403, "forbidden")
|
508
508
|
lambda { @client.send(:handle_exception, e, @type, @request_uuid, @now, 1) }.should \
|
509
509
|
raise_error(RightScale::Exceptions::RetryableError, "Authorization expired")
|
510
510
|
@auth_client.expired_called.should be_true
|
511
511
|
end
|
512
512
|
|
513
513
|
it "handles retry with and updates request_uuid to distinguish for retry" do
|
514
|
-
e =
|
514
|
+
e = RightScale::HttpExceptions.create(449, "retry with")
|
515
515
|
flexmock(@client).should_receive(:handle_retry_with).with(e, @type, @request_uuid, @now, 1).
|
516
516
|
and_return("modified uuid").once
|
517
517
|
@client.send(:handle_exception, e, @type, @request_uuid, @now, 1).should == "modified uuid"
|
518
518
|
end
|
519
519
|
|
520
520
|
it "handles internal server error" do
|
521
|
-
e =
|
521
|
+
e = RightScale::HttpExceptions.create(500, "test internal error")
|
522
522
|
lambda { @client.send(:handle_exception, e, @type, @request_uuid, @now, 1) }.should \
|
523
523
|
raise_error(RightScale::Exceptions::InternalServerError, "test internal error")
|
524
524
|
end
|
@@ -530,7 +530,7 @@ describe RightScale::BaseRetryClient do
|
|
530
530
|
end
|
531
531
|
|
532
532
|
it "causes other HTTP exceptions to be re-raised by returning nil" do
|
533
|
-
e =
|
533
|
+
e = RightScale::HttpExceptions.create(400, "bad request")
|
534
534
|
@client.send(:handle_exception, e, @type, @request_uuid, @now, 1).should be_nil
|
535
535
|
end
|
536
536
|
|
@@ -542,7 +542,7 @@ describe RightScale::BaseRetryClient do
|
|
542
542
|
context :handle_redirect do
|
543
543
|
it "initiates redirect by notifying auth client and raising retryable error" do
|
544
544
|
location = "http://somewhere.com"
|
545
|
-
e =
|
545
|
+
e = RightScale::HttpExceptions.create(301, "moved permanently", {:location => location})
|
546
546
|
@log.should_receive(:info).with(/Received REDIRECT/).once.ordered
|
547
547
|
@log.should_receive(:info).with("Requesting auth client to handle redirect to #{location.inspect}").once.ordered
|
548
548
|
lambda { @client.send(:handle_redirect, e, @type, @request_uuid) }.should \
|
@@ -551,7 +551,7 @@ describe RightScale::BaseRetryClient do
|
|
551
551
|
end
|
552
552
|
|
553
553
|
it "raises internal error if no redirect location is provided" do
|
554
|
-
e =
|
554
|
+
e = RightScale::HttpExceptions.create(301, "moved permanently")
|
555
555
|
@log.should_receive(:info).with(/Received REDIRECT/).once
|
556
556
|
lambda { @client.send(:handle_redirect, e, @type, @request_uuid) }.should \
|
557
557
|
raise_error(RightScale::Exceptions::InternalServerError, "No redirect location provided")
|
@@ -560,10 +560,10 @@ describe RightScale::BaseRetryClient do
|
|
560
560
|
|
561
561
|
context :handle_retry_with do
|
562
562
|
before(:each) do
|
563
|
-
@exception =
|
563
|
+
@exception = RightScale::HttpExceptions.create(449, "retry with")
|
564
564
|
end
|
565
565
|
|
566
|
-
it "
|
566
|
+
it "waits for configured interval and does not raise if retry still viable" do
|
567
567
|
@log.should_receive(:error).with(/Retrying type request/).once
|
568
568
|
flexmock(@client).should_receive(:sleep).with(4).once
|
569
569
|
@client.send(:handle_retry_with, @exception, @type, @request_uuid, @now, 1)
|
@@ -598,13 +598,13 @@ describe RightScale::BaseRetryClient do
|
|
598
598
|
@exception = RightScale::BalancedHttpClient::NotResponding.new("Server not responding")
|
599
599
|
end
|
600
600
|
|
601
|
-
it "
|
601
|
+
it "waits for configured interval and does not raise if retry still viable" do
|
602
602
|
@log.should_receive(:error).with(/Retrying type request/).once
|
603
603
|
flexmock(@client).should_receive(:sleep).with(4).once
|
604
604
|
@client.send(:handle_not_responding, @exception, @type, @request_uuid, @now, 1)
|
605
605
|
end
|
606
606
|
|
607
|
-
it "changes
|
607
|
+
it "changes wait interval for successive retries" do
|
608
608
|
@log.should_receive(:error).with(/Retrying type request/).once
|
609
609
|
flexmock(@client).should_receive(:sleep).with(12).once
|
610
610
|
@client.send(:handle_not_responding, @exception, @type, @request_uuid, @now, 2)
|
@@ -636,4 +636,20 @@ describe RightScale::BaseRetryClient do
|
|
636
636
|
end
|
637
637
|
end
|
638
638
|
end
|
639
|
+
|
640
|
+
context :wait do
|
641
|
+
it "waits using timer if non-blocking enabled" do
|
642
|
+
@fiber = flexmock("fiber", :resume => true).by_default
|
643
|
+
flexmock(Fiber).should_receive(:current).and_return(@fiber)
|
644
|
+
flexmock(Fiber).should_receive(:yield).once
|
645
|
+
flexmock(EM).should_receive(:add_timer).with(1, Proc).and_yield.once
|
646
|
+
@client.init(:test, @auth_client, @options.merge(:non_blocking => true))
|
647
|
+
@client.send(:wait, 1).should be true
|
648
|
+
end
|
649
|
+
|
650
|
+
it " waits using sleep if non-blocking disabled" do
|
651
|
+
flexmock(@client).should_receive(:sleep).with(1).once
|
652
|
+
@client.send(:wait, 1).should be true
|
653
|
+
end
|
654
|
+
end
|
639
655
|
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013-2014 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
25
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'right_agent', 'clients', 'blocking_client'))
|
26
|
+
|
27
|
+
describe RightScale::BlockingClient do
|
28
|
+
|
29
|
+
include FlexMock::ArgumentTypes
|
30
|
+
|
31
|
+
before(:each) do
|
32
|
+
@log = flexmock(RightScale::Log)
|
33
|
+
@log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
|
34
|
+
@log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
|
35
|
+
@url = "http://my.com"
|
36
|
+
@urls = [@url]
|
37
|
+
@host = @url
|
38
|
+
@path = "/foo/bar"
|
39
|
+
@balancer = flexmock("balancer")
|
40
|
+
flexmock(RightSupport::Net::RequestBalancer).should_receive(:new).and_return(@balancer).by_default
|
41
|
+
@later = (@now = Time.now)
|
42
|
+
flexmock(Time).should_receive(:now).and_return { @later += 1 }
|
43
|
+
end
|
44
|
+
|
45
|
+
context :initialize do
|
46
|
+
before(:each) do
|
47
|
+
@options = {}
|
48
|
+
@client = RightScale::BlockingClient.new(@options)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "does not initialize use of proxy if not defined in environment" do
|
52
|
+
flexmock(RestClient).should_receive(:proxy=).never
|
53
|
+
RightScale::BlockingClient.new(@options)
|
54
|
+
end
|
55
|
+
|
56
|
+
['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY'].each do |proxy|
|
57
|
+
context "when #{proxy} defined in environment" do
|
58
|
+
after(:each) do
|
59
|
+
ENV.delete(proxy)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "initializes use of proxy" do
|
63
|
+
ENV[proxy] = "https://my.proxy.com"
|
64
|
+
flexmock(RestClient).should_receive(:proxy=).with("https://my.proxy.com").once
|
65
|
+
RightScale::BlockingClient.new(@options)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "infers http scheme for proxy address" do
|
69
|
+
ENV[proxy] = "1.2.3.4"
|
70
|
+
flexmock(RestClient).should_receive(:proxy=).with("http://1.2.3.4").once
|
71
|
+
RightScale::BlockingClient.new(@options)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "constructs health check proc" do
|
77
|
+
@client.health_check_proc.should be_a Proc
|
78
|
+
end
|
79
|
+
|
80
|
+
context "health check proc" do
|
81
|
+
before(:each) do
|
82
|
+
@http_client = flexmock("http client")
|
83
|
+
flexmock(RightSupport::Net::HTTPClient).should_receive(:new).and_return(@http_client).by_default
|
84
|
+
end
|
85
|
+
|
86
|
+
it "removes user and password from URL when checking health" do
|
87
|
+
@url = "http://111:secret@me.com"
|
88
|
+
@client = RightScale::BlockingClient.new(@options.merge(:health_check_path => "/health-check-me"))
|
89
|
+
flexmock(@client).should_receive(:request).with(:get, "", "http://me.com/health-check-me", {}, Hash).once
|
90
|
+
@client.health_check_proc.call(@url)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "uses default path if none specified" do
|
94
|
+
@client = RightScale::BlockingClient.new(@options)
|
95
|
+
flexmock(@client).should_receive(:request).with(:get, "", "http://my.com/health-check", {}, Hash).once
|
96
|
+
@client.health_check_proc.call(@url)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "appends health check path to any existing path" do
|
100
|
+
@url = "http://my.com/foo"
|
101
|
+
@client = RightScale::BlockingClient.new(@options)
|
102
|
+
flexmock(@client).should_receive(:request).with(:get, "", "http://my.com/foo/health-check", {}, Hash).once
|
103
|
+
@client.health_check_proc.call(@url)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "uses fixed timeout values" do
|
107
|
+
@client = RightScale::BlockingClient.new(:open_timeout => 5, :request_timeout => 30)
|
108
|
+
flexmock(@client).should_receive(:request).with(:get, "", String, {}, {:open_timeout => 2, :timeout => 5}).once
|
109
|
+
@client.health_check_proc.call(@url)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "sets API version if specified" do
|
113
|
+
@client = RightScale::BlockingClient.new(:api_version => "2.0")
|
114
|
+
flexmock(@client).should_receive(:request).with(:get, "", String, {}, hsh(:headers => {"X-API-Version" => "2.0"})).once
|
115
|
+
@client.health_check_proc.call(@url)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context :options do
|
121
|
+
before(:each) do
|
122
|
+
@options = {}
|
123
|
+
@verb = :get
|
124
|
+
@params = {}
|
125
|
+
@headers = {}
|
126
|
+
@client = RightScale::BlockingClient.new(@options)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "sets default open and request timeouts" do
|
130
|
+
connect_options, request_options = @client.options(@verb, @path, @params, @headers, @options)
|
131
|
+
connect_options.should == {}
|
132
|
+
request_options[:open_timeout].should == RightScale::BalancedHttpClient::DEFAULT_OPEN_TIMEOUT
|
133
|
+
request_options[:timeout].should == RightScale::BalancedHttpClient::DEFAULT_REQUEST_TIMEOUT
|
134
|
+
end
|
135
|
+
|
136
|
+
it "sets specified open and request timeouts" do
|
137
|
+
@options = {:open_timeout => 1, :request_timeout => 2}
|
138
|
+
_, request_options = @client.options(@verb, @path, @params, @headers, @options)
|
139
|
+
request_options[:open_timeout].should == 1
|
140
|
+
request_options[:timeout].should == 2
|
141
|
+
end
|
142
|
+
|
143
|
+
it "sets request headers" do
|
144
|
+
@headers = {:some => "header"}
|
145
|
+
_, request_options = @client.options(@verb, @path, @params, @headers, @options)
|
146
|
+
request_options[:headers].should == @headers
|
147
|
+
end
|
148
|
+
|
149
|
+
[:get, :delete].each do |verb|
|
150
|
+
context "with #{verb.inspect}" do
|
151
|
+
it "uses form-encoded query option for parameters" do
|
152
|
+
@params = {:some => "data", :more => ["a", "b"]}
|
153
|
+
_, request_options = @client.options(verb, @path, @params, @headers, @options)
|
154
|
+
request_options[:query].should == "?some=data&more[]=a&more[]=b"
|
155
|
+
request_options[:payload].should be nil
|
156
|
+
end
|
157
|
+
|
158
|
+
it "omits query option if there are no parameters" do
|
159
|
+
_, request_options = @client.options(verb, @path, @params, @headers, @options)
|
160
|
+
request_options.has_key?(:query).should be false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
[:post, :put].each do |verb|
|
166
|
+
context "with #{verb.inspect}" do
|
167
|
+
it "uses JSON-encoded payload options for parameters" do
|
168
|
+
@params = {:some => "data"}
|
169
|
+
_, request_options = @client.options(verb, @path, @params, @headers, @options)
|
170
|
+
request_options[:query].should be_nil
|
171
|
+
request_options[:payload].should == JSON.dump(@params)
|
172
|
+
request_options[:headers][:content_type].should == "application/json"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context "requesting" do
|
179
|
+
before(:each) do
|
180
|
+
@options = {}
|
181
|
+
@query = "?some=data&more[]=a&more[]=b"
|
182
|
+
@connect_options = {}
|
183
|
+
@request_options = {:open_timeout => 2, :request_timeout => 5, :headers => {:accept => "application/json"}, :query => @query}
|
184
|
+
@result = {"out" => 123}
|
185
|
+
@body = JSON.dump({:out => 123})
|
186
|
+
@headers = {:status => "200 OK"}
|
187
|
+
@response = flexmock("response", :code => 200, :body => @body, :headers => @headers).by_default
|
188
|
+
@http_client = flexmock("http client")
|
189
|
+
flexmock(RightSupport::Net::HTTPClient).should_receive(:new).and_return(@http_client).by_default
|
190
|
+
@client = RightScale::BlockingClient.new(@options)
|
191
|
+
end
|
192
|
+
|
193
|
+
context :request do
|
194
|
+
it "makes request" do
|
195
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@response).once
|
196
|
+
@client.request(:get, @path, @host, @connect_options, @request_options)
|
197
|
+
@request_options[:query].should be nil
|
198
|
+
end
|
199
|
+
|
200
|
+
it "processes response and returns result plus response code, body, and headers" do
|
201
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@response).once
|
202
|
+
result = @client.request(:get, @path, @host, @connect_options, @request_options)
|
203
|
+
result.should == [@result, 200, @body, @headers]
|
204
|
+
end
|
205
|
+
|
206
|
+
it "returns nil if response is nil" do
|
207
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(nil).once
|
208
|
+
result = @client.request(:get, @path, @host, @connect_options, @request_options)
|
209
|
+
result.should == [nil, nil, nil, nil]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
context :poll do
|
214
|
+
before(:each) do
|
215
|
+
@stop_at = @later + 10
|
216
|
+
@connection = {:host => @host, :path => @path, :expires_at => @later}
|
217
|
+
@nil_response = flexmock("nil response", :code => 200, :body => "null", :headers => @headers).by_default
|
218
|
+
end
|
219
|
+
|
220
|
+
it "uses existing connection info to form url" do
|
221
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@response).once
|
222
|
+
@client.poll(@connection, @request_options, @stop_at)
|
223
|
+
end
|
224
|
+
|
225
|
+
it "makes request" do
|
226
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@response).once
|
227
|
+
result = @client.poll(@connection, @request_options, @stop_at)
|
228
|
+
result.should == [@result, 200, @body, @headers]
|
229
|
+
end
|
230
|
+
|
231
|
+
it "makes at least one request regardless of stop time" do
|
232
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@nil_response).once
|
233
|
+
result = @client.poll(@connection, @request_options, @now - 10)
|
234
|
+
result.should == [nil, 200, "null", @headers]
|
235
|
+
end
|
236
|
+
|
237
|
+
it "stops polling if there is a non-nil result" do
|
238
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@nil_response).once.ordered
|
239
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@response).once.ordered
|
240
|
+
result = @client.poll(@connection, @request_options, @stop_at)
|
241
|
+
result.should == [@result, 200, @body, @headers]
|
242
|
+
end
|
243
|
+
|
244
|
+
it "stops polling if there is a nil result but it is time to stop" do
|
245
|
+
@http_client.should_receive(:get).with(@host + @path + @query, @request_options).and_return(@nil_response).twice
|
246
|
+
result = @client.poll(@connection, @request_options, @later + 2)
|
247
|
+
result.should == [nil, 200, "null", @headers]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context :request_once do
|
252
|
+
it "makes request and returns results" do
|
253
|
+
@http_client.should_receive(:get).with(@url, @request_options).and_return(@response).once
|
254
|
+
result = @client.send(:request_once, :get, @url, @request_options)
|
255
|
+
result.should == [@result, 200, @body, @headers]
|
256
|
+
end
|
257
|
+
|
258
|
+
it "returns nil if response is nil" do
|
259
|
+
@http_client.should_receive(:get).with(@url, @request_options).and_return(nil).once
|
260
|
+
result = @client.send(:request_once, :get, @url, @request_options)
|
261
|
+
result.should == [nil, nil, nil, nil]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,387 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013-2014 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
25
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'right_agent', 'clients', 'non_blocking_client'))
|
26
|
+
|
27
|
+
describe RightScale::NonBlockingClient do
|
28
|
+
|
29
|
+
include FlexMock::ArgumentTypes
|
30
|
+
|
31
|
+
before(:each) do
|
32
|
+
@log = flexmock(RightScale::Log)
|
33
|
+
@log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
|
34
|
+
@log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
|
35
|
+
@url = "http://my.com"
|
36
|
+
@urls = [@url]
|
37
|
+
@host = @url
|
38
|
+
@path = "/foo/bar"
|
39
|
+
@balancer = flexmock("balancer")
|
40
|
+
flexmock(RightSupport::Net::RequestBalancer).should_receive(:new).and_return(@balancer).by_default
|
41
|
+
@later = (@now = Time.now)
|
42
|
+
flexmock(Time).should_receive(:now).and_return { @later += 1 }
|
43
|
+
end
|
44
|
+
|
45
|
+
context :initialize do
|
46
|
+
before(:each) do
|
47
|
+
@options = {}
|
48
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "does not initialize use of proxy if not defined in environment" do
|
52
|
+
@client.instance_variable_get(:@proxy).should be nil
|
53
|
+
end
|
54
|
+
|
55
|
+
['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY'].each do |proxy|
|
56
|
+
context "when #{proxy} defined in environment" do
|
57
|
+
after(:each) do
|
58
|
+
ENV.delete(proxy)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "initializes use of proxy" do
|
62
|
+
ENV[proxy] = "https://my.proxy.com"
|
63
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
64
|
+
@client.instance_variable_get(:@proxy).should == {:host => "my.proxy.com", :port => 443}
|
65
|
+
end
|
66
|
+
|
67
|
+
it "infers http scheme for proxy address" do
|
68
|
+
ENV[proxy] = "1.2.3.4"
|
69
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
70
|
+
@client.instance_variable_get(:@proxy).should == {:host => "1.2.3.4", :port => 80}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "applies user and password to proxy address if defined in proxy address" do
|
74
|
+
ENV[proxy] = "https://111:secret@my.proxy.com"
|
75
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
76
|
+
@client.instance_variable_get(:@proxy).should == {:host => "my.proxy.com", :port => 443, :authorization => ["111", "secret"]}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "constructs health check proc" do
|
82
|
+
@client.health_check_proc.should be_a Proc
|
83
|
+
end
|
84
|
+
|
85
|
+
context "health check proc" do
|
86
|
+
it "removes user and password from URL when checking health" do
|
87
|
+
@url = "http://111:secret@me.com"
|
88
|
+
@client = RightScale::NonBlockingClient.new(@options.merge(:health_check_path => "/health-check-me"))
|
89
|
+
flexmock(@client).should_receive(:request).with(:get, "", "http://me.com", Hash, {:path => "/health-check-me"}).once
|
90
|
+
@client.health_check_proc.call(@url)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "uses default path if none specified" do
|
94
|
+
flexmock(@client).should_receive(:request).with(:get, "", @host, Hash, {:path => "/health-check"}).once
|
95
|
+
@client.health_check_proc.call(@url)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "appends health check path to any existing path" do
|
99
|
+
@url = "http://my.com/foo"
|
100
|
+
flexmock(@client).should_receive(:request).with(:get, "", @host, Hash, {:path => "/foo/health-check"}).once
|
101
|
+
@client.health_check_proc.call(@url)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "uses fixed timeout values" do
|
105
|
+
@client = RightScale::NonBlockingClient.new(@options.merge(:open_timeout => 5, :request_timeout => 30))
|
106
|
+
connect_options = {:connect_timeout => 2, :inactivity_timeout => 5}
|
107
|
+
flexmock(@client).should_receive(:request).with(:get, "", @host, connect_options, Hash).once
|
108
|
+
@client.health_check_proc.call(@url)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "sets API version if specified" do
|
112
|
+
@client = RightScale::NonBlockingClient.new(@options.merge(:api_version => "2.0"))
|
113
|
+
request_options = {:path => "/health-check", :head => {"X-API-Version" => "2.0"}}
|
114
|
+
flexmock(@client).should_receive(:request).with(:get, "", @host, Hash, request_options).once
|
115
|
+
@client.health_check_proc.call(@url)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "uses proxy if defined" do
|
119
|
+
ENV["HTTPS_PROXY"] = "https://my.proxy.com"
|
120
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
121
|
+
connect_options = {:connect_timeout => 2, :inactivity_timeout => 5, :proxy => {:host => "my.proxy.com", :port => 443}}
|
122
|
+
flexmock(@client).should_receive(:request).with(:get, "", @host, connect_options, Hash).once
|
123
|
+
@client.health_check_proc.call(@url)
|
124
|
+
ENV.delete("HTTPS_PROXY")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context :options do
|
130
|
+
before(:each) do
|
131
|
+
@options = {}
|
132
|
+
@verb = :get
|
133
|
+
@params = {}
|
134
|
+
@headers = {}
|
135
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
136
|
+
end
|
137
|
+
|
138
|
+
it "sets default open and request timeouts" do
|
139
|
+
connect_options, _ = @client.options(@verb, @path, @params, @headers, @options)
|
140
|
+
connect_options[:connect_timeout].should == RightScale::BalancedHttpClient::DEFAULT_OPEN_TIMEOUT
|
141
|
+
connect_options[:inactivity_timeout].should == RightScale::BalancedHttpClient::DEFAULT_REQUEST_TIMEOUT
|
142
|
+
end
|
143
|
+
|
144
|
+
it "sets specified open and request timeouts" do
|
145
|
+
@options = {:open_timeout => 1, :request_timeout => 2}
|
146
|
+
connect_options, _ = @client.options(@verb, @path, @params, @headers, @options)
|
147
|
+
connect_options[:connect_timeout].should == 1
|
148
|
+
connect_options[:inactivity_timeout].should == 2
|
149
|
+
end
|
150
|
+
|
151
|
+
it "uses poll timeout instead of request timeout if defined" do
|
152
|
+
@options = {:open_timeout => 1, :poll_timeout=> 2, :request_timeout => 3}
|
153
|
+
connect_options, _ = @client.options(:poll, @path, @params, @headers, @options)
|
154
|
+
connect_options[:connect_timeout].should == 1
|
155
|
+
connect_options[:inactivity_timeout].should == 2
|
156
|
+
end
|
157
|
+
|
158
|
+
it "does not uses poll timeout if defined but not polling" do
|
159
|
+
@options = {:open_timeout => 1, :poll_timeout=> 2, :request_timeout => 3}
|
160
|
+
connect_options, _ = @client.options(@verb, @path, @params, @headers, @options)
|
161
|
+
connect_options[:connect_timeout].should == 1
|
162
|
+
connect_options[:inactivity_timeout].should == 3
|
163
|
+
end
|
164
|
+
|
165
|
+
it "sets :keepalive if this is a poll request" do
|
166
|
+
_, request_options = @client.options(:poll, @path, @params, @headers, @options)
|
167
|
+
request_options[:keepalive].should be true
|
168
|
+
end
|
169
|
+
|
170
|
+
it "sets request headers" do
|
171
|
+
@headers = {:some => "header"}
|
172
|
+
_, request_options = @client.options(@verb, @path, @params, @headers, @options)
|
173
|
+
request_options[:head].should == @headers
|
174
|
+
end
|
175
|
+
|
176
|
+
it "sets :proxy if defined" do
|
177
|
+
ENV["HTTPS_PROXY"] = "https://my.proxy.com"
|
178
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
179
|
+
connect_options, _ = @client.options(@verb, @path, @params, @headers, @options)
|
180
|
+
connect_options[:proxy].should == {:host => "my.proxy.com", :port => 443}
|
181
|
+
ENV.delete("HTTPS_PROXY")
|
182
|
+
end
|
183
|
+
|
184
|
+
[:get, :delete].each do |verb|
|
185
|
+
context "with #{verb.inspect}" do
|
186
|
+
it "appends form-encoded query option to path" do
|
187
|
+
@params = {:some => "data"}
|
188
|
+
_, request_options = @client.options(verb, @path, @params, @headers, @options)
|
189
|
+
request_options[:path].should == "/foo/bar?some=data"
|
190
|
+
request_options[:body].should be nil
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
[:post, :put].each do |verb|
|
196
|
+
context "with #{verb.inspect}" do
|
197
|
+
it "stores JSON-encoded parameters in body" do
|
198
|
+
@params = {:some => "data"}
|
199
|
+
_, request_options = @client.options(verb, @path, @params, @headers, @options)
|
200
|
+
request_options[:path].should == @path
|
201
|
+
request_options[:body].should == JSON.dump(@params)
|
202
|
+
request_options[:head][:content_type].should == "application/json"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
context "requesting" do
|
209
|
+
before(:each) do
|
210
|
+
@options = {}
|
211
|
+
@connect_options = {:connect_timeout => 2, :inactivity_timeout => 5}
|
212
|
+
@request_options = {:path => @path, :head => {:accept => "application/json"}}
|
213
|
+
@result = {"out" => 123}
|
214
|
+
@body = JSON.dump({:out => 123})
|
215
|
+
@headers = EventMachine::HttpResponseHeader.new
|
216
|
+
@headers.http_status = 200
|
217
|
+
@headers["STATUS"] = "200 OK"
|
218
|
+
@beautified_headers = {:status => "200 OK"}
|
219
|
+
@response = flexmock("em http response", :response_header => @headers, :response => @body,
|
220
|
+
:error => nil, :errback => true, :callback => true).by_default
|
221
|
+
@request = flexmock("em http request", :get => @response).by_default
|
222
|
+
flexmock(EM::HttpRequest).should_receive(:new).and_return(@request).by_default
|
223
|
+
@fiber = flexmock("fiber", :resume => true).by_default
|
224
|
+
flexmock(Fiber).should_receive(:current).and_return(@fiber)
|
225
|
+
flexmock(Fiber).should_receive(:yield).and_return([200, @body, @headers]).by_default
|
226
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
227
|
+
end
|
228
|
+
|
229
|
+
context :request do
|
230
|
+
it "makes request" do
|
231
|
+
@response.should_receive(:callback).and_yield.once
|
232
|
+
@fiber.should_receive(:resume).with(200, @body, @headers).once
|
233
|
+
flexmock(EM::HttpRequest).should_receive(:new).with(@host, @connect_options).and_return(@request).once
|
234
|
+
@request.should_receive(:get).with(@request_options).and_return(@response).once
|
235
|
+
@client.request(:get, @path, @host, @connect_options, @request_options)
|
236
|
+
@client.connections[@path].should be nil
|
237
|
+
end
|
238
|
+
|
239
|
+
it "processes response and returns result plus response code, body, and headers" do
|
240
|
+
@response.should_receive(:callback).and_yield.once
|
241
|
+
@fiber.should_receive(:resume).with(200, @body, @headers).once
|
242
|
+
flexmock(EM::HttpRequest).should_receive(:new).with(@host, @connect_options).and_return(@request).once
|
243
|
+
result = @client.request(:get, @path, @host, @connect_options, @request_options)
|
244
|
+
result.should == [@result, 200, @body, @beautified_headers]
|
245
|
+
end
|
246
|
+
|
247
|
+
it "handles host value that has path" do
|
248
|
+
@response.should_receive(:callback).and_yield.once
|
249
|
+
@fiber.should_receive(:resume).with(200, @body, @headers).once
|
250
|
+
flexmock(EM::HttpRequest).should_receive(:new).with(@host, @connect_options).and_return(@request).once
|
251
|
+
@request.should_receive(:get).with(on { |a| a[:path] == "/api" + @path }).and_return(@response).once
|
252
|
+
@client.request(:get, @path, @host + "/api", @connect_options, @request_options)
|
253
|
+
@request_options[:path].should == "/api/foo/bar"
|
254
|
+
end
|
255
|
+
|
256
|
+
it "converts Errno::ETIMEDOUT error to 504" do
|
257
|
+
@headers.http_status = 504
|
258
|
+
@response.should_receive(:errback).and_yield.once
|
259
|
+
@response.should_receive(:error).and_return("Errno::ETIMEDOUT")
|
260
|
+
@fiber.should_receive(:resume).with(504, "Errno::ETIMEDOUT").once
|
261
|
+
flexmock(Fiber).should_receive(:yield).and_return([504, "Errno::ETIMEDOUT"]).once
|
262
|
+
flexmock(EM::HttpRequest).should_receive(:new).with(@host, @connect_options).and_return(@request).once
|
263
|
+
lambda { @client.request(:get, @path, @host, @connect_options, @request_options) }.
|
264
|
+
should raise_error(RightScale::HttpExceptions::GatewayTimeout)
|
265
|
+
end
|
266
|
+
|
267
|
+
it "stores the connection if keepalive enabled" do
|
268
|
+
@request_options.merge!(:keepalive => true)
|
269
|
+
flexmock(EM::HttpRequest).should_receive(:new).and_return(@request).once
|
270
|
+
@client.request(:get, @path, @host, @connect_options, @request_options)
|
271
|
+
@client.connections[@path].should == {:host => @host, :connection => @request, :expires_at => @later + 5}
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
context "polling" do
|
276
|
+
before(:each) do
|
277
|
+
@stop_at = @later + 10
|
278
|
+
@request_options.merge!(:keepalive => true)
|
279
|
+
@connection = {:host => @host, :connection => @request, :expires_at => @later}
|
280
|
+
end
|
281
|
+
|
282
|
+
context :poll do
|
283
|
+
before(:each) do
|
284
|
+
flexmock(Fiber).should_receive(:yield).and_return([200, @body, @headers]).once
|
285
|
+
end
|
286
|
+
|
287
|
+
it "polls using specified connection and returns result data" do
|
288
|
+
@request.should_receive(:get).with(@request_options).and_return(@response).once
|
289
|
+
result = @client.poll(@connection, @request_options, @stop_at)
|
290
|
+
result.should be_a Array
|
291
|
+
result[1].should == 200
|
292
|
+
end
|
293
|
+
|
294
|
+
it "handles host value that has path" do
|
295
|
+
@connection[:host] += "/api"
|
296
|
+
@request.should_receive(:get).with(on { |a| a[:path] == "/api" + @path }).and_return(@response).once
|
297
|
+
@client.poll(@connection, @request_options, @stop_at)
|
298
|
+
end
|
299
|
+
|
300
|
+
it "beautifies response headers" do
|
301
|
+
_, _, _, headers = @client.poll(@connection, @request_options, @stop_at)
|
302
|
+
headers.should == @beautified_headers
|
303
|
+
end
|
304
|
+
|
305
|
+
it "processes the response to get the result" do
|
306
|
+
result, _, _, _ = @client.poll(@connection, @request_options, @stop_at)
|
307
|
+
result.should == @result
|
308
|
+
end
|
309
|
+
|
310
|
+
it "updates the timeout in the connection" do
|
311
|
+
@client.poll(@connection, @request_options, @stop_at)
|
312
|
+
@connection[:expires_at].should == @later + 5
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
context :poll_again do
|
317
|
+
it "makes HTTP get request using specified connection" do
|
318
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @stop_at).should be true
|
319
|
+
end
|
320
|
+
|
321
|
+
it "stops polling if there is a non-nil result" do
|
322
|
+
@response.should_receive(:callback).and_yield.once
|
323
|
+
@fiber.should_receive(:resume).with(200, @body, @headers).once
|
324
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @stop_at).should be true
|
325
|
+
end
|
326
|
+
|
327
|
+
it "stops polling if the status code is not 200" do
|
328
|
+
@headers.http_status = 204
|
329
|
+
@response.should_receive(:callback).and_yield.once
|
330
|
+
@response.should_receive(:response).and_return(nil).once
|
331
|
+
@fiber.should_receive(:resume).with(204, nil, @headers).once
|
332
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @stop_at).should be true
|
333
|
+
end
|
334
|
+
|
335
|
+
it "stops polling if it is time to" do
|
336
|
+
@response.should_receive(:callback).and_yield.once
|
337
|
+
@response.should_receive(:response).and_return(nil).once
|
338
|
+
@fiber.should_receive(:resume).with(200, nil, @headers).once
|
339
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @later + 1).should be true
|
340
|
+
end
|
341
|
+
|
342
|
+
it "stops polling if there is an error" do
|
343
|
+
@response.should_receive(:error).and_return("some error").twice
|
344
|
+
@response.should_receive(:errback).and_yield.once
|
345
|
+
@fiber.should_receive(:resume).with(500, "some error").once
|
346
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @stop_at).should be true
|
347
|
+
end
|
348
|
+
|
349
|
+
it "otherwise polls again" do
|
350
|
+
@response.should_receive(:callback).and_yield.twice
|
351
|
+
@response.should_receive(:response).and_return(nil).once.ordered
|
352
|
+
@response.should_receive(:response).and_return(@body).once.ordered
|
353
|
+
@fiber.should_receive(:resume).once
|
354
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @later + 2).should be true
|
355
|
+
end
|
356
|
+
|
357
|
+
it "polls again for JSON-encoded nil result" do
|
358
|
+
@response.should_receive(:callback).and_yield.twice
|
359
|
+
@response.should_receive(:response).and_return("null").once.ordered
|
360
|
+
@response.should_receive(:response).and_return(@body).once.ordered
|
361
|
+
@fiber.should_receive(:resume).once
|
362
|
+
@client.send(:poll_again, @fiber, @request, @request_options, @later + 2).should be true
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
context :beautify_headers do
|
369
|
+
before(:each) do
|
370
|
+
@client = RightScale::NonBlockingClient.new(@options)
|
371
|
+
end
|
372
|
+
|
373
|
+
it "beautifies header keys" do
|
374
|
+
headers = {"SERVER" => "nginx/1.4.2", "DATE" => "Tue, 25 Mar 2014 22:19:52 GMT",
|
375
|
+
"CONTENT_TYPE" => "application/json; charset=utf-8", "CONTENT_LENGTH" => "4", "CONNECTION" => "close",
|
376
|
+
"STATUS" => "200 OK", "CACHE_CONTROL" => "private, max-age=0, must-revalidate", "X_RUNTIME" => "1", "SET_COOKIE" => ""}
|
377
|
+
result = @client.send(:beautify_headers, headers)
|
378
|
+
result.should == {:server => "nginx/1.4.2", :date => "Tue, 25 Mar 2014 22:19:52 GMT",
|
379
|
+
:content_type => "application/json; charset=utf-8", :content_length => "4", :connection => "close",
|
380
|
+
:status => "200 OK", :cache_control => "private, max-age=0, must-revalidate", :x_runtime => "1", :set_cookie => ""}
|
381
|
+
end
|
382
|
+
|
383
|
+
it "converts hyphenations to underscores" do
|
384
|
+
@client.send(:beautify_headers, {"NON-BLOCKING" => "yes"}).should == {:non_blocking => "yes"}
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|