Permalink
Browse files

#1576, right_http_connection: "You have a nil object when you didnt e…

…xpect it! The error occurred while evaluating nil.body_stream" exception fix

git-svn-id: https://wush.net/svn/rightscale/right_http_connection/trunk@4984 9f0cbaf6-ce18-0410-ad37-d14a22affa91
  • Loading branch information...
1 parent a7e3e1f commit eb8130d353fb7eeceb656c7fc854a0f8e357f881 @konstantin-dzreev konstantin-dzreev committed Aug 11, 2008
Showing with 61 additions and 55 deletions.
  1. +7 −1 History.txt
  2. +54 −54 lib/right_http_connection.rb
View
@@ -49,4 +49,10 @@ Initial public release
- Added support for setting retry & timeout parameters in the constructor
- Improve handling of data streams during upload: if there is a failure and a retry, reset
- the seek pointer for the subsequent re-request
+ the seek pointer for the subsequent re-request
+
+== 1.2.4
+
+- fixed a bug: <NoMethodError: You have a nil object when you didn't expect it!
+ The error occurred while evaluating nil.body_stream>
+
@@ -34,15 +34,15 @@ module RightHttpConnection #:nodoc:
module VERSION #:nodoc:
MAJOR = 1
MINOR = 2
- TINY = 3
+ TINY = 4
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
module Rightscale
-
+
=begin rdoc
HttpConnection maintains a persistent HTTP connection to a remote
server. Each instance maintains its own unique connection to the
@@ -75,16 +75,16 @@ module Rightscale
them.
=end
- class HttpConnection
+ class HttpConnection
# Number of times to retry the request after encountering the first error
- HTTP_CONNECTION_RETRY_COUNT = 3
+ HTTP_CONNECTION_RETRY_COUNT = 3
# Throw a Timeout::Error if a connection isn't established within this number of seconds
- HTTP_CONNECTION_OPEN_TIMEOUT = 5
+ HTTP_CONNECTION_OPEN_TIMEOUT = 5
# Throw a Timeout::Error if no data have been read on this connnection within this number of seconds
- HTTP_CONNECTION_READ_TIMEOUT = 120
- # Length of the post-error probationary period during which all requests will fail
- HTTP_CONNECTION_RETRY_DELAY = 15
+ HTTP_CONNECTION_READ_TIMEOUT = 120
+ # Length of the post-error probationary period during which all requests will fail
+ HTTP_CONNECTION_RETRY_DELAY = 15
#--------------------
# class methods
@@ -95,10 +95,10 @@ class HttpConnection
@@params[:http_connection_open_timeout] = HTTP_CONNECTION_OPEN_TIMEOUT
@@params[:http_connection_read_timeout] = HTTP_CONNECTION_READ_TIMEOUT
@@params[:http_connection_retry_delay] = HTTP_CONNECTION_RETRY_DELAY
-
+
# Query the global (class-level) parameters:
- #
- # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
+ #
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
# :ca_file => 'path_to_file' # Path to a CA certification file in PEM format. The file can contain several CA certificates. If this parameter isn't set, HTTPS certs won't be verified.
# :logger => Logger object # If omitted, HttpConnection logs to STDOUT
# :exception => Exception to raise # The type of exception to raise
@@ -110,7 +110,7 @@ class HttpConnection
def self.params
@@params
end
-
+
# Set the global (class-level) parameters
def self.params=(params)
@@params = params
@@ -125,7 +125,7 @@ def self.params=(params)
attr_accessor :logger
# Params hash:
- # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
# :ca_file => 'path_to_file' # A path of a CA certification file in PEM format. The file can contain several CA certificates.
# :logger => Logger object # If omitted, HttpConnection logs to STDOUT
# :exception => Exception to raise # The type of exception to raise if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
@@ -142,7 +142,7 @@ def initialize(params={})
@params[:http_connection_retry_delay] ||= @@params[:http_connection_retry_delay]
@http = nil
@server = nil
- @logger = get_param(:logger) ||
+ @logger = get_param(:logger) ||
(RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
Logger.new(STDOUT)
end
@@ -164,7 +164,7 @@ def socket_read_size?
def socket_read_size=(newsize)
Net::BufferedIO.socket_read_size=(newsize)
end
-
+
# Query for the maximum size (in bytes) of a single read from local data
# sources like files. This is important, for example, in a streaming PUT of a
# large buffer.
@@ -190,27 +190,27 @@ def local_read_size=(newsize)
def error_count
@@state[@server] ? @@state[@server][:count] : 0
end
-
+
# time of last error for server, nil if all is ok
def error_time
@@state[@server] && @@state[@server][:time]
end
-
+
# message for last error for server, "" if all is ok
def error_message
@@state[@server] ? @@state[@server][:message] : ""
end
-
+
# add an error for a server
def error_add(message)
@@state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
end
-
+
# reset the error state for a server (i.e. a request succeeded)
def error_reset
@@state.delete(@server)
end
-
+
# Error message stuff...
def banana_message
return "#{@server} temporarily unavailable: (#{error_message})"
@@ -219,7 +219,7 @@ def banana_message
def err_header
return "#{self.class.name} :"
end
-
+
# Adds new EOF timestamp.
# Returns the number of seconds to wait before new conection retry:
# 0.5, 1, 2, 4, 8
@@ -232,37 +232,37 @@ def add_eof
def eof_time
@@eof[@server] && @@eof[@server].last
end
-
+
# Returns true if we are receiving EOFs during last @params[:http_connection_retry_delay] seconds
# and there were no successful response from server
def raise_on_eof_exception?
@@eof[@server].blank? ? false : ( (Time.now.to_i-@params[:http_connection_retry_delay]) > @@eof[@server].last.to_i )
- end
-
+ end
+
# Reset a list of EOFs for this server.
# This is being called when we have got an successful response from server.
def eof_reset
@@eof.delete(@server)
end
-
- # Detects if an object is 'streamable' - can we read from it, and can we know the size?
+
+ # Detects if an object is 'streamable' - can we read from it, and can we know the size?
def setup_streaming(request)
if(request.body && request.body.respond_to?(:read))
body = request.body
- request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
+ request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
request.body_stream = request.body
true
end
end
-
+
def get_fileptr_offset(request_params)
request_params[:request].body.pos
rescue Exception => e
# Probably caught this because the body doesn't support the pos() method, like if it is a socket.
# Just return 0 and get on with life.
0
end
-
+
def reset_fileptr_offset(request, offset = 0)
if(request.body_stream && request.body_stream.respond_to?(:pos))
begin
@@ -272,7 +272,7 @@ def reset_fileptr_offset(request, offset = 0)
" -- #{err_header} #{e.inspect}")
raise e
end
- end
+ end
end
# Start a fresh connection. The object closes any existing connection and
@@ -284,12 +284,12 @@ def start(request_params)
@server = request_params[:server]
@port = request_params[:port]
@protocol = request_params[:protocol]
-
+
@logger.info("Opening new #{@protocol.upcase} connection to #@server:#@port")
@http = Net::HTTP.new(@server, @port)
@http.open_timeout = @params[:http_connection_open_timeout]
@http.read_timeout = @params[:http_connection_read_timeout]
-
+
if @protocol == 'https'
verifyCallbackProc = Proc.new{ |ok, x509_store_ctx|
code = x509_store_ctx.error
@@ -303,7 +303,7 @@ def start(request_params)
if ca_file
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
@http.verify_callback = verifyCallbackProc
- @http.ca_file = ca_file
+ @http.ca_file = ca_file
end
end
# open connection
@@ -312,55 +312,55 @@ def start(request_params)
public
-=begin rdoc
+=begin rdoc
Send HTTP request to server
request_params hash:
:server => 'www.HostName.com' # Hostname or IP address of HTTP server
- :port => '80' # Port of HTTP server
- :protocol => 'https' # http and https are supported on any port
+ :port => '80' # Port of HTTP server
+ :protocol => 'https' # http and https are supported on any port
:request => 'requeststring' # Fully-formed HTTP request to make
Raises RuntimeError, Interrupt, and params[:exception] (if specified in new).
-
+
=end
def request(request_params, &block)
# We save the offset here so that if we need to retry, we can return the file pointer to its initial position
mypos = get_fileptr_offset(request_params)
loop do
# if we are inside a delay between retries: no requests this time!
- if error_count > @params[:http_connection_retry_count] &&
+ if error_count > @params[:http_connection_retry_count] &&
error_time + @params[:http_connection_retry_delay] > Time.now
# store the message (otherwise it will be lost after error_reset and
# we will raise an exception with an empty text)
banana_message_text = banana_message
@logger.warn("#{err_header} re-raising same error: #{banana_message_text} " +
- "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
+ "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
exception = get_param(:exception) || RuntimeError
raise exception.new(banana_message_text)
end
-
+
# try to connect server(if connection does not exist) and get response data
begin
request_params[:protocol] ||= (request_params[:port] == 443 ? 'https' : 'http')
+
+ request = request_params[:request]
+ request['User-Agent'] = get_param(:user_agent) || ''
+
# (re)open connection to server if none exists or params has changed
- unless @http &&
+ unless @http &&
@http.started? &&
@server == request_params[:server] &&
@port == request_params[:port] &&
@protocol == request_params[:protocol]
start(request_params)
end
-
- # get response and return it
- request = request_params[:request]
- request['User-Agent'] = get_param(:user_agent) || ''
# Detect if the body is a streamable object like a file or socket. If so, stream that
# bad boy.
setup_streaming(request)
response = @http.request(request, &block)
-
+
error_reset
eof_reset
return response
@@ -373,26 +373,26 @@ def request(request_params, &block)
# 'slept'. It is still not clear which way we should treat errors
# like RST and resolution failures. For now, there is no additional
# delay for these errors although this may change in the future.
-
+
# EOFError means the server closed the connection on us.
rescue EOFError => e
@logger.debug("#{err_header} server #{@server} closed connection")
@http = nil
-
+
# if we have waited long enough - raise an exception...
if raise_on_eof_exception?
exception = get_param(:exception) || RuntimeError
- @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
+ @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
raise exception.new("Permanent EOF is being received from #{@server}.")
else
# ... else just sleep a bit before new retry
sleep(add_eof)
# We will be retrying the request, so reset the file pointer
reset_fileptr_offset(request, mypos)
- end
+ end
rescue Exception => e # See comment at bottom for the list of errors seen...
@http = nil
- # if ctrl+c is pressed - we have to reraise exception to terminate proggy
+ # if ctrl+c is pressed - we have to reraise exception to terminate proggy
if e.is_a?(Interrupt) && !( e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error))
@logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
raise
@@ -407,7 +407,7 @@ def request(request_params, &block)
# We will be retrying the request, so reset the file pointer
reset_fileptr_offset(request, mypos)
-
+
end
end
end
@@ -416,7 +416,7 @@ def finish(reason = '')
if @http && @http.started?
reason = ", reason: '#{reason}'" unless reason.blank?
@logger.info("Closing #{@http.use_ssl? ? 'HTTPS' : 'HTTP'} connection to #{@http.address}:#{@http.port}#{reason}")
- @http.finish
+ @http.finish
end
end
@@ -430,6 +430,6 @@ def finish(reason = '')
# #<Errno::ECONNRESET: Connection reset by peer>
# #<OpenSSL::SSL::SSLError: SSL_write:: bad write retry>
end
-
+
end

0 comments on commit eb8130d

Please sign in to comment.