right_agent 2.0.8-x86-mingw32 → 2.1.0-x86-mingw32
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/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
|