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.
@@ -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 [RightSupport::Net::BalancedHttpClient] client
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 = EM::PeriodicTimer.new(rand(@options[:reconnect_interval])) do
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
- sleep(interval)
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 [RightScale::BalancedHttpClient::NotResponding] not_responding exception
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
- sleep(interval)
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
  #