right_agent 2.0.8 → 2.1.0
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
@@ -73,6 +73,8 @@ module RightScale
|
|
73
73
|
# @option options [Array] :retry_intervals between successive retries; defaults to DEFAULT_RETRY_INTERVALS
|
74
74
|
# @option options [Boolean] :retry_enabled for requests that fail to connect or that return a retry result
|
75
75
|
# @option options [Numeric] :reconnect_interval for reconnect attempts after lose connectivity
|
76
|
+
# @option options [Boolean] :non_blocking i/o is to be used for HTTP requests by applying
|
77
|
+
# EM::HttpRequest and fibers instead of RestClient; requests remain synchronous
|
76
78
|
# @option options [Array] :filter_params symbols or strings for names of request parameters
|
77
79
|
# whose values are to be hidden when logging; can be augmented on individual requests
|
78
80
|
# @option options [Proc] :exception_callback for unexpected exceptions
|
@@ -224,15 +226,16 @@ module RightScale
|
|
224
226
|
#
|
225
227
|
# @return [TrueClass] always true
|
226
228
|
#
|
227
|
-
# @return [
|
229
|
+
# @return [BalancedHttpClient] client
|
228
230
|
def create_http_client
|
229
231
|
url = @auth_client.send(@type.to_s + "_url")
|
230
232
|
Log.info("Connecting to #{@options[:server_name]} via #{url.inspect}")
|
231
233
|
options = {
|
232
234
|
:server_name => @options[:server_name],
|
233
235
|
:open_timeout => @options[:open_timeout],
|
234
|
-
:request_timeout => @options[:request_timeout] }
|
236
|
+
:request_timeout => @options[:request_timeout], }
|
235
237
|
options[:api_version] = @options[:api_version] if @options[:api_version]
|
238
|
+
options[:non_blocking] = @options[:non_blocking] if @options[:non_blocking]
|
236
239
|
options[:filter_params] = @options[:filter_params] if @options[:filter_params]
|
237
240
|
@http_client = RightScale::BalancedHttpClient.new(url, options)
|
238
241
|
end
|
@@ -271,7 +274,7 @@ module RightScale
|
|
271
274
|
unless @reconnecting
|
272
275
|
@reconnecting = true
|
273
276
|
@stats["reconnects"].update("initiate")
|
274
|
-
@reconnect_timer =
|
277
|
+
@reconnect_timer = EM_S::PeriodicTimer.new(rand(@options[:reconnect_interval])) do
|
275
278
|
begin
|
276
279
|
create_http_client
|
277
280
|
if check_health == :connected
|
@@ -446,7 +449,7 @@ module RightScale
|
|
446
449
|
if attempts == 1 && interval && (Time.now - started_at) < @options[:retry_timeout]
|
447
450
|
Log.error("Retrying #{type} request <#{request_uuid}> in #{interval} seconds " +
|
448
451
|
"in response to retryable error (#{retry_result.http_body})")
|
449
|
-
|
452
|
+
wait(interval)
|
450
453
|
else
|
451
454
|
@stats["request failures"].update("#{type} - retry")
|
452
455
|
raise Exceptions::RetryableError.new(retry_result.http_body, retry_result)
|
@@ -462,7 +465,7 @@ module RightScale
|
|
462
465
|
# Handle not responding response by determining whether okay to retry
|
463
466
|
# If request is being retried, this function does not return until it is time to retry
|
464
467
|
#
|
465
|
-
# @param [
|
468
|
+
# @param [BalancedHttpClient::NotResponding] not_responding exception
|
466
469
|
# indicating targeted server is too busy or out of service
|
467
470
|
# @param [String] type of request for use in logging
|
468
471
|
# @param [String] request_uuid originally created for this request
|
@@ -479,7 +482,7 @@ module RightScale
|
|
479
482
|
if interval && (Time.now - started_at) < @options[:retry_timeout]
|
480
483
|
Log.error("Retrying #{type} request <#{request_uuid}> in #{interval} seconds " +
|
481
484
|
"in response to routing failure (#{BalancedHttpClient.exception_text(not_responding)})")
|
482
|
-
|
485
|
+
wait(interval)
|
483
486
|
else
|
484
487
|
@stats["request failures"].update("#{type} - no result")
|
485
488
|
self.state = :disconnected
|
@@ -493,6 +496,22 @@ module RightScale
|
|
493
496
|
true
|
494
497
|
end
|
495
498
|
|
499
|
+
# Wait the specified interval in non-blocking fashion if possible
|
500
|
+
#
|
501
|
+
# @param [Numeric] interval to wait
|
502
|
+
#
|
503
|
+
# @return [TrueClass] always true
|
504
|
+
def wait(interval)
|
505
|
+
if @options[:non_blocking]
|
506
|
+
fiber = Fiber.current
|
507
|
+
EM.add_timer(interval) { fiber.resume }
|
508
|
+
Fiber.yield
|
509
|
+
else
|
510
|
+
sleep(interval)
|
511
|
+
end
|
512
|
+
true
|
513
|
+
end
|
514
|
+
|
496
515
|
end # BaseRetryClient
|
497
516
|
|
498
517
|
end # RightScale
|
@@ -0,0 +1,155 @@
|
|
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 'restclient'
|
25
|
+
|
26
|
+
module RightScale
|
27
|
+
|
28
|
+
# Interface to HTTP using RightSupport::Net::HTTPClient and RestClient
|
29
|
+
# This interfaces blocks the given thread until an HTTP response is received
|
30
|
+
class BlockingClient
|
31
|
+
|
32
|
+
# Fully configured health check procedure for use with this client
|
33
|
+
attr_reader :health_check_proc
|
34
|
+
|
35
|
+
# Hash of active connections with request path as key and hash value containing
|
36
|
+
# :host and :expires_at
|
37
|
+
attr_reader :connections
|
38
|
+
|
39
|
+
# Initialize client
|
40
|
+
#
|
41
|
+
# @option options [String] :api_version for X-API-Version header
|
42
|
+
# @option options [String] :health_check_path in URI for health check resource;
|
43
|
+
# defaults to DEFAULT_HEALTH_CHECK_PATH
|
44
|
+
def initialize(options)
|
45
|
+
@connections = {}
|
46
|
+
|
47
|
+
# Initialize use of proxy if defined
|
48
|
+
if (v = BalancedHttpClient::PROXY_ENVIRONMENT_VARIABLES.detect { |v| ENV.has_key?(v) })
|
49
|
+
proxy_uri = ENV[v].match(/^[[:alpha:]]+:\/\//) ? URI.parse(ENV[v]) : URI.parse("http://" + ENV[v])
|
50
|
+
RestClient.proxy = proxy_uri.to_s if proxy_uri
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create health check proc for use by request balancer
|
54
|
+
# Strip user and password from host name since health-check does not require authorization
|
55
|
+
@health_check_proc = Proc.new do |host|
|
56
|
+
uri = URI.parse(host)
|
57
|
+
uri.user = uri.password = nil
|
58
|
+
uri.path = uri.path + (options[:health_check_path] || BalancedHttpClient::DEFAULT_HEALTH_CHECK_PATH)
|
59
|
+
request_options = {
|
60
|
+
:open_timeout => BalancedHttpClient::DEFAULT_OPEN_TIMEOUT,
|
61
|
+
:timeout => BalancedHttpClient::HEALTH_CHECK_TIMEOUT }
|
62
|
+
request_options[:headers] = {"X-API-Version" => options[:api_version]} if options[:api_version]
|
63
|
+
request(:get, "", uri.to_s, {}, request_options)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Construct options for HTTP request
|
68
|
+
#
|
69
|
+
# @param [Symbol] verb for HTTP REST request
|
70
|
+
# @param [String] path in URI for desired resource (ignored)
|
71
|
+
# @param [Hash] params for HTTP request
|
72
|
+
# @param [String] request_headers to be applied to request
|
73
|
+
#
|
74
|
+
# @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
|
75
|
+
# @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
|
76
|
+
# @option options [Numeric] :poll_timeout maximum wait for individual poll; defaults to :request_timeout
|
77
|
+
#
|
78
|
+
# @return [Array] connect and request option hashes
|
79
|
+
def options(verb, path, params, request_headers, options)
|
80
|
+
request_options = {
|
81
|
+
:open_timeout => options[:open_timeout] || BalancedHttpClient::DEFAULT_OPEN_TIMEOUT,
|
82
|
+
:timeout => options[:poll_timeout] || options[:request_timeout] || BalancedHttpClient::DEFAULT_REQUEST_TIMEOUT,
|
83
|
+
:headers => request_headers }
|
84
|
+
|
85
|
+
if [:get, :delete].include?(verb)
|
86
|
+
# Doing own formatting because :query option for HTTPClient uses addressable gem
|
87
|
+
# for conversion and that gem encodes arrays in a Rails-compatible fashion without []
|
88
|
+
# markers and that is inconsistent with what sinatra expects
|
89
|
+
request_options[:query] = "?#{BalancedHttpClient.format(params)}" if params.is_a?(Hash) && params.any?
|
90
|
+
else
|
91
|
+
request_options[:payload] = JSON.dump(params)
|
92
|
+
request_options[:headers][:content_type] = "application/json"
|
93
|
+
end
|
94
|
+
[{}, request_options]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Make HTTP request
|
98
|
+
#
|
99
|
+
# @param [Symbol] verb for HTTP REST request
|
100
|
+
# @param [String] path in URI for desired resource
|
101
|
+
# @param [String] host name of server
|
102
|
+
# @param [Hash] connect_options for HTTP connection (ignored)
|
103
|
+
# @param [Hash] request_options for HTTP request
|
104
|
+
#
|
105
|
+
# @return [Array] result to be returned followed by response code, body, and headers
|
106
|
+
#
|
107
|
+
# @raise [HttpException] HTTP failure with associated status code
|
108
|
+
def request(verb, path, host, connect_options, request_options)
|
109
|
+
url = host + path + request_options.delete(:query).to_s
|
110
|
+
result = request_once(verb, url, request_options)
|
111
|
+
@connections[path] = {:host => host, :path => path, :expires_at => Time.now + BalancedHttpClient::CONNECTION_REUSE_TIMEOUT }
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
115
|
+
# Make long-polling requests until receive data or timeout
|
116
|
+
#
|
117
|
+
# @param [Hash] connection to server from previous request with keys :host, :path,
|
118
|
+
# and :expires_at, with the :expires_at being adjusted on return
|
119
|
+
# @param [Hash] request_options for HTTP request
|
120
|
+
# @param [Time] stop_at time for polling
|
121
|
+
#
|
122
|
+
# @return [Array] result to be returned followed by response code, body, and headers
|
123
|
+
#
|
124
|
+
# @raise [HttpException] HTTP failure with associated status code
|
125
|
+
def poll(connection, request_options, stop_at)
|
126
|
+
url = connection[:host] + connection[:path] + request_options.delete(:query).to_s
|
127
|
+
begin
|
128
|
+
result, code, body, headers = request_once(:get, url, request_options)
|
129
|
+
end until result || Time.now >= stop_at
|
130
|
+
connection[:expires_at] = Time.now + BalancedHttpClient::CONNECTION_REUSE_TIMEOUT
|
131
|
+
[result, code, body, headers]
|
132
|
+
end
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
# Make HTTP request once
|
137
|
+
#
|
138
|
+
# @param [Symbol] verb for HTTP REST request
|
139
|
+
# @param [String] url for request
|
140
|
+
# @param [Hash] request_options for HTTP request
|
141
|
+
#
|
142
|
+
# @return [Array] result to be returned followed by response code, body, and headers
|
143
|
+
#
|
144
|
+
# @raise [HttpException] HTTP failure with associated status code
|
145
|
+
def request_once(verb, url, request_options)
|
146
|
+
if (r = RightSupport::Net::HTTPClient.new.send(verb, url, request_options))
|
147
|
+
[BalancedHttpClient.response(r.code, r.body, r.headers, request_options[:headers][:accept]), r.code, r.body, r.headers]
|
148
|
+
else
|
149
|
+
[nil, nil, nil, nil]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end # BlockingClient
|
154
|
+
|
155
|
+
end # RightScale
|
@@ -0,0 +1,198 @@
|
|
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
|
+
module RightScale
|
25
|
+
|
26
|
+
# Interface to HTTP using EM::HttpRequest
|
27
|
+
# This interface uses non-blocking i/o so that HTTP requests are synchronous
|
28
|
+
# to the caller but the underlying thread yields to other activity when blocked on i/o
|
29
|
+
class NonBlockingClient
|
30
|
+
|
31
|
+
# Fully configured health check procedure for use with this client
|
32
|
+
attr_reader :health_check_proc
|
33
|
+
|
34
|
+
# Hash of active connections with request path as key and hash value containing
|
35
|
+
# :host, :connection, and :expires_at
|
36
|
+
attr_reader :connections
|
37
|
+
|
38
|
+
# Initialize client
|
39
|
+
#
|
40
|
+
# @option options [String] :api_version for X-API-Version header
|
41
|
+
# @option options [String] :health_check_path in URI for health check resource;
|
42
|
+
# defaults to BalancedHttpClient::DEFAULT_HEALTH_CHECK_PATH
|
43
|
+
def initialize(options)
|
44
|
+
# Defer requiring this gem until now so that right_agent can be used with ruby 1.8.7
|
45
|
+
require 'em-http-request'
|
46
|
+
|
47
|
+
@connections = {}
|
48
|
+
|
49
|
+
# Initialize use of proxy if defined
|
50
|
+
if (v = BalancedHttpClient::PROXY_ENVIRONMENT_VARIABLES.detect { |v| ENV.has_key?(v) })
|
51
|
+
proxy_uri = ENV[v].match(/^[[:alpha:]]+:\/\//) ? URI.parse(ENV[v]) : URI.parse("http://" + ENV[v])
|
52
|
+
@proxy = {:host => proxy_uri.host, :port => proxy_uri.port}
|
53
|
+
@proxy[:authorization] = [proxy_uri.user, proxy_uri.password] if proxy_uri.user
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create health check proc for use by request balancer
|
57
|
+
# Strip user and password from host name since health-check does not require authorization
|
58
|
+
@health_check_proc = Proc.new do |host|
|
59
|
+
uri = URI.parse(host)
|
60
|
+
uri.user = uri.password = nil
|
61
|
+
uri.path = uri.path + (options[:health_check_path] || BalancedHttpClient::DEFAULT_HEALTH_CHECK_PATH)
|
62
|
+
connect_options = {
|
63
|
+
:connect_timeout => BalancedHttpClient::DEFAULT_OPEN_TIMEOUT,
|
64
|
+
:inactivity_timeout => BalancedHttpClient::HEALTH_CHECK_TIMEOUT }
|
65
|
+
connect_options[:proxy] = @proxy if @proxy
|
66
|
+
request_options = {:path => uri.path}
|
67
|
+
request_options[:head] = {"X-API-Version" => options[:api_version]} if options[:api_version]
|
68
|
+
uri.path = ""
|
69
|
+
request(:get, "", uri.to_s, connect_options, request_options)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Construct options for HTTP request
|
74
|
+
#
|
75
|
+
# @param [Symbol] verb for HTTP REST request
|
76
|
+
# @param [String] path in URI for desired resource
|
77
|
+
# @param [Hash] params for HTTP request
|
78
|
+
# @param [String] request_headers to be applied to request
|
79
|
+
#
|
80
|
+
# @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
|
81
|
+
# @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
|
82
|
+
# @option options [Numeric] :poll_timeout maximum wait for individual poll; defaults to :request_timeout
|
83
|
+
#
|
84
|
+
# @return [Array] connect and request option hashes
|
85
|
+
def options(verb, path, params, request_headers, options)
|
86
|
+
poll_timeout = verb == :poll && options[:poll_timeout]
|
87
|
+
connect_options = {
|
88
|
+
:connect_timeout => options[:open_timeout] || BalancedHttpClient::DEFAULT_OPEN_TIMEOUT,
|
89
|
+
:inactivity_timeout => poll_timeout || options[:request_timeout] || BalancedHttpClient::DEFAULT_REQUEST_TIMEOUT }
|
90
|
+
connect_options[:proxy] = @proxy if @proxy
|
91
|
+
|
92
|
+
request_body, request_path = if [:get, :delete].include?(verb)
|
93
|
+
# Doing own formatting because :query option on EM::HttpRequest does not reliably
|
94
|
+
# URL encode, e.g., messes up on arrays in hashes
|
95
|
+
[nil, (params.is_a?(Hash) && params.any?) ? path + "?#{BalancedHttpClient.format(params)}" : path]
|
96
|
+
else
|
97
|
+
request_headers[:content_type] = "application/json"
|
98
|
+
[(params.is_a?(Hash) && params.any?) ? JSON.dump(params) : nil, path]
|
99
|
+
end
|
100
|
+
request_options = {:path => request_path, :body => request_body, :head => request_headers}
|
101
|
+
request_options[:keepalive] = true if verb == :poll
|
102
|
+
[connect_options, request_options]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Make HTTP request
|
106
|
+
# Note that the underlying thread is not blocked by the HTTP i/o, but this call itself is blocking
|
107
|
+
#
|
108
|
+
# @param [Symbol] verb for HTTP REST request
|
109
|
+
# @param [String] path in URI for desired resource
|
110
|
+
# @param [String] host name of server
|
111
|
+
# @param [Hash] connect_options for HTTP connection
|
112
|
+
# @param [Hash] request_options for HTTP request
|
113
|
+
#
|
114
|
+
# @return [Array] result to be returned followed by response code, body, and headers
|
115
|
+
#
|
116
|
+
# @raise [HttpException] HTTP failure with associated status code
|
117
|
+
def request(verb, path, host, connect_options, request_options)
|
118
|
+
# Finish forming path by stripping path, if any, from host
|
119
|
+
uri = URI.parse(host)
|
120
|
+
request_options[:path] = uri.path + request_options[:path]
|
121
|
+
uri.path = ""
|
122
|
+
|
123
|
+
# Make request an then yield fiber until it completes
|
124
|
+
fiber = Fiber.current
|
125
|
+
connection = EM::HttpRequest.new(uri.to_s, connect_options)
|
126
|
+
http = connection.send(verb, request_options)
|
127
|
+
http.errback { fiber.resume(http.error.to_s == "Errno::ETIMEDOUT" ? 504 : 500, http.error) }
|
128
|
+
http.callback { fiber.resume(http.response_header.status, http.response, http.response_header) }
|
129
|
+
response_code, response_body, response_headers = Fiber.yield
|
130
|
+
response_headers = beautify_headers(response_headers) if response_headers
|
131
|
+
result = BalancedHttpClient.response(response_code, response_body, response_headers, request_options[:head][:accept])
|
132
|
+
if request_options[:keepalive]
|
133
|
+
expires_at = Time.now + BalancedHttpClient::CONNECTION_REUSE_TIMEOUT
|
134
|
+
@connections[path] = {:host => host, :connection => connection, :expires_at => expires_at}
|
135
|
+
end
|
136
|
+
[result, response_code, response_body, response_headers]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Make long-polling request
|
140
|
+
# Note that the underlying thread is not blocked by the HTTP i/o, but this call itself is blocking
|
141
|
+
#
|
142
|
+
# @param [Hash] connection to server from previous request with keys :host, :connection,
|
143
|
+
# and :expires_at, with the :expires_at being adjusted on return
|
144
|
+
# @param [Hash] request_options for HTTP request
|
145
|
+
# @param [Time] stop_at time for polling
|
146
|
+
#
|
147
|
+
# @return [Array] result to be returned followed by response code, body, and headers
|
148
|
+
#
|
149
|
+
# @raise [HttpException] HTTP failure with associated status code
|
150
|
+
def poll(connection, request_options, stop_at)
|
151
|
+
uri = URI.parse(connection[:host])
|
152
|
+
request_options[:path] = uri.path + request_options[:path]
|
153
|
+
poll_again(Fiber.current, connection[:connection], request_options, stop_at)
|
154
|
+
code, body, headers = Fiber.yield
|
155
|
+
headers = beautify_headers(headers) if headers
|
156
|
+
result = BalancedHttpClient.response(code, body, headers, request_options[:head][:accept])
|
157
|
+
connection[:expires_at] = Time.now + BalancedHttpClient::CONNECTION_REUSE_TIMEOUT
|
158
|
+
[result, code, body, headers]
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
# Repeatedly make long-polling request until receive data or timeout
|
164
|
+
#
|
165
|
+
# @param [Symbol] verb for HTTP REST request
|
166
|
+
# @param [EM:HttpRequest] connection to server from previous request
|
167
|
+
# @param [Hash] request_options for HTTP request
|
168
|
+
# @param [Time] stop_at time for polling
|
169
|
+
#
|
170
|
+
# @return [TrueClass] always true
|
171
|
+
#
|
172
|
+
# @raise [HttpException] HTTP failure with associated status code
|
173
|
+
def poll_again(fiber, connection, request_options, stop_at)
|
174
|
+
http = connection.send(:get, request_options)
|
175
|
+
http.errback { fiber.resume(http.error.to_s == "Errno::ETIMEDOUT" ? 504 : 500, http.error) }
|
176
|
+
http.callback do
|
177
|
+
code, body, headers = http.response_header.status, http.response, http.response_header
|
178
|
+
if code == 200 && (body.nil? || body == "null") && Time.now < stop_at
|
179
|
+
poll_again(fiber, connection, request_options, stop_at)
|
180
|
+
else
|
181
|
+
fiber.resume(code, body, headers)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
true
|
185
|
+
end
|
186
|
+
|
187
|
+
# Beautify response header keys so that in same form as RestClient
|
188
|
+
#
|
189
|
+
# @param [Hash] headers from response
|
190
|
+
#
|
191
|
+
# @return [Hash] response headers with keys as lower case symbols
|
192
|
+
def beautify_headers(headers)
|
193
|
+
headers.inject({}) { |out, (key, value)| out[key.gsub(/-/, '_').downcase.to_sym] = value; out }
|
194
|
+
end
|
195
|
+
|
196
|
+
end # NonBlockingClient
|
197
|
+
|
198
|
+
end # RightScale
|
@@ -46,6 +46,8 @@ module RightScale
|
|
46
46
|
# @option options [Numeric] :retry_timeout maximum before stop retrying
|
47
47
|
# @option options [Array] :retry_intervals between successive retries
|
48
48
|
# @option options [Boolean] :retry_enabled for requests that fail to connect or that return a retry result
|
49
|
+
# @option options [Boolean] :non_blocking i/o is to be used for HTTP requests by applying
|
50
|
+
# EM::HttpRequest and fibers instead of RestClient; requests remain synchronous
|
49
51
|
# @option options [Boolean] :long_polling_only never attempt to create a WebSocket, always long-polling instead
|
50
52
|
# @option options [Array] :filter_params symbols or strings for names of request parameters
|
51
53
|
# whose values are to be hidden when logging
|
@@ -164,8 +166,6 @@ module RightScale
|
|
164
166
|
end
|
165
167
|
|
166
168
|
# Receive events via an HTTP WebSocket if available, otherwise via an HTTP long-polling
|
167
|
-
# This is a blocking call and therefore should be used from a thread different than
|
168
|
-
# otherwise used with this object, e.g., EM.defer thread
|
169
169
|
#
|
170
170
|
# @param [Array, NilClass] routing_keys for event sources of interest with nil meaning all
|
171
171
|
#
|