lib/persistent_http.rb in persistent_http-1.0.6 vs lib/persistent_http.rb in persistent_http-2.0.0

- old
+ new

@@ -1,9 +1,7 @@ -require 'net/http' -require 'net/https' -require 'persistent_http/faster' -require 'uri' +require 'persistent_http/connection' +require 'persistent_http/version' require 'gene_pool' ## # Persistent connections for Net::HTTP # @@ -20,86 +18,36 @@ # :pool_size => 10, # :warn_timeout => 0.25, # :force_retry => true, # :url => 'https://www.example.com/echo/foo' # equivalent to :use_ssl => true, :host => 'www.example.com', :default_path => '/echo/foo' # ) -# +# # def send_get_message # response = @@persistent_http.request # ... Handle response as you would a normal Net::HTTPResponse ... # end -# +# # def send_post_message # request = Net::HTTP::Post.new('/perform_service) # ... Modify request as needed ... # response = @@persistent_http.request(request) # ... Handle response as you would a normal Net::HTTPResponse ... # end class PersistentHTTP ## - # The version of PersistentHTTP use are using - VERSION = '1.0.0' - - ## # Error class for errors raised by PersistentHTTP. Various # SystemCallErrors are re-raised with a human-readable message under this # class. class Error < StandardError; end ## - # An SSL certificate authority. Setting this will set verify_mode to - # VERIFY_PEER. - attr_accessor :ca_file - - ## - # This client's OpenSSL::X509::Certificate - attr_accessor :certificate - - ## - # Sends debug_output to this IO via Net::HTTP#set_debug_output. - # - # Never use this method in production code, it causes a serious security - # hole. - attr_accessor :debug_output - - ## - # Default path for the request - attr_accessor :default_path - - ## - # Retry even for non-idempotent (POST) requests. - attr_accessor :force_retry - - ## - # Headers that are added to every request - attr_accessor :headers - - ## - # Host for the Net:HTTP connection - attr_reader :host - - ## - # HTTP version to enable version specific features. - attr_reader :http_version - - ## # Connection will be renewed if it hasn't been used in this amount of time. Defaults to 10 seconds. attr_reader :idle_timeout ## - # The value sent in the Keep-Alive header. Defaults to 30. Not needed for - # HTTP/1.1 servers. - # - # This may not work correctly for HTTP/1.0 servers - # - # This method may be removed in a future version as RFC 2616 does not - # require this header. - attr_accessor :keep_alive - - ## # Logger for message logging. attr_accessor :logger ## # A name for this connection. Allows you to keep your connections apart @@ -107,51 +55,17 @@ attr_reader :name ## # Seconds to wait for an available connection before a Timeout::Error is raised attr_accessor :pool_timeout - ## - # Seconds to wait until a connection is opened. See Net::HTTP#open_timeout - attr_accessor :open_timeout ## # The maximum size of the connection pool attr_reader :pool_size ## - # Port for the Net:HTTP connection - attr_reader :port - - ## - # This client's SSL private key - attr_accessor :private_key - - ## - # The URL through which requests will be proxied - attr_reader :proxy_uri - - ## - # Seconds to wait until reading one block. See Net::HTTP#read_timeout - attr_accessor :read_timeout - - ## - # Use ssl if set - attr_reader :use_ssl - - ## - # SSL verification callback. Used when ca_file is set. - attr_accessor :verify_callback - - ## - # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_NONE which ignores - # certificate problems. - # - # You can use +verify_mode+ to override any default values. - attr_accessor :verify_mode - - ## - # The threshold in seconds for checking out a connection at which a warning + # The threshold in seconds for checking out a connection at which a warning # will be logged via the logger attr_reader :warn_timeout ## # Creates a new PersistentHTTP. @@ -170,96 +84,25 @@ # proxy.user = 'AzureDiamond' # proxy.password = 'hunter2' def initialize(options={}) @name = options[:name] || 'PersistentHTTP' - @ca_file = options[:ca_file] - @certificate = options[:certificate] - @debug_output = options[:debug_output] - @default_path = options[:default_path] - @force_retry = options[:force_retry] - @headers = options[:header] || {} - @host = options[:host] @idle_timeout = options[:idle_timeout] || 10 - @keep_alive = options[:keep_alive] || 30 @logger = options[:logger] @pool_timeout = options[:pool_timeout] - @open_timeout = options[:open_timeout] @pool_size = options[:pool_size] || 1 - @port = options[:port] - @private_key = options[:private_key] - @read_timeout = options[:read_timeout] - @use_ssl = options[:use_ssl] - @verify_callback = options[:verify_callback] - @verify_mode = options[:verify_mode] @warn_timeout = options[:warn_timeout] || 0.5 - - url = options[:url] - if url - url = URI.parse(url) if url.kind_of? String - @default_path ||= url.request_uri - @host ||= url.host - @port ||= url.port - @use_ssl ||= url.scheme == 'https' - end - - @port ||= (@use_ssl ? 443 : 80) - # Hash containing the request counts based on the connection - @count_hash = Hash.new(0) - - raise 'host not set' unless @host - net_http_args = [@host, @port] - connection_id = net_http_args.join ':' - - proxy = options[:proxy] - - @proxy_uri = case proxy - when :ENV then proxy_from_env - when URI::HTTP then proxy - when nil then # ignore - else raise ArgumentError, 'proxy must be :ENV or a URI::HTTP' - end - - if @proxy_uri then - @proxy_args = [ - @proxy_uri.host, - @proxy_uri.port, - @proxy_uri.user, - @proxy_uri.password, - ] - - @proxy_connection_id = [nil, *@proxy_args].join ':' - - connection_id << @proxy_connection_id - net_http_args.concat @proxy_args - end - - @pool = GenePool.new(:name => name + '-' + connection_id, + @pool = GenePool.new(:name => name, :pool_size => @pool_size, :timeout => @pool_timeout, :warn_timeout => @warn_timeout, :idle_timeout => @idle_timeout, - :close_proc => nil, + :close_proc => :finish, :logger => @logger) do - begin - @logger.debug { "#{name}: Creating connection" } if @logger - connection = Net::HTTP.new(*net_http_args) - connection.set_debug_output @debug_output if @debug_output - connection.open_timeout = @open_timeout if @open_timeout - connection.read_timeout = @read_timeout if @read_timeout - - ssl connection if @use_ssl - - connection.start - @logger.debug { "#{name} #{connection}: Connection created" } if @logger - connection - rescue Errno::ECONNREFUSED - raise Error, "connection refused: #{connection.address}:#{connection.port}" - rescue Errno::EHOSTDOWN - raise Error, "host down: #{connection.address}:#{connection.port}" - end + @logger.debug { "#{name}: Creating connection" } if @logger + Connection.new(options) end end # Reset the size of the connection pool def pool_size=(pool_size) @@ -282,187 +125,21 @@ # # If there is an error and the request is idempontent according to RFC 2616 # it will be retried automatically. def request(req = nil, options = {}, &block) - retried = false - bad_response = false - - req = Net::HTTP::Get.new @default_path unless req - - headers.each do |pair| - req.add_field(*pair) - end - - req.add_field 'Connection', 'keep-alive' - req.add_field 'Keep-Alive', @keep_alive - @pool.with_connection do |connection| begin - options.each do |key, value| - connection.send("#{key}=", value) - end - response = connection.request req, &block - @http_version ||= response.http_version - @count_hash[connection.object_id] += 1 - return response - - rescue Timeout::Error => e - due_to = "(due to #{e.message} - #{e.class})" - message = error_message connection - @logger.info "#{name}: Removing connection #{due_to} #{message}" if @logger - remove connection + connection.request req, options, &block + rescue Exception => e + @pool.remove(connection) raise - - rescue Net::HTTPBadResponse => e - message = error_message connection - if bad_response or not (idempotent? req or @force_retry) - @logger.info "#{name}: Removing connection because of too many bad responses #{message}" if @logger - remove connection - raise Error, "too many bad responses #{message}" - else - bad_response = true - @logger.info "#{name}: Renewing connection because of bad response #{message}" if @logger - connection = renew connection - retry - end - - rescue IOError, EOFError, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE => e - due_to = "(due to #{e.message} - #{e.class})" - message = error_message connection - if retried or not (idempotent? req or @force_retry) - @logger.info "#{name}: Removing connection #{due_to} #{message}" if @logger - remove connection - raise Error, "too many connection resets #{due_to} #{message}" - else - retried = true - @logger.info "#{name}: Renewing connection #{due_to} #{message}" if @logger - connection = renew connection - retry - end end end end ## # Shuts down all connections. def shutdown(timeout=10) @pool.close(timeout) end - - ####### - private - ####### - - ## - # Returns an error message containing the number of requests performed on - # this connection - - def error_message connection - requests = @count_hash[connection.object_id] || 0 - "after #{requests} requests on #{connection.object_id}" - end - - ## - # URI::escape wrapper - - def escape str - URI.escape str if str - end - - ## - # Finishes the Net::HTTP +connection+ - - def finish connection - @count_hash.delete(connection.object_id) - connection.finish - rescue IOError - end - - ## - # Is +req+ idempotent according to RFC 2616? - - def idempotent? req - case req - when Net::HTTP::Delete, Net::HTTP::Get, Net::HTTP::Head, - Net::HTTP::Options, Net::HTTP::Put, Net::HTTP::Trace then - true - end - end - - ## - # Adds "http://" to the String +uri+ if it is missing. - - def normalize_uri uri - (uri =~ /^https?:/) ? uri : "http://#{uri}" - end - - ## - # Creates a URI for an HTTP proxy server from ENV variables. - # - # If +HTTP_PROXY+ is set a proxy will be returned. - # - # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the URI is given the - # indicated user and password unless HTTP_PROXY contains either of these in - # the URI. - # - # For Windows users lowercase ENV variables are preferred over uppercase ENV - # variables. - - def proxy_from_env - env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] - - return nil if env_proxy.nil? or env_proxy.empty? - - uri = URI.parse(normalize_uri(env_proxy)) - - unless uri.user or uri.password then - uri.user = escape ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'] - uri.password = escape ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'] - end - - uri - end - - ## - # Finishes then removes the Net::HTTP +connection+ - - def remove connection - finish connection - @pool.remove(connection) - end - - ## - # Finishes then renews the Net::HTTP +connection+. It may be unnecessary - # to completely recreate the connection but connections that get timed out - # in JRuby leave the ssl context in a frozen object state. - - def renew connection - finish connection - connection = @pool.renew(connection) - end - - ## - # Enables SSL on +connection+ - - def ssl connection - connection.use_ssl = true - - # suppress warning but allow override - connection.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @verify_mode - - if @ca_file then - connection.ca_file = @ca_file - connection.verify_mode = OpenSSL::SSL::VERIFY_PEER - connection.verify_callback = @verify_callback if @verify_callback - end - - if @certificate and @private_key then - connection.cert = @certificate - connection.key = @private_key - end - - connection.verify_mode = @verify_mode if @verify_mode - end - end -