Class: RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy

Inherits:
Object
  • Object
show all
Defined in:
lib/base/routines/connection_proxies/net_http_persistent_proxy.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

TIMEOUT_ERRORS =

Known timeout errors

/Timeout|ETIMEDOUT/
OTHER_ERRORS =

Other re-triable errors

/SocketError|EOFError|SSL_connect/

Instance Method Summary collapse

Instance Method Details

#create_new_http_requestNet::HTTPRequest

Creates and configures a new HTTP request object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 131

def create_new_http_request
  # Create a new HTTP request instance
  request_spec = @data[:request][:instance]
  http_class   = "Net::HTTP::#{request_spec.verb._camelize}"
  http_request = http_class._constantize::new(request_spec.path)
  # Set the request body
  if request_spec.is_io?
    http_request.body_stream = request_spec.body
  else
    http_request.body = request_spec.body
  end
  # Copy headers
  request_spec.headers.each { |header, value| http_request[header] = value }
  # Save the new request
  request_spec.raw = http_request
  # Set user-agent
  if @data[:options].has_key?(:connection_user_agent)
    http_request['user-agent'] ||= @data[:options][:connection_user_agent]
  end
  http_request
end

#create_new_persistent_connectionObject

Creates a new connection.

There is a bug in Net::HTTP::Persistent where it allows you to reuse an SSL connection created by another instance of Net::HTTP::Persistent, if they share the same app name. To avoid this, every instance of Net::HTTP::Persistent should have its own ‘name’.

If your app does not care about SSL certs and keys (like AWS does) then it is safe to reuse connections.

see github.com/drbrain/net-http-persistent/issues/45



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 85

def create_new_persistent_connection
  app_name = if @data[:options][:connection_ca_file] ||
                @data[:credentials][:cert]           ||
                @data[:credentials][:key]
               'right_cloud_api_gem_%s' % Utils::generate_token
             else
               'right_cloud_api_gem'
             end
  connection = Net::HTTP::Persistent.new(app_name)
  set_persistent_connection_options!(connection)
  # Register a callback to close current connection
  @data[:callbacks][:close_current_connection] = Proc::new do |reason|
    connection.shutdown
    log "Current connection closed: #{reason}"
  end
  connection
end

#disable_net_http_persistent_retries(connection) ⇒ void

This method returns an undefined value.

Net::HTTP::Persistent believes that it can retry on any GET call what is not true for Query like API clouds (Amazon, CloudStack, Euca, etc). The solutions is to monkeypatch Net::HTTP::Persistent#can_retry? so that is returns Net::HTTP::Persistent#retry_change_requests.



240
241
242
243
244
245
246
247
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 240

def disable_net_http_persistent_retries(connection)
  connection.retry_change_requests = false
  # Monkey patch this connection instance only.
  def connection.can_retry?(*args)
    false
  end
  nil
end

#log(message) ⇒ Object



41
42
43
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 41

def log(message)
  @data[:options][:cloud_api_logger].log(message, :connection_proxy, :warn)
end

#make_request_with_retries(connection, uri, http_request) ⇒ void

This method returns an undefined value.

Makes request with low level retries.

Net::HTTP::Persistent does not fully support retries logic that we used to have. To deal with this we disable Net::HTTP::Persistent’s retries and handle them in our code.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 165

def make_request_with_retries(connection, uri, http_request)
  disable_net_http_persistent_retries(connection)
  # Initialize retry vars:
  connection_retry_count = @data[:options][:connection_retry_count] || 3
  connection_retry_delay = @data[:options][:connection_retry_delay] || 0.5
  retries_performed      = 0
  # If block is given - pass there all the chunks of a response and then stop
  # (don't do any parsing, analysis, etc)
  block = @data[:vars][:system][:block]
  begin
    if block
      # Response.body is a Net::ReadAdapter instance - it can't be read as a string.
      # WEB: On its own, Net::HTTP causes response.body to be a Net::ReadAdapter when you make a request with a block
      # that calls read_body on the response.
      connection.request(uri, http_request) do |response|
        # If we are at the point when we have started reading from the remote end
        # then there is no low level retry is allowed. Otherwise we would need to reset the
        # IO pointer, etc.
        connection_retry_count = 0
        if response.is_a?(Net::HTTPSuccess)
          set_http_response(response, :skip_body)
          response.read_body(&block)
        else
          set_http_response(response)
        end
      end
    else
      # Set text response
      response = connection.request(uri, http_request)
      set_http_response(response)
    end
    nil
  rescue => e
    # Fail if it is an unknown error
    fail(e) if !(e.message[TIMEOUT_ERRORS] || e.message[OTHER_ERRORS])
    # Fail if it is a Timeout and timeouts are banned
    fail(e) if e.message[TIMEOUT_ERRORS] && !!@data[:options][:abort_on_timeout]
    # Fail if there are no retries left...
    fail(e) if (connection_retry_count -= 1) < 0
    # ... otherwise sleep a bit and retry.
    retries_performed += 1
    log("#{self.class.name}: Performing retry ##{retries_performed} caused by: #{e.class.name}: #{e.message}")
    sleep(connection_retry_delay) unless connection_retry_delay._blank?
    connection_retry_delay *= 2
    retry
  end
end

#request(data) ⇒ Object

Performs an HTTP request.

P.S. Options not supported by Net::HTTP::Persistent: :connection_retry_count, :connection_retry_delay, :cloud_api_logger



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 54

def request(data)
  require "net/http/persistent"
  # Initialize things:
  @data            = data
  @data[:response] = {}
  # Create a new HTTP request instance
  http_request = create_new_http_request
  # Create and tweak Net::HTTP::Persistent instance
  connection   = create_new_persistent_connection
  # Make a request
  begin
    make_request_with_retries(connection, @data[:connection][:uri], http_request)
  rescue => e
    fail(ConnectionError, e.message)
  ensure
    connection.shutdown
  end
end

#set_http_response(response, skip_body = false) ⇒ void

This method returns an undefined value.

Saves HTTP Response into data hash.



220
221
222
223
224
225
226
227
228
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 220

def set_http_response(response, skip_body=false)
  @data[:response][:instance] = HTTPResponse.new(
    response.code,
    skip_body ? nil : response.body,
    response.to_hash,
    response
  )
  nil
end

#set_persistent_connection_options!(connection) ⇒ Net::HTTP::Persistent

Sets connection_ca_file, connection_read_timeout, connection_open_timeout, connection_verify_mode and SSL cert and key



111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/base/routines/connection_proxies/net_http_persistent_proxy.rb', line 111

def set_persistent_connection_options!(connection)
  [:ca_file, :read_timeout, :open_timeout, :verify_mode].each do |connection_method|
    connection_option_name = "connection_#{connection_method}".to_sym
    next unless @data[:options].has_key?(connection_option_name)
    connection.__send__("#{connection_method}=", @data[:options][connection_option_name])
  end
  if @data[:credentials].has_key?(:cert)
    connection.cert = OpenSSL::X509::Certificate.new(@data[:credentials][:cert])
  end
  if @data[:credentials].has_key?(:key)
    connection.key  = OpenSSL::PKey::RSA.new(@data[:credentials][:key])
  end
  connection
end