mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-12-10 00:03:00 -05:00
changed internal session structure, so that it uses local selectors directly
pools are then used only to fetch new conenctions; selectors are discarded when not needed anymore; HTTPX.wrap is for now patched, but would ideally be done with in the future
This commit is contained in:
parent
12fbca468b
commit
11d197ff24
@ -43,11 +43,14 @@ module HTTPX
|
|||||||
|
|
||||||
attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session
|
attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session
|
||||||
|
|
||||||
attr_writer :timers
|
attr_writer :current_selector, :coalesced_connection
|
||||||
|
|
||||||
attr_accessor :family
|
attr_accessor :current_session, :family
|
||||||
|
|
||||||
def initialize(uri, options)
|
def initialize(uri, options)
|
||||||
|
@current_session = @current_selector = @coalesced_connection = nil
|
||||||
|
@exhausted = @cloned = false
|
||||||
|
|
||||||
@origins = [uri.origin]
|
@origins = [uri.origin]
|
||||||
@origin = Utils.to_uri(uri.origin)
|
@origin = Utils.to_uri(uri.origin)
|
||||||
@options = Options.new(options)
|
@options = Options.new(options)
|
||||||
@ -58,6 +61,7 @@ module HTTPX
|
|||||||
@read_buffer = Buffer.new(@options.buffer_size)
|
@read_buffer = Buffer.new(@options.buffer_size)
|
||||||
@write_buffer = Buffer.new(@options.buffer_size)
|
@write_buffer = Buffer.new(@options.buffer_size)
|
||||||
@pending = []
|
@pending = []
|
||||||
|
|
||||||
on(:error, &method(:on_error))
|
on(:error, &method(:on_error))
|
||||||
if @options.io
|
if @options.io
|
||||||
# if there's an already open IO, get its
|
# if there's an already open IO, get its
|
||||||
@ -68,6 +72,40 @@ module HTTPX
|
|||||||
else
|
else
|
||||||
transition(:idle)
|
transition(:idle)
|
||||||
end
|
end
|
||||||
|
on(:activate) do
|
||||||
|
@current_session.select_connection(self, @current_selector)
|
||||||
|
end
|
||||||
|
on(:close) do
|
||||||
|
next if @exhausted # it'll reset
|
||||||
|
|
||||||
|
# may be called after ":close" above, so after the connection has been checked back in.
|
||||||
|
# next unless @current_session
|
||||||
|
|
||||||
|
next unless @current_session
|
||||||
|
|
||||||
|
@current_session.deselect_connection(self, @current_selector, @cloned)
|
||||||
|
end
|
||||||
|
on(:terminate) do
|
||||||
|
next if @exhausted # it'll reset
|
||||||
|
|
||||||
|
# may be called after ":close" above, so after the connection has been checked back in.
|
||||||
|
next unless @current_session
|
||||||
|
|
||||||
|
@current_session.deselect_connection(self, @current_selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
# sets the callbacks on the +connection+ required to process certain specific
|
||||||
|
# connection lifecycle events which deal with request rerouting.
|
||||||
|
on(:misdirected) do |misdirected_request|
|
||||||
|
other_connection = @current_session.find_connection(@origin, @current_selector,
|
||||||
|
@options.merge(ssl: { alpn_protocols: %w[http/1.1] }))
|
||||||
|
other_connection.merge(self)
|
||||||
|
misdirected_request.transition(:idle)
|
||||||
|
other_connection.send(misdirected_request)
|
||||||
|
end
|
||||||
|
on(:altsvc) do |alt_origin, origin, alt_params|
|
||||||
|
build_altsvc_connection(alt_origin, origin, alt_params)
|
||||||
|
end
|
||||||
|
|
||||||
@inflight = 0
|
@inflight = 0
|
||||||
@keep_alive_timeout = @options.timeout[:keep_alive_timeout]
|
@keep_alive_timeout = @options.timeout[:keep_alive_timeout]
|
||||||
@ -168,7 +206,12 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def inflight?
|
def inflight?
|
||||||
@parser && !@parser.empty? && !@write_buffer.empty?
|
@parser && (
|
||||||
|
# parser may be dealing with other requests (possibly started from a different fiber)
|
||||||
|
!@parser.empty? ||
|
||||||
|
# connection may be doing connection termination handshake
|
||||||
|
!@write_buffer.empty?
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def interests
|
def interests
|
||||||
@ -184,6 +227,9 @@ module HTTPX
|
|||||||
|
|
||||||
return @parser.interests if @parser
|
return @parser.interests if @parser
|
||||||
|
|
||||||
|
nil
|
||||||
|
rescue StandardError => e
|
||||||
|
emit(:error, e)
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -205,6 +251,9 @@ module HTTPX
|
|||||||
consume
|
consume
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
|
rescue StandardError => e
|
||||||
|
emit(:error, e)
|
||||||
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
@ -221,8 +270,9 @@ module HTTPX
|
|||||||
|
|
||||||
# bypasses the state machine to force closing of connections still connecting.
|
# bypasses the state machine to force closing of connections still connecting.
|
||||||
# **only** used for Happy Eyeballs v2.
|
# **only** used for Happy Eyeballs v2.
|
||||||
def force_reset
|
def force_reset(cloned = false)
|
||||||
@state = :closing
|
@state = :closing
|
||||||
|
@cloned = cloned
|
||||||
transition(:closed)
|
transition(:closed)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -235,6 +285,8 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send(request)
|
def send(request)
|
||||||
|
return @coalesced_connection.send(request) if @coalesced_connection
|
||||||
|
|
||||||
if @parser && !@write_buffer.full?
|
if @parser && !@write_buffer.full?
|
||||||
if @response_received_at && @keep_alive_timeout &&
|
if @response_received_at && @keep_alive_timeout &&
|
||||||
Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
|
Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
|
||||||
@ -255,6 +307,8 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
def timeout
|
||||||
|
return if @state == :closed || @state == :inactive
|
||||||
|
|
||||||
return @timeout if @timeout
|
return @timeout if @timeout
|
||||||
|
|
||||||
return @options.timeout[:connect_timeout] if @state == :idle
|
return @options.timeout[:connect_timeout] if @state == :idle
|
||||||
@ -301,6 +355,12 @@ module HTTPX
|
|||||||
transition(:open)
|
transition(:open)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disconnect
|
||||||
|
emit(:close)
|
||||||
|
@current_session = nil
|
||||||
|
@current_selector = nil
|
||||||
|
end
|
||||||
|
|
||||||
def consume
|
def consume
|
||||||
return unless @io
|
return unless @io
|
||||||
|
|
||||||
@ -475,8 +535,25 @@ module HTTPX
|
|||||||
request.emit(:promise, parser, stream)
|
request.emit(:promise, parser, stream)
|
||||||
end
|
end
|
||||||
parser.on(:exhausted) do
|
parser.on(:exhausted) do
|
||||||
|
@exhausted = true
|
||||||
|
current_session = @current_session
|
||||||
|
current_selector = @current_selector
|
||||||
|
parser.close
|
||||||
@pending.concat(parser.pending)
|
@pending.concat(parser.pending)
|
||||||
emit(:exhausted)
|
case @state
|
||||||
|
when :closed
|
||||||
|
idling
|
||||||
|
@exhausted = false
|
||||||
|
@current_session = current_session
|
||||||
|
@current_selector = current_selector
|
||||||
|
when :closing
|
||||||
|
once(:close) do
|
||||||
|
idling
|
||||||
|
@exhausted = false
|
||||||
|
@current_session = current_session
|
||||||
|
@current_selector = current_selector
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
parser.on(:origin) do |origin|
|
parser.on(:origin) do |origin|
|
||||||
@origins |= [origin]
|
@origins |= [origin]
|
||||||
@ -492,8 +569,14 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
parser.on(:reset) do
|
parser.on(:reset) do
|
||||||
@pending.concat(parser.pending) unless parser.empty?
|
@pending.concat(parser.pending) unless parser.empty?
|
||||||
|
current_session = @current_session
|
||||||
|
current_selector = @current_selector
|
||||||
reset
|
reset
|
||||||
idling unless @pending.empty?
|
unless @pending.empty?
|
||||||
|
idling
|
||||||
|
@current_session = current_session
|
||||||
|
@current_selector = current_selector
|
||||||
|
end
|
||||||
end
|
end
|
||||||
parser.on(:current_timeout) do
|
parser.on(:current_timeout) do
|
||||||
@current_timeout = @timeout = parser.timeout
|
@current_timeout = @timeout = parser.timeout
|
||||||
@ -531,13 +614,13 @@ module HTTPX
|
|||||||
error.set_backtrace(e.backtrace)
|
error.set_backtrace(e.backtrace)
|
||||||
connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
|
connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
|
||||||
@state = :closed
|
@state = :closed
|
||||||
emit(:close)
|
disconnect
|
||||||
rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
|
rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
|
||||||
# connect errors, exit gracefully
|
# connect errors, exit gracefully
|
||||||
handle_error(e)
|
handle_error(e)
|
||||||
connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
|
connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
|
||||||
@state = :closed
|
@state = :closed
|
||||||
emit(:close)
|
disconnect
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_transition(nextstate)
|
def handle_transition(nextstate)
|
||||||
@ -582,7 +665,7 @@ module HTTPX
|
|||||||
return unless @write_buffer.empty?
|
return unless @write_buffer.empty?
|
||||||
|
|
||||||
purge_after_closed
|
purge_after_closed
|
||||||
emit(:close) if @pending.empty?
|
disconnect if @pending.empty?
|
||||||
when :already_open
|
when :already_open
|
||||||
nextstate = :open
|
nextstate = :open
|
||||||
# the first check for given io readiness must still use a timeout.
|
# the first check for given io readiness must still use a timeout.
|
||||||
@ -617,6 +700,34 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# returns an HTTPX::Connection for the negotiated Alternative Service (or none).
|
||||||
|
def build_altsvc_connection(alt_origin, origin, alt_params)
|
||||||
|
# do not allow security downgrades on altsvc negotiation
|
||||||
|
return if @origin.scheme == "https" && alt_origin.scheme != "https"
|
||||||
|
|
||||||
|
altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
|
||||||
|
|
||||||
|
# altsvc already exists, somehow it wasn't advertised, probably noop
|
||||||
|
return unless altsvc
|
||||||
|
|
||||||
|
alt_options = @options.merge(ssl: @options.ssl.merge(hostname: URI(origin).host))
|
||||||
|
|
||||||
|
connection = @current_session.find_connection(alt_origin, @current_selector, alt_options)
|
||||||
|
|
||||||
|
# advertised altsvc is the same origin being used, ignore
|
||||||
|
return if connection == self
|
||||||
|
|
||||||
|
connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
|
||||||
|
|
||||||
|
log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
|
||||||
|
|
||||||
|
connection.merge(self)
|
||||||
|
terminate
|
||||||
|
rescue UnsupportedSchemeError
|
||||||
|
altsvc["noop"] = true
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def build_socket(addrs = nil)
|
def build_socket(addrs = nil)
|
||||||
case @type
|
case @type
|
||||||
when "tcp"
|
when "tcp"
|
||||||
@ -734,7 +845,7 @@ module HTTPX
|
|||||||
|
|
||||||
def set_request_timeout(request, timeout, start_event, finish_events, &callback)
|
def set_request_timeout(request, timeout, start_event, finish_events, &callback)
|
||||||
request.once(start_event) do
|
request.once(start_event) do
|
||||||
interval = @timers.after(timeout, callback)
|
interval = @current_selector.after(timeout, callback)
|
||||||
|
|
||||||
Array(finish_events).each do |event|
|
Array(finish_events).each do |event|
|
||||||
# clean up request timeouts if the connection errors out
|
# clean up request timeouts if the connection errors out
|
||||||
|
|||||||
@ -327,10 +327,14 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
send(@pending.shift) unless @pending.empty?
|
send(@pending.shift) unless @pending.empty?
|
||||||
|
|
||||||
return unless @streams.empty? && exhausted?
|
return unless @streams.empty? && exhausted?
|
||||||
|
|
||||||
close
|
if @pending.empty?
|
||||||
emit(:exhausted) unless @pending.empty?
|
close
|
||||||
|
else
|
||||||
|
emit(:exhausted)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_frame(bytes)
|
def on_frame(bytes)
|
||||||
|
|||||||
@ -27,7 +27,7 @@ module HTTPX
|
|||||||
use_get: false,
|
use_get: false,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
|
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?
|
||||||
|
|
||||||
def initialize(_, options)
|
def initialize(_, options)
|
||||||
super
|
super
|
||||||
@ -66,23 +66,15 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def resolver_connection
|
def resolver_connection
|
||||||
@resolver_connection ||= @pool.find_connection(@uri, @options) || begin
|
@resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
|
||||||
@building_connection = true
|
@options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
|
||||||
connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
|
emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
|
||||||
@pool.init_connection(connection, @options)
|
|
||||||
# only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
|
|
||||||
catch(:coalesced) do
|
|
||||||
@building_connection = false
|
|
||||||
emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
|
|
||||||
connection
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def resolve(connection = @connections.first, hostname = nil)
|
def resolve(connection = @connections.first, hostname = nil)
|
||||||
return if @building_connection
|
|
||||||
return unless connection
|
return unless connection
|
||||||
|
|
||||||
hostname ||= @queries.key(connection)
|
hostname ||= @queries.key(connection)
|
||||||
@ -176,7 +168,7 @@ module HTTPX
|
|||||||
alias_address = answers[address["alias"]]
|
alias_address = answers[address["alias"]]
|
||||||
if alias_address.nil?
|
if alias_address.nil?
|
||||||
reset_hostname(address["name"])
|
reset_hostname(address["name"])
|
||||||
if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
|
if early_resolve(connection, hostname: address["alias"])
|
||||||
@connections.delete(connection)
|
@connections.delete(connection)
|
||||||
else
|
else
|
||||||
resolve(connection, address["alias"])
|
resolve(connection, address["alias"])
|
||||||
|
|||||||
@ -8,27 +8,45 @@ module HTTPX
|
|||||||
include Callbacks
|
include Callbacks
|
||||||
using ArrayExtensions::FilterMap
|
using ArrayExtensions::FilterMap
|
||||||
|
|
||||||
attr_reader :resolvers
|
attr_reader :resolvers, :options
|
||||||
|
|
||||||
def initialize(resolver_type, options)
|
def initialize(resolver_type, options)
|
||||||
|
@current_selector = nil
|
||||||
|
@current_session = nil
|
||||||
@options = options
|
@options = options
|
||||||
@resolver_options = @options.resolver_options
|
@resolver_options = @options.resolver_options
|
||||||
|
|
||||||
@resolvers = options.ip_families.map do |ip_family|
|
@resolvers = options.ip_families.map do |ip_family|
|
||||||
resolver = resolver_type.new(ip_family, options)
|
resolver = resolver_type.new(ip_family, options)
|
||||||
resolver.on(:resolve, &method(:on_resolver_connection))
|
resolver.multi = self
|
||||||
resolver.on(:error, &method(:on_resolver_error))
|
|
||||||
resolver.on(:close) { on_resolver_close(resolver) }
|
|
||||||
resolver
|
resolver
|
||||||
end
|
end
|
||||||
|
|
||||||
@errors = Hash.new { |hs, k| hs[k] = [] }
|
@errors = Hash.new { |hs, k| hs[k] = [] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_selector=(s)
|
||||||
|
@current_selector = s
|
||||||
|
@resolvers.each { |r| r.__send__(__method__, s) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_session=(s)
|
||||||
|
@current_session = s
|
||||||
|
@resolvers.each { |r| r.__send__(__method__, s) }
|
||||||
|
end
|
||||||
|
|
||||||
def closed?
|
def closed?
|
||||||
@resolvers.all?(&:closed?)
|
@resolvers.all?(&:closed?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
@resolvers.all?(&:empty?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inflight?
|
||||||
|
@resolvers.any(&:inflight?)
|
||||||
|
end
|
||||||
|
|
||||||
def timeout
|
def timeout
|
||||||
@resolvers.filter_map(&:timeout).min
|
@resolvers.filter_map(&:timeout).min
|
||||||
end
|
end
|
||||||
@ -58,18 +76,13 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def lazy_resolve(connection)
|
||||||
|
@resolvers.each do |resolver|
|
||||||
|
resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
|
||||||
|
next if resolver.empty?
|
||||||
|
|
||||||
def on_resolver_connection(connection)
|
@current_session.select_resolver(resolver, @current_selector)
|
||||||
emit(:resolve, connection)
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def on_resolver_error(connection, error)
|
|
||||||
emit(:error, connection, error)
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_resolver_close(resolver)
|
|
||||||
emit(:close, resolver)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -164,7 +164,10 @@ module HTTPX
|
|||||||
@connections.delete(connection)
|
@connections.delete(connection)
|
||||||
# This loop_time passed to the exception is bogus. Ideally we would pass the total
|
# This loop_time passed to the exception is bogus. Ideally we would pass the total
|
||||||
# resolve timeout, including from the previous retries.
|
# resolve timeout, including from the previous retries.
|
||||||
raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
|
ex = ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
|
||||||
|
ex.set_backtrace(ex ? ex.backtrace : caller)
|
||||||
|
emit_resolve_error(connection, host, ex)
|
||||||
|
emit(:close, self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -248,7 +251,10 @@ module HTTPX
|
|||||||
|
|
||||||
unless @queries.value?(connection)
|
unless @queries.value?(connection)
|
||||||
@connections.delete(connection)
|
@connections.delete(connection)
|
||||||
raise NativeResolveError.new(connection, connection.origin.host, "name or service not known")
|
ex = NativeResolveError.new(connection, connection.origin.host, "name or service not known")
|
||||||
|
ex.set_backtrace(ex ? ex.backtrace : caller)
|
||||||
|
emit_resolve_error(connection, connection.origin.host, ex)
|
||||||
|
emit(:close, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
resolve
|
resolve
|
||||||
@ -311,7 +317,7 @@ module HTTPX
|
|||||||
# clean up intermediate queries
|
# clean up intermediate queries
|
||||||
@timeouts.delete(name) unless connection.origin.host == name
|
@timeouts.delete(name) unless connection.origin.host == name
|
||||||
|
|
||||||
if catch(:coalesced) { early_resolve(connection, hostname: hostname_alias) }
|
if early_resolve(connection, hostname: hostname_alias)
|
||||||
@connections.delete(connection)
|
@connections.delete(connection)
|
||||||
else
|
else
|
||||||
if @socket_type == :tcp
|
if @socket_type == :tcp
|
||||||
@ -332,7 +338,7 @@ module HTTPX
|
|||||||
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
|
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return emit(:close) if @connections.empty?
|
return emit(:close, self) if @connections.empty?
|
||||||
|
|
||||||
resolve
|
resolve
|
||||||
end
|
end
|
||||||
@ -358,7 +364,10 @@ module HTTPX
|
|||||||
begin
|
begin
|
||||||
@write_buffer << encode_dns_query(hostname)
|
@write_buffer << encode_dns_query(hostname)
|
||||||
rescue Resolv::DNS::EncodeError => e
|
rescue Resolv::DNS::EncodeError => e
|
||||||
|
reset_hostname(hostname, connection: connection)
|
||||||
|
@connections.delete(connection)
|
||||||
emit_resolve_error(connection, hostname, e)
|
emit_resolve_error(connection, hostname, e)
|
||||||
|
emit(:close, self) if @connections.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -435,12 +444,17 @@ module HTTPX
|
|||||||
def handle_error(error)
|
def handle_error(error)
|
||||||
if error.respond_to?(:connection) &&
|
if error.respond_to?(:connection) &&
|
||||||
error.respond_to?(:host)
|
error.respond_to?(:host)
|
||||||
|
reset_hostname(error.host, connection: error.connection)
|
||||||
|
@connections.delete(error.connection)
|
||||||
emit_resolve_error(error.connection, error.host, error)
|
emit_resolve_error(error.connection, error.host, error)
|
||||||
else
|
else
|
||||||
@queries.each do |host, connection|
|
@queries.each do |host, connection|
|
||||||
|
reset_hostname(host, connection: connection)
|
||||||
|
@connections.delete(connection)
|
||||||
emit_resolve_error(connection, host, error)
|
emit_resolve_error(connection, host, error)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
emit(:close, self) if @connections.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
|
def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
|
||||||
|
|||||||
@ -26,14 +26,26 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :family
|
attr_reader :family, :options
|
||||||
|
|
||||||
attr_writer :pool
|
attr_writer :current_selector, :current_session
|
||||||
|
|
||||||
|
attr_accessor :multi
|
||||||
|
|
||||||
def initialize(family, options)
|
def initialize(family, options)
|
||||||
@family = family
|
@family = family
|
||||||
@record_type = RECORD_TYPES[family]
|
@record_type = RECORD_TYPES[family]
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
set_resolver_callbacks
|
||||||
|
end
|
||||||
|
|
||||||
|
def each_connection(&block)
|
||||||
|
enum_for(__method__) unless block
|
||||||
|
|
||||||
|
return unless @connections
|
||||||
|
|
||||||
|
@connections.each(&block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def close; end
|
def close; end
|
||||||
@ -48,6 +60,10 @@ module HTTPX
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inflight?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def emit_addresses(connection, family, addresses, early_resolve = false)
|
def emit_addresses(connection, family, addresses, early_resolve = false)
|
||||||
addresses.map! do |address|
|
addresses.map! do |address|
|
||||||
address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
|
address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
|
||||||
@ -57,13 +73,13 @@ module HTTPX
|
|||||||
return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
|
return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
|
||||||
|
|
||||||
log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
|
log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
|
||||||
if @pool && # if triggered by early resolve, pool may not be here yet
|
if @current_selector && # if triggered by early resolve, session may not be here yet
|
||||||
!connection.io &&
|
!connection.io &&
|
||||||
connection.options.ip_families.size > 1 &&
|
connection.options.ip_families.size > 1 &&
|
||||||
family == Socket::AF_INET &&
|
family == Socket::AF_INET &&
|
||||||
addresses.first.to_s != connection.origin.host.to_s
|
addresses.first.to_s != connection.origin.host.to_s
|
||||||
log { "resolver: A response, applying resolution delay..." }
|
log { "resolver: A response, applying resolution delay..." }
|
||||||
@pool.after(0.05) do
|
@current_selector.after(0.05) do
|
||||||
unless connection.state == :closed ||
|
unless connection.state == :closed ||
|
||||||
# double emission check
|
# double emission check
|
||||||
(connection.addresses && addresses.intersect?(connection.addresses))
|
(connection.addresses && addresses.intersect?(connection.addresses))
|
||||||
@ -102,10 +118,12 @@ module HTTPX
|
|||||||
return if addresses.empty?
|
return if addresses.empty?
|
||||||
|
|
||||||
emit_addresses(connection, @family, addresses, true)
|
emit_addresses(connection, @family, addresses, true)
|
||||||
|
|
||||||
|
addresses
|
||||||
end
|
end
|
||||||
|
|
||||||
def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
|
def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
|
||||||
emit(:error, connection, resolve_error(hostname, ex))
|
emit_connection_error(connection, resolve_error(hostname, ex))
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_error(hostname, ex = nil)
|
def resolve_error(hostname, ex = nil)
|
||||||
@ -116,5 +134,25 @@ module HTTPX
|
|||||||
error.set_backtrace(ex ? ex.backtrace : caller)
|
error.set_backtrace(ex ? ex.backtrace : caller)
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_resolver_callbacks
|
||||||
|
on(:resolve, &method(:resolve_connection))
|
||||||
|
on(:error, &method(:emit_connection_error))
|
||||||
|
on(:close, &method(:close_resolver))
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_connection(connection)
|
||||||
|
@current_session.__send__(:on_resolver_connection, connection, @current_selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
def emit_connection_error(connection, error)
|
||||||
|
return connection.emit(:connect_error, error) if connection.connecting? && connection.callbacks_for?(:connect_error)
|
||||||
|
|
||||||
|
connection.emit(:error, error)
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_resolver(resolver)
|
||||||
|
@current_session.__send__(:on_resolver_close, resolver, @current_selector)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -47,8 +47,12 @@ module HTTPX
|
|||||||
yield self
|
yield self
|
||||||
end
|
end
|
||||||
|
|
||||||
def connections
|
def multi
|
||||||
EMPTY
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
@ -92,6 +96,11 @@ module HTTPX
|
|||||||
resolve
|
resolve
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def early_resolve(connection)
|
||||||
|
self << connection
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def handle_socket_timeout(interval)
|
def handle_socket_timeout(interval)
|
||||||
error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
|
error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
|
||||||
error.set_backtrace(caller)
|
error.set_backtrace(caller)
|
||||||
@ -120,23 +129,26 @@ module HTTPX
|
|||||||
def consume
|
def consume
|
||||||
return if @connections.empty?
|
return if @connections.empty?
|
||||||
|
|
||||||
while @pipe_read.ready? && (event = @pipe_read.getbyte)
|
if @pipe_read.wait_readable
|
||||||
|
event = @pipe_read.getbyte
|
||||||
|
|
||||||
case event
|
case event
|
||||||
when DONE
|
when DONE
|
||||||
*pair, addrs = @pipe_mutex.synchronize { @ips.pop }
|
*pair, addrs = @pipe_mutex.synchronize { @ips.pop }
|
||||||
@queries.delete(pair)
|
@queries.delete(pair)
|
||||||
|
_, connection = pair
|
||||||
|
@connections.delete(connection)
|
||||||
|
|
||||||
family, connection = pair
|
family, connection = pair
|
||||||
catch(:coalesced) { emit_addresses(connection, family, addrs) }
|
catch(:coalesced) { emit_addresses(connection, family, addrs) }
|
||||||
when ERROR
|
when ERROR
|
||||||
*pair, error = @pipe_mutex.synchronize { @ips.pop }
|
*pair, error = @pipe_mutex.synchronize { @ips.pop }
|
||||||
@queries.delete(pair)
|
@queries.delete(pair)
|
||||||
|
@connections.delete(connection)
|
||||||
|
|
||||||
family, connection = pair
|
_, connection = pair
|
||||||
emit_resolve_error(connection, connection.origin.host, error)
|
emit_resolve_error(connection, connection.origin.host, error)
|
||||||
end
|
end
|
||||||
|
|
||||||
@connections.delete(connection) if @queries.empty?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return emit(:close, self) if @connections.empty?
|
return emit(:close, self) if @connections.empty?
|
||||||
@ -210,5 +222,11 @@ module HTTPX
|
|||||||
def __addrinfo_resolve(host, scheme)
|
def __addrinfo_resolve(host, scheme)
|
||||||
Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
|
Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def emit_connection_error(_, error)
|
||||||
|
throw(:resolve_error, error)
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_resolver(resolver); end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -2,137 +2,206 @@
|
|||||||
|
|
||||||
require "io/wait"
|
require "io/wait"
|
||||||
|
|
||||||
class HTTPX::Selector
|
module HTTPX
|
||||||
READABLE = %i[rw r].freeze
|
class Selector
|
||||||
WRITABLE = %i[rw w].freeze
|
extend Forwardable
|
||||||
|
|
||||||
private_constant :READABLE
|
READABLE = %i[rw r].freeze
|
||||||
private_constant :WRITABLE
|
WRITABLE = %i[rw w].freeze
|
||||||
|
|
||||||
def initialize
|
private_constant :READABLE
|
||||||
@selectables = []
|
private_constant :WRITABLE
|
||||||
end
|
|
||||||
|
|
||||||
# deregisters +io+ from selectables.
|
def_delegator :@timers, :after
|
||||||
def deregister(io)
|
|
||||||
@selectables.delete(io)
|
|
||||||
end
|
|
||||||
|
|
||||||
# register +io+.
|
def_delegator :@selectables, :empty?
|
||||||
def register(io)
|
|
||||||
return if @selectables.include?(io)
|
|
||||||
|
|
||||||
@selectables << io
|
def initialize
|
||||||
end
|
@timers = Timers.new
|
||||||
|
@selectables = []
|
||||||
|
end
|
||||||
|
|
||||||
private
|
def each(&blk)
|
||||||
|
@selectables.each(&blk)
|
||||||
|
end
|
||||||
|
|
||||||
def select_many(interval, &block)
|
def next_tick
|
||||||
selectables, r, w = nil
|
catch(:jump_tick) do
|
||||||
|
timeout = next_timeout
|
||||||
|
if timeout && timeout.negative?
|
||||||
|
@timers.fire
|
||||||
|
throw(:jump_tick)
|
||||||
|
end
|
||||||
|
|
||||||
# first, we group IOs based on interest type. On call to #interests however,
|
|
||||||
# things might already happen, and new IOs might be registered, so we might
|
|
||||||
# have to start all over again. We do this until we group all selectables
|
|
||||||
begin
|
|
||||||
loop do
|
|
||||||
begin
|
begin
|
||||||
r = nil
|
select(timeout, &:call)
|
||||||
w = nil
|
@timers.fire
|
||||||
|
rescue TimeoutError => e
|
||||||
|
@timers.fire(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
emit_error(e)
|
||||||
|
rescue Exception # rubocop:disable Lint/RescueException
|
||||||
|
each_connection(&:force_reset)
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
selectables = @selectables
|
def terminate
|
||||||
@selectables = []
|
# array may change during iteration
|
||||||
|
selectables = @selectables.reject(&:inflight?)
|
||||||
|
|
||||||
selectables.delete_if do |io|
|
selectables.each(&:terminate)
|
||||||
interests = io.interests
|
|
||||||
|
|
||||||
(r ||= []) << io if READABLE.include?(interests)
|
until selectables.empty?
|
||||||
(w ||= []) << io if WRITABLE.include?(interests)
|
next_tick
|
||||||
|
|
||||||
io.state == :closed
|
selectables &= @selectables
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if @selectables.empty?
|
def find_resolver(options)
|
||||||
@selectables = selectables
|
res = @selectables.find do |c|
|
||||||
|
c.is_a?(Resolver::Resolver) && options == c.options
|
||||||
|
end
|
||||||
|
|
||||||
# do not run event loop if there's nothing to wait on.
|
res.multi if res
|
||||||
# this might happen if connect failed and connection was unregistered.
|
end
|
||||||
return if (!r || r.empty?) && (!w || w.empty?) && !selectables.empty?
|
|
||||||
|
|
||||||
break
|
def each_connection(&block)
|
||||||
else
|
return enum_for(__method__) unless block
|
||||||
@selectables.concat(selectables)
|
|
||||||
end
|
@selectables.each do |c|
|
||||||
rescue StandardError
|
if c.is_a?(Resolver::Resolver)
|
||||||
@selectables = selectables if selectables
|
c.each_connection(&block)
|
||||||
raise
|
else
|
||||||
|
yield c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_connection(request_uri, options)
|
||||||
|
each_connection.find do |connection|
|
||||||
|
connection.match?(request_uri, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_mergeable_connection(connection)
|
||||||
|
each_connection.find do |ch|
|
||||||
|
ch != connection && ch.mergeable?(connection)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
@selectables.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
# deregisters +io+ from selectables.
|
||||||
|
def deregister(io)
|
||||||
|
@selectables.delete(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
# register +io+.
|
||||||
|
def register(io)
|
||||||
|
return if @selectables.include?(io)
|
||||||
|
|
||||||
|
@selectables << io
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def select(interval, &block)
|
||||||
|
# do not cause an infinite loop here.
|
||||||
|
#
|
||||||
|
# this may happen if timeout calculation actually triggered an error which causes
|
||||||
|
# the connections to be reaped (such as the total timeout error) before #select
|
||||||
|
# gets called.
|
||||||
|
return if interval.nil? && @selectables.empty?
|
||||||
|
|
||||||
|
return select_one(interval, &block) if @selectables.size == 1
|
||||||
|
|
||||||
|
select_many(interval, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_many(interval, &block)
|
||||||
|
r, w = nil
|
||||||
|
|
||||||
|
# first, we group IOs based on interest type. On call to #interests however,
|
||||||
|
# things might already happen, and new IOs might be registered, so we might
|
||||||
|
# have to start all over again. We do this until we group all selectables
|
||||||
|
begin
|
||||||
|
@selectables.delete_if do |io|
|
||||||
|
interests = io.interests
|
||||||
|
|
||||||
|
(r ||= []) << io if READABLE.include?(interests)
|
||||||
|
(w ||= []) << io if WRITABLE.include?(interests)
|
||||||
|
|
||||||
|
io.state == :closed
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: what to do if there are no selectables?
|
||||||
|
|
||||||
|
readers, writers = IO.select(r, w, nil, interval)
|
||||||
|
|
||||||
|
if readers.nil? && writers.nil? && interval
|
||||||
|
[*r, *w].each { |io| io.handle_socket_timeout(interval) }
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: what to do if there are no selectables?
|
if writers
|
||||||
|
readers.each do |io|
|
||||||
|
yield io
|
||||||
|
|
||||||
readers, writers = IO.select(r, w, nil, interval)
|
# so that we don't yield 2 times
|
||||||
|
writers.delete(io)
|
||||||
|
end if readers
|
||||||
|
|
||||||
if readers.nil? && writers.nil? && interval
|
writers.each(&block)
|
||||||
[*r, *w].each { |io| io.handle_socket_timeout(interval) }
|
else
|
||||||
|
readers.each(&block) if readers
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_one(interval)
|
||||||
|
io = @selectables.first
|
||||||
|
|
||||||
|
return unless io
|
||||||
|
|
||||||
|
interests = io.interests
|
||||||
|
|
||||||
|
result = case interests
|
||||||
|
when :r then io.to_io.wait_readable(interval)
|
||||||
|
when :w then io.to_io.wait_writable(interval)
|
||||||
|
when :rw then io.to_io.wait(interval, :read_write)
|
||||||
|
when nil then return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless result || interval.nil?
|
||||||
|
io.handle_socket_timeout(interval)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
rescue IOError, SystemCallError
|
# raise TimeoutError.new(interval, "timed out while waiting on select")
|
||||||
@selectables.reject!(&:closed?)
|
|
||||||
retry
|
yield io
|
||||||
|
# rescue IOError, SystemCallError
|
||||||
|
# @selectables.reject!(&:closed?)
|
||||||
|
# raise unless @selectables.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
if writers
|
def next_timeout
|
||||||
readers.each do |io|
|
[
|
||||||
yield io
|
@timers.wait_interval,
|
||||||
|
@selectables.filter_map(&:timeout).min,
|
||||||
|
].compact.min
|
||||||
|
end
|
||||||
|
|
||||||
# so that we don't yield 2 times
|
def emit_error(e)
|
||||||
writers.delete(io)
|
@selectables.each do |c|
|
||||||
end if readers
|
next if c.is_a?(Resolver::Resolver)
|
||||||
|
|
||||||
writers.each(&block)
|
c.emit(:error, e)
|
||||||
else
|
end
|
||||||
readers.each(&block) if readers
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_one(interval)
|
|
||||||
io = @selectables.first
|
|
||||||
|
|
||||||
return unless io
|
|
||||||
|
|
||||||
interests = io.interests
|
|
||||||
|
|
||||||
result = case interests
|
|
||||||
when :r then io.to_io.wait_readable(interval)
|
|
||||||
when :w then io.to_io.wait_writable(interval)
|
|
||||||
when :rw then io.to_io.wait(interval, :read_write)
|
|
||||||
when nil then return
|
|
||||||
end
|
|
||||||
|
|
||||||
unless result || interval.nil?
|
|
||||||
io.handle_socket_timeout(interval)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
# raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
|
|
||||||
|
|
||||||
yield io
|
|
||||||
rescue IOError, SystemCallError
|
|
||||||
@selectables.reject!(&:closed?)
|
|
||||||
raise unless @selectables.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def select(interval, &block)
|
|
||||||
# do not cause an infinite loop here.
|
|
||||||
#
|
|
||||||
# this may happen if timeout calculation actually triggered an error which causes
|
|
||||||
# the connections to be reaped (such as the total timeout error) before #select
|
|
||||||
# gets called.
|
|
||||||
return if interval.nil? && @selectables.empty?
|
|
||||||
|
|
||||||
return select_one(interval, &block) if @selectables.size == 1
|
|
||||||
|
|
||||||
select_many(interval, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
public :select
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -19,6 +19,7 @@ module HTTPX
|
|||||||
@options = self.class.default_options.merge(options)
|
@options = self.class.default_options.merge(options)
|
||||||
@responses = {}
|
@responses = {}
|
||||||
@persistent = @options.persistent
|
@persistent = @options.persistent
|
||||||
|
@wrapped = false
|
||||||
wrap(&blk) if blk
|
wrap(&blk) if blk
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -28,21 +29,49 @@ module HTTPX
|
|||||||
# http.get("https://wikipedia.com")
|
# http.get("https://wikipedia.com")
|
||||||
# end # wikipedia connection closes here
|
# end # wikipedia connection closes here
|
||||||
def wrap
|
def wrap
|
||||||
prev_persistent = @persistent
|
prev_wrapped = @wrapped
|
||||||
@persistent = true
|
@wrapped = true
|
||||||
pool.wrap do
|
prev_selector = Thread.current[:httpx_selector]
|
||||||
begin
|
Thread.current[:httpx_selector] = current_selector = Selector.new
|
||||||
yield self
|
begin
|
||||||
ensure
|
yield self
|
||||||
@persistent = prev_persistent
|
ensure
|
||||||
close unless @persistent
|
unless prev_wrapped
|
||||||
|
if @persistent
|
||||||
|
deactivate(current_selector)
|
||||||
|
else
|
||||||
|
close(current_selector)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@wrapped = prev_wrapped
|
||||||
|
Thread.current[:httpx_selector] = prev_selector
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# closes all the active connections from the session
|
# closes all the active connections from the session.
|
||||||
def close(*args)
|
#
|
||||||
pool.close(*args)
|
# when called directly with +selector+ as nil, all available connections
|
||||||
|
# will be picked up from the connection pool and closed. Connections in use
|
||||||
|
# by other sessions, or same session in a different thread, will not be reaped.
|
||||||
|
def close(selector = nil)
|
||||||
|
if selector.nil?
|
||||||
|
selector = Selector.new
|
||||||
|
|
||||||
|
while (connection = pool.checkout_by_options(@options))
|
||||||
|
connection.current_session = self
|
||||||
|
connection.current_selector = selector
|
||||||
|
select_connection(connection, selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
return close(selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
@closing = true
|
||||||
|
selector.terminate
|
||||||
|
ensure
|
||||||
|
@closing = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# performs one, or multple requests; it accepts:
|
# performs one, or multple requests; it accepts:
|
||||||
@ -89,8 +118,106 @@ module HTTPX
|
|||||||
request
|
request
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def select_connection(connection, selector)
|
||||||
|
selector.register(connection)
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :select_resolver, :select_connection
|
||||||
|
|
||||||
|
def deselect_connection(connection, selector, cloned = false)
|
||||||
|
selector.deregister(connection)
|
||||||
|
|
||||||
|
return if cloned
|
||||||
|
|
||||||
|
pool.checkin_connection(connection)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deselect_resolver(resolver, selector)
|
||||||
|
selector.deregister(resolver)
|
||||||
|
|
||||||
|
pool.checkin_resolver(resolver)
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_clone_connection(connection, selector, family)
|
||||||
|
connection.family ||= family
|
||||||
|
|
||||||
|
return connection if connection.family == family
|
||||||
|
|
||||||
|
new_connection = connection.class.new(connection.origin, connection.options)
|
||||||
|
|
||||||
|
new_connection.family = family
|
||||||
|
new_connection.current_session = self
|
||||||
|
new_connection.current_selector = selector
|
||||||
|
|
||||||
|
connection.once(:tcp_open) { new_connection.force_reset(true) }
|
||||||
|
connection.once(:connect_error) do |err|
|
||||||
|
if new_connection.connecting?
|
||||||
|
new_connection.merge(connection)
|
||||||
|
connection.emit(:cloned, new_connection)
|
||||||
|
connection.force_reset(true)
|
||||||
|
else
|
||||||
|
connection.__send__(:handle_error, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
new_connection.once(:tcp_open) do |new_conn|
|
||||||
|
if new_conn != connection
|
||||||
|
new_conn.merge(connection)
|
||||||
|
connection.force_reset(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
new_connection.once(:connect_error) do |err|
|
||||||
|
if connection.connecting?
|
||||||
|
# main connection has the requests
|
||||||
|
connection.merge(new_connection)
|
||||||
|
new_connection.emit(:cloned, connection)
|
||||||
|
new_connection.force_reset(true)
|
||||||
|
else
|
||||||
|
new_connection.__send__(:handle_error, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do_init_connection(new_connection, selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns the HTTPX::Connection through which the +request+ should be sent through.
|
||||||
|
def find_connection(request_uri, selector, options)
|
||||||
|
if (connection = selector.find_connection(request_uri, options))
|
||||||
|
return connection
|
||||||
|
end
|
||||||
|
|
||||||
|
connection = pool.checkout_connection(request_uri, options)
|
||||||
|
|
||||||
|
connection.current_session = self
|
||||||
|
connection.current_selector = selector
|
||||||
|
|
||||||
|
case connection.state
|
||||||
|
when :idle
|
||||||
|
do_init_connection(connection, selector)
|
||||||
|
when :open
|
||||||
|
select_connection(connection, selector) if options.io
|
||||||
|
when :closed
|
||||||
|
connection.idling
|
||||||
|
select_connection(connection, selector)
|
||||||
|
when :closing
|
||||||
|
connection.once(:close) do
|
||||||
|
connection.idling
|
||||||
|
select_connection(connection, selector)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
connection
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def deactivate(selector)
|
||||||
|
selector.each_connection do |connection|
|
||||||
|
connection.deactivate
|
||||||
|
deselect_connection(connection, selector) if connection.state == :inactive
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# returns the HTTPX::Pool object which manages the networking required to
|
# returns the HTTPX::Pool object which manages the networking required to
|
||||||
# perform requests.
|
# perform requests.
|
||||||
def pool
|
def pool
|
||||||
@ -109,26 +236,14 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
# returns the corresponding HTTP::Response to the given +request+ if it has been received.
|
# returns the corresponding HTTP::Response to the given +request+ if it has been received.
|
||||||
def fetch_response(request, _, _)
|
def fetch_response(request, _selector, _options)
|
||||||
@responses.delete(request)
|
@responses.delete(request)
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns the HTTPX::Connection through which the +request+ should be sent through.
|
|
||||||
def find_connection(request, connections, options)
|
|
||||||
uri = request.uri
|
|
||||||
|
|
||||||
connection = pool.find_connection(uri, options) || init_connection(uri, options)
|
|
||||||
unless connections.nil? || connections.include?(connection)
|
|
||||||
connections << connection
|
|
||||||
set_connection_callbacks(connection, connections, options)
|
|
||||||
end
|
|
||||||
connection
|
|
||||||
end
|
|
||||||
|
|
||||||
# sends the +request+ to the corresponding HTTPX::Connection
|
# sends the +request+ to the corresponding HTTPX::Connection
|
||||||
def send_request(request, connections, options = request.options)
|
def send_request(request, selector, options = request.options)
|
||||||
error = catch(:resolve_error) do
|
error = catch(:resolve_error) do
|
||||||
connection = find_connection(request, connections, options)
|
connection = find_connection(request.uri, selector, options)
|
||||||
connection.send(request)
|
connection.send(request)
|
||||||
end
|
end
|
||||||
return unless error.is_a?(Error)
|
return unless error.is_a?(Error)
|
||||||
@ -136,61 +251,6 @@ module HTTPX
|
|||||||
request.emit(:response, ErrorResponse.new(request, error))
|
request.emit(:response, ErrorResponse.new(request, error))
|
||||||
end
|
end
|
||||||
|
|
||||||
# sets the callbacks on the +connection+ required to process certain specific
|
|
||||||
# connection lifecycle events which deal with request rerouting.
|
|
||||||
def set_connection_callbacks(connection, connections, options, cloned: false)
|
|
||||||
connection.only(:misdirected) do |misdirected_request|
|
|
||||||
other_connection = connection.create_idle(ssl: { alpn_protocols: %w[http/1.1] })
|
|
||||||
other_connection.merge(connection)
|
|
||||||
catch(:coalesced) do
|
|
||||||
pool.init_connection(other_connection, options)
|
|
||||||
end
|
|
||||||
set_connection_callbacks(other_connection, connections, options)
|
|
||||||
connections << other_connection
|
|
||||||
misdirected_request.transition(:idle)
|
|
||||||
other_connection.send(misdirected_request)
|
|
||||||
end
|
|
||||||
connection.only(:altsvc) do |alt_origin, origin, alt_params|
|
|
||||||
other_connection = build_altsvc_connection(connection, connections, alt_origin, origin, alt_params, options)
|
|
||||||
connections << other_connection if other_connection
|
|
||||||
end
|
|
||||||
connection.only(:cloned) do |cloned_conn|
|
|
||||||
set_connection_callbacks(cloned_conn, connections, options, cloned: true)
|
|
||||||
connections << cloned_conn
|
|
||||||
end unless cloned
|
|
||||||
end
|
|
||||||
|
|
||||||
# returns an HTTPX::Connection for the negotiated Alternative Service (or none).
|
|
||||||
def build_altsvc_connection(existing_connection, connections, alt_origin, origin, alt_params, options)
|
|
||||||
# do not allow security downgrades on altsvc negotiation
|
|
||||||
return if existing_connection.origin.scheme == "https" && alt_origin.scheme != "https"
|
|
||||||
|
|
||||||
altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
|
|
||||||
|
|
||||||
# altsvc already exists, somehow it wasn't advertised, probably noop
|
|
||||||
return unless altsvc
|
|
||||||
|
|
||||||
alt_options = options.merge(ssl: options.ssl.merge(hostname: URI(origin).host))
|
|
||||||
|
|
||||||
connection = pool.find_connection(alt_origin, alt_options) || init_connection(alt_origin, alt_options)
|
|
||||||
|
|
||||||
# advertised altsvc is the same origin being used, ignore
|
|
||||||
return if connection == existing_connection
|
|
||||||
|
|
||||||
connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
|
|
||||||
|
|
||||||
set_connection_callbacks(connection, connections, alt_options)
|
|
||||||
|
|
||||||
log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
|
|
||||||
|
|
||||||
connection.merge(existing_connection)
|
|
||||||
existing_connection.terminate
|
|
||||||
connection
|
|
||||||
rescue UnsupportedSchemeError
|
|
||||||
altsvc["noop"] = true
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# returns a set of HTTPX::Request objects built from the given +args+ and +options+.
|
# returns a set of HTTPX::Request objects built from the given +args+ and +options+.
|
||||||
def build_requests(*args, params)
|
def build_requests(*args, params)
|
||||||
requests = if args.size == 1
|
requests = if args.size == 1
|
||||||
@ -224,12 +284,9 @@ module HTTPX
|
|||||||
request.on(:promise, &method(:on_promise))
|
request.on(:promise, &method(:on_promise))
|
||||||
end
|
end
|
||||||
|
|
||||||
def init_connection(uri, options)
|
def do_init_connection(connection, selector)
|
||||||
connection = options.connection_class.new(uri, options)
|
resolve_connection(connection, selector) unless connection.family
|
||||||
catch(:coalesced) do
|
connection
|
||||||
pool.init_connection(connection, options)
|
|
||||||
connection
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def deactivate_connection(request, connections, options)
|
def deactivate_connection(request, connections, options)
|
||||||
@ -242,64 +299,128 @@ module HTTPX
|
|||||||
|
|
||||||
# sends an array of HTTPX::Request +requests+, returns the respective array of HTTPX::Response objects.
|
# sends an array of HTTPX::Request +requests+, returns the respective array of HTTPX::Response objects.
|
||||||
def send_requests(*requests)
|
def send_requests(*requests)
|
||||||
connections = _send_requests(requests)
|
selector = Thread.current[:httpx_selector] || Selector.new
|
||||||
receive_requests(requests, connections)
|
_send_requests(requests, selector)
|
||||||
|
receive_requests(requests, selector)
|
||||||
|
ensure
|
||||||
|
unless @wrapped
|
||||||
|
if @persistent
|
||||||
|
deactivate(selector)
|
||||||
|
else
|
||||||
|
close(selector)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# sends an array of HTTPX::Request objects
|
# sends an array of HTTPX::Request objects
|
||||||
def _send_requests(requests)
|
def _send_requests(requests, selector)
|
||||||
connections = []
|
|
||||||
|
|
||||||
requests.each do |request|
|
requests.each do |request|
|
||||||
send_request(request, connections)
|
send_request(request, selector)
|
||||||
end
|
end
|
||||||
|
|
||||||
connections
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
|
# returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
|
||||||
def receive_requests(requests, connections)
|
def receive_requests(requests, selector)
|
||||||
# @type var responses: Array[response]
|
# @type var responses: Array[response]
|
||||||
responses = []
|
responses = []
|
||||||
|
|
||||||
begin
|
# guarantee ordered responses
|
||||||
# guarantee ordered responses
|
loop do
|
||||||
loop do
|
request = requests.first
|
||||||
request = requests.first
|
|
||||||
|
|
||||||
return responses unless request
|
return responses unless request
|
||||||
|
|
||||||
catch(:coalesced) { pool.next_tick(connections) } until (response = fetch_response(request, connections, request.options))
|
catch(:coalesced) { selector.next_tick } until (response = fetch_response(request, selector, request.options))
|
||||||
request.emit(:complete, response)
|
request.emit(:complete, response)
|
||||||
|
|
||||||
|
responses << response
|
||||||
|
requests.shift
|
||||||
|
|
||||||
|
break if requests.empty?
|
||||||
|
|
||||||
|
next unless selector.empty?
|
||||||
|
|
||||||
|
# in some cases, the pool of connections might have been drained because there was some
|
||||||
|
# handshake error, and the error responses have already been emitted, but there was no
|
||||||
|
# opportunity to traverse the requests, hence we're returning only a fraction of the errors
|
||||||
|
# we were supposed to. This effectively fetches the existing responses and return them.
|
||||||
|
while (request = requests.shift)
|
||||||
|
response = fetch_response(request, selector, request.options)
|
||||||
|
request.emit(:complete, response) if response
|
||||||
responses << response
|
responses << response
|
||||||
requests.shift
|
|
||||||
|
|
||||||
break if requests.empty?
|
|
||||||
|
|
||||||
next unless pool.empty?
|
|
||||||
|
|
||||||
# in some cases, the pool of connections might have been drained because there was some
|
|
||||||
# handshake error, and the error responses have already been emitted, but there was no
|
|
||||||
# opportunity to traverse the requests, hence we're returning only a fraction of the errors
|
|
||||||
# we were supposed to. This effectively fetches the existing responses and return them.
|
|
||||||
while (request = requests.shift)
|
|
||||||
response = fetch_response(request, connections, request.options)
|
|
||||||
request.emit(:complete, response) if response
|
|
||||||
responses << response
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
responses
|
break
|
||||||
ensure
|
end
|
||||||
if @persistent
|
responses
|
||||||
pool.deactivate(*connections)
|
end
|
||||||
else
|
|
||||||
close(connections)
|
def resolve_connection(connection, selector)
|
||||||
|
if connection.addresses || connection.open?
|
||||||
|
#
|
||||||
|
# there are two cases in which we want to activate initialization of
|
||||||
|
# connection immediately:
|
||||||
|
#
|
||||||
|
# 1. when the connection already has addresses, i.e. it doesn't need to
|
||||||
|
# resolve a name (not the same as name being an IP, yet)
|
||||||
|
# 2. when the connection is initialized with an external already open IO.
|
||||||
|
#
|
||||||
|
connection.once(:connect_error, &connection.method(:handle_error))
|
||||||
|
on_resolver_connection(connection, selector)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
resolver = find_resolver_for(connection, selector)
|
||||||
|
|
||||||
|
resolver.early_resolve(connection) || resolver.lazy_resolve(connection)
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_resolver_connection(connection, selector)
|
||||||
|
found_connection = selector.find_mergeable_connection(connection) ||
|
||||||
|
pool.checkout_mergeable_connection(connection)
|
||||||
|
|
||||||
|
return select_connection(connection, selector) unless found_connection
|
||||||
|
|
||||||
|
if found_connection.open?
|
||||||
|
coalesce_connections(found_connection, connection, selector)
|
||||||
|
else
|
||||||
|
found_connection.once(:open) do
|
||||||
|
coalesce_connections(found_connection, connection, selector)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_resolver_close(resolver, selector)
|
||||||
|
return if resolver.closed?
|
||||||
|
|
||||||
|
deselect_resolver(resolver, selector)
|
||||||
|
resolver.close unless resolver.closed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_resolver_for(connection, selector)
|
||||||
|
resolver = selector.find_resolver(connection.options)
|
||||||
|
|
||||||
|
unless resolver
|
||||||
|
resolver = pool.checkout_resolver(connection.options)
|
||||||
|
resolver.current_session = self
|
||||||
|
resolver.current_selector = selector
|
||||||
|
end
|
||||||
|
|
||||||
|
resolver
|
||||||
|
end
|
||||||
|
|
||||||
|
def coalesce_connections(conn1, conn2, selector)
|
||||||
|
unless conn1.coalescable?(conn2)
|
||||||
|
select_connection(conn2, selector)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
conn2.emit(:tcp_open, conn1)
|
||||||
|
conn1.merge(conn2)
|
||||||
|
conn2.coalesced_connection = conn1
|
||||||
|
deselect_connection(conn2, selector)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
@default_options = Options.new
|
@default_options = Options.new
|
||||||
@default_options.freeze
|
@default_options.freeze
|
||||||
@plugins = []
|
@plugins = []
|
||||||
|
|||||||
@ -26,8 +26,9 @@ module HTTPX
|
|||||||
attr_reader pending: Array[Request]
|
attr_reader pending: Array[Request]
|
||||||
attr_reader options: Options
|
attr_reader options: Options
|
||||||
attr_reader ssl_session: OpenSSL::SSL::Session?
|
attr_reader ssl_session: OpenSSL::SSL::Session?
|
||||||
attr_writer timers: Timers
|
attr_writer current_selector: Selector?
|
||||||
|
attr_writer coalesced_connection: instance?
|
||||||
|
attr_accessor current_session: Session?
|
||||||
attr_accessor family: Integer?
|
attr_accessor family: Integer?
|
||||||
|
|
||||||
@window_size: Integer
|
@window_size: Integer
|
||||||
@ -42,6 +43,8 @@ module HTTPX
|
|||||||
@connected_at: Float
|
@connected_at: Float
|
||||||
@response_received_at: Float
|
@response_received_at: Float
|
||||||
@intervals: Array[Timers::Interval]
|
@intervals: Array[Timers::Interval]
|
||||||
|
@exhausted: bool
|
||||||
|
@cloned: bool
|
||||||
|
|
||||||
def addresses: () -> Array[ipaddr]?
|
def addresses: () -> Array[ipaddr]?
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ module HTTPX
|
|||||||
|
|
||||||
def coalescable?: (Connection connection) -> bool
|
def coalescable?: (Connection connection) -> bool
|
||||||
|
|
||||||
def create_idle: (?Hash[Symbol, untyped] options) -> Connection
|
def create_idle: (?Hash[Symbol, untyped] options) -> instance
|
||||||
|
|
||||||
def merge: (Connection connection) -> void
|
def merge: (Connection connection) -> void
|
||||||
|
|
||||||
@ -77,6 +80,8 @@ module HTTPX
|
|||||||
|
|
||||||
def close: () -> void
|
def close: () -> void
|
||||||
|
|
||||||
|
def force_reset: (?bool cloned) -> void
|
||||||
|
|
||||||
def reset: () -> void
|
def reset: () -> void
|
||||||
|
|
||||||
def timeout: () -> Numeric?
|
def timeout: () -> Numeric?
|
||||||
@ -99,6 +104,8 @@ module HTTPX
|
|||||||
|
|
||||||
def connect: () -> void
|
def connect: () -> void
|
||||||
|
|
||||||
|
def disconnect: () -> void
|
||||||
|
|
||||||
def exhausted?: () -> boolish
|
def exhausted?: () -> boolish
|
||||||
|
|
||||||
def consume: () -> void
|
def consume: () -> void
|
||||||
@ -117,6 +124,8 @@ module HTTPX
|
|||||||
|
|
||||||
def handle_transition: (Symbol nextstate) -> void
|
def handle_transition: (Symbol nextstate) -> void
|
||||||
|
|
||||||
|
def build_altsvc_connection: (URI::Generic alt_origin, String origin, Hash[String, String] alt_params) -> void
|
||||||
|
|
||||||
def build_socket: (?Array[ipaddr]? addrs) -> (TCP | SSL | UNIX)
|
def build_socket: (?Array[ipaddr]? addrs) -> (TCP | SSL | UNIX)
|
||||||
|
|
||||||
def on_error: (HTTPX::TimeoutError | Error | StandardError error, ?Request? request) -> void
|
def on_error: (HTTPX::TimeoutError | Error | StandardError error, ?Request? request) -> void
|
||||||
|
|||||||
@ -1,7 +1,32 @@
|
|||||||
module HTTPX
|
module HTTPX
|
||||||
module Resolver
|
module Resolver
|
||||||
class Multi
|
class Multi
|
||||||
|
attr_reader resolvers: Array[Native | HTTPS]
|
||||||
|
|
||||||
|
attr_reader options: Options
|
||||||
|
|
||||||
|
@current_selector: Selector?
|
||||||
|
@current_session: Session?
|
||||||
|
@resolver_options: Hash[Symbol, untyped]
|
||||||
|
# @errors: Hash[Symbol, untyped]
|
||||||
|
|
||||||
|
def current_selector=: (Selector s) -> void
|
||||||
|
|
||||||
|
def current_session=: (Session s) -> void
|
||||||
|
|
||||||
|
def closed?: () -> bool
|
||||||
|
|
||||||
|
def empty?: () -> bool
|
||||||
|
|
||||||
|
def timeout: () -> Numeric?
|
||||||
|
|
||||||
|
def close: () -> void
|
||||||
|
|
||||||
|
def connections: () -> Array[Connection]
|
||||||
|
|
||||||
def early_resolve: (Connection connection, ?hostname: String) -> void
|
def early_resolve: (Connection connection, ?hostname: String) -> void
|
||||||
|
|
||||||
|
def lazy_resolve: (Connection connection) -> void
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -7,8 +7,6 @@ module HTTPX
|
|||||||
DEFAULTS: Hash[Symbol, untyped]
|
DEFAULTS: Hash[Symbol, untyped]
|
||||||
DNS_PORT: Integer
|
DNS_PORT: Integer
|
||||||
|
|
||||||
attr_reader family: ip_family
|
|
||||||
|
|
||||||
@options: Options
|
@options: Options
|
||||||
@ns_index: Integer
|
@ns_index: Integer
|
||||||
@nameserver: Array[String]
|
@nameserver: Array[String]
|
||||||
|
|||||||
@ -6,8 +6,17 @@ module HTTPX
|
|||||||
|
|
||||||
RECORD_TYPES: Hash[Integer, singleton(Resolv::DNS::Resource)]
|
RECORD_TYPES: Hash[Integer, singleton(Resolv::DNS::Resource)]
|
||||||
|
|
||||||
|
attr_reader family: ip_family
|
||||||
|
|
||||||
|
attr_reader options: Options
|
||||||
|
|
||||||
|
attr_writer current_selector: Selector?
|
||||||
|
|
||||||
|
attr_writer current_session: Session?
|
||||||
|
|
||||||
|
attr_accessor multi: Multi?
|
||||||
|
|
||||||
@record_type: singleton(Resolv::DNS::Resource)
|
@record_type: singleton(Resolv::DNS::Resource)
|
||||||
@options: Options
|
|
||||||
@resolver_options: Hash[Symbol, untyped]
|
@resolver_options: Hash[Symbol, untyped]
|
||||||
@queries: Hash[String, Connection]
|
@queries: Hash[String, Connection]
|
||||||
@system_resolver: Resolv::Hosts
|
@system_resolver: Resolv::Hosts
|
||||||
@ -20,6 +29,8 @@ module HTTPX
|
|||||||
|
|
||||||
def empty?: () -> bool
|
def empty?: () -> bool
|
||||||
|
|
||||||
|
def each_connection: () { (Connection connection) -> void } -> void
|
||||||
|
|
||||||
def emit_addresses: (Connection connection, ip_family family, Array[IPAddr], ?bool early_resolve) -> void
|
def emit_addresses: (Connection connection, ip_family family, Array[IPAddr], ?bool early_resolve) -> void
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -28,7 +39,15 @@ module HTTPX
|
|||||||
|
|
||||||
def initialize: (ip_family? family, Options options) -> void
|
def initialize: (ip_family? family, Options options) -> void
|
||||||
|
|
||||||
def early_resolve: (Connection connection, ?hostname: String) -> boolish
|
def early_resolve: (Connection connection, ?hostname: String) -> Array[IPAddr]?
|
||||||
|
|
||||||
|
def set_resolver_callbacks: () -> void
|
||||||
|
|
||||||
|
def resolve_connection: (Connection connection) -> void
|
||||||
|
|
||||||
|
def emit_connection_error: (Connection connection, StandardError error) -> void
|
||||||
|
|
||||||
|
def close_resolver: (Resolver resolver) -> void
|
||||||
|
|
||||||
def emit_resolve_error: (Connection connection, ?String hostname, ?StandardError) -> void
|
def emit_resolve_error: (Connection connection, ?String hostname, ?StandardError) -> void
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,48 @@
|
|||||||
module HTTPX
|
module HTTPX
|
||||||
|
type selectable = Connection | Resolver::Native
|
||||||
|
|
||||||
class Selector
|
class Selector
|
||||||
type selectable = Connection | Resolver::Native | Resolver::System
|
include _Each[selectable]
|
||||||
|
|
||||||
READABLE: Array[Symbol]
|
READABLE: Array[Symbol]
|
||||||
WRITABLE: Array[Symbol]
|
WRITABLE: Array[Symbol]
|
||||||
|
|
||||||
|
@timers: Timers
|
||||||
|
|
||||||
@selectables: Array[selectable]
|
@selectables: Array[selectable]
|
||||||
|
|
||||||
def register: (selectable io) -> void
|
def next_tick: () -> void
|
||||||
def deregister: (selectable io) -> selectable?
|
|
||||||
|
|
||||||
def select: (Numeric? interval) { (selectable) -> void } -> void
|
def terminate: () -> void
|
||||||
|
|
||||||
|
def find_resolver: (Options options) -> Resolver::Resolver?
|
||||||
|
|
||||||
|
def find_connection: (http_uri request_uri, Options options) -> Connection?
|
||||||
|
|
||||||
|
def each_connection: () { (Connection) -> void} -> void
|
||||||
|
| () -> Enumerable[Connection]
|
||||||
|
|
||||||
|
def find_mergeable_connection: (Connection connection) -> Connection?
|
||||||
|
|
||||||
|
def empty?: () -> bool
|
||||||
|
|
||||||
|
def register: (selectable io) -> void
|
||||||
|
|
||||||
|
def deregister: (selectable io) -> selectable?
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def initialize: () -> untyped
|
def initialize: () -> void
|
||||||
|
|
||||||
|
def select: (Numeric? interval) { (selectable) -> void } -> void
|
||||||
|
|
||||||
def select_many: (Numeric? interval) { (selectable) -> void } -> void
|
def select_many: (Numeric? interval) { (selectable) -> void } -> void
|
||||||
|
|
||||||
def select_one: (Numeric? interval) { (selectable) -> void } -> void
|
def select_one: (Numeric? interval) { (selectable) -> void } -> void
|
||||||
|
|
||||||
|
def next_timeout: () -> Numeric?
|
||||||
|
|
||||||
|
def emit_error: (StandardError e) -> void
|
||||||
end
|
end
|
||||||
|
|
||||||
type io_interests = :r | :w | :rw
|
type io_interests = :r | :w | :rw
|
||||||
|
|||||||
@ -7,20 +7,35 @@ module HTTPX
|
|||||||
|
|
||||||
@options: Options
|
@options: Options
|
||||||
@responses: Hash[Request, response]
|
@responses: Hash[Request, response]
|
||||||
@persistent: bool?
|
@persistent: bool
|
||||||
|
@wrapped: bool
|
||||||
|
|
||||||
def self.plugin: (Symbol | Module plugin, ?options? options) ?{ (Class) -> void } -> singleton(Session)
|
def self.plugin: (Symbol | Module plugin, ?options? options) ?{ (Class) -> void } -> singleton(Session)
|
||||||
|
|
||||||
def wrap: () { (instance) -> void } -> void
|
def wrap: () { (instance) -> void } -> void
|
||||||
|
|
||||||
def close: (*untyped) -> void
|
def close: (?Selector selector) -> void
|
||||||
|
|
||||||
def build_request: (verb verb, generic_uri uri, ?request_params params, ?Options options) -> Request
|
def build_request: (verb verb, generic_uri uri, ?request_params params, ?Options options) -> Request
|
||||||
|
|
||||||
|
def select_connection: (Connection connection, Selector selector) -> void
|
||||||
|
|
||||||
|
def deselect_connection: (Connection connection, Selector selector, ?bool cloned) -> void
|
||||||
|
|
||||||
|
def select_resolver: (Resolver::Native | Resolver::HTTPS resolver, Selector selector) -> void
|
||||||
|
|
||||||
|
def deselect_resolver: (Resolver::Resolver resolver, Selector selector) -> void
|
||||||
|
|
||||||
|
def try_clone_connection: (Connection connection, Selector selector, Integer? family) -> Connection
|
||||||
|
|
||||||
|
def find_connection: (http_uri request_uri, Selector selector, Options options) -> Connection
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def initialize: (?options) { (self) -> void } -> void
|
def initialize: (?options) { (self) -> void } -> void
|
||||||
| (?options) -> void
|
| (?options) -> void
|
||||||
|
|
||||||
private
|
def deactivate: (Selector selector) -> void
|
||||||
|
|
||||||
def pool: -> Pool
|
def pool: -> Pool
|
||||||
|
|
||||||
@ -28,33 +43,35 @@ module HTTPX
|
|||||||
|
|
||||||
def on_promise: (untyped, untyped) -> void
|
def on_promise: (untyped, untyped) -> void
|
||||||
|
|
||||||
def fetch_response: (Request request, Array[Connection] connections, untyped options) -> response?
|
def fetch_response: (Request request, Selector selector, Options options) -> response?
|
||||||
|
|
||||||
def find_connection: (Request request, Array[Connection] connections, Options options) -> Connection
|
def send_request: (Request request, Selector selector, ?Options options) -> void
|
||||||
|
|
||||||
def deactivate_connection: (Request request, Array[Connection] connections, Options options) -> void
|
|
||||||
|
|
||||||
def send_request: (Request request, Array[Connection] connections, ?Options options) -> void
|
|
||||||
|
|
||||||
def set_connection_callbacks: (Connection connection, Array[Connection] connections, Options options, ?cloned: bool) -> void
|
|
||||||
|
|
||||||
def set_request_callbacks: (Request request) -> void
|
def set_request_callbacks: (Request request) -> void
|
||||||
|
|
||||||
def build_altsvc_connection: (Connection existing_connection, Array[Connection] connections, URI::Generic alt_origin, String origin, Hash[String, String] alt_params, Options options) -> (Connection & AltSvc::ConnectionMixin)?
|
|
||||||
|
|
||||||
def build_requests: (verb, uri, request_params) -> Array[Request]
|
def build_requests: (verb, uri, request_params) -> Array[Request]
|
||||||
| (Array[[verb, uri, request_params]], Hash[Symbol, untyped]) -> Array[Request]
|
| (Array[[verb, uri, request_params]], Hash[Symbol, untyped]) -> Array[Request]
|
||||||
| (Array[[verb, uri]], request_params) -> Array[Request]
|
| (Array[[verb, uri]], request_params) -> Array[Request]
|
||||||
| (verb, _Each[[uri, request_params]], Hash[Symbol, untyped]) -> Array[Request]
|
| (verb, _Each[[uri, request_params]], Hash[Symbol, untyped]) -> Array[Request]
|
||||||
| (verb, _Each[uri], request_params) -> Array[Request]
|
| (verb, _Each[uri], request_params) -> Array[Request]
|
||||||
|
|
||||||
def init_connection: (http_uri uri, Options options) -> Connection
|
def do_init_connection: (Connection connection, Selector selector) -> Connection
|
||||||
|
|
||||||
def send_requests: (*Request) -> Array[response]
|
def send_requests: (*Request) -> Array[response]
|
||||||
|
|
||||||
def _send_requests: (Array[Request] requests) -> Array[Connection]
|
def _send_requests: (Array[Request] requests, Selector selector) -> void
|
||||||
|
|
||||||
def receive_requests: (Array[Request] requests, Array[Connection] connections) -> Array[response]
|
def receive_requests: (Array[Request] requests, Selector selector) -> Array[response]
|
||||||
|
|
||||||
|
def resolve_connection: (Connection connection, Selector selector) -> void
|
||||||
|
|
||||||
|
def on_resolve_connection: (Connection connection, Selector selector) -> void
|
||||||
|
|
||||||
|
def on_resolver_close: (Resolver::Resolver resolver, Selector selector) -> void
|
||||||
|
|
||||||
|
def find_resolver_for: (Connection connection, Selector selector) -> (Resolver::Multi | Resolver::Resolver)
|
||||||
|
|
||||||
|
def coalesce_connections: (Connection conn1, Connection conn2, Selector selector) -> bool
|
||||||
|
|
||||||
attr_reader self.default_options: Options
|
attr_reader self.default_options: Options
|
||||||
end
|
end
|
||||||
|
|||||||
@ -13,7 +13,7 @@ module MinitestExtensions
|
|||||||
module FirstFailedTestInThread
|
module FirstFailedTestInThread
|
||||||
def self.prepended(*)
|
def self.prepended(*)
|
||||||
super
|
super
|
||||||
HTTPX::Session.include SessionExtensions
|
HTTPX::Connection.include ConnectionExtensions
|
||||||
end
|
end
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
@ -21,11 +21,10 @@ module MinitestExtensions
|
|||||||
extend(OnTheFly)
|
extend(OnTheFly)
|
||||||
end
|
end
|
||||||
|
|
||||||
module SessionExtensions
|
module ConnectionExtensions
|
||||||
def find_connection(request, connections, _)
|
def send(request)
|
||||||
connection = super
|
|
||||||
request.instance_variable_set(:@connection, connection)
|
request.instance_variable_set(:@connection, connection)
|
||||||
connection
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -36,41 +36,6 @@ module Requests
|
|||||||
assert options.persistent
|
assert options.persistent
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_persistent_with_wrap
|
|
||||||
return unless origin.start_with?("https")
|
|
||||||
|
|
||||||
uri = build_uri("/get")
|
|
||||||
session1 = HTTPX.plugin(:persistent)
|
|
||||||
|
|
||||||
begin
|
|
||||||
pool = session1.send(:pool)
|
|
||||||
|
|
||||||
initial_size = pool.instance_variable_get(:@connections).size
|
|
||||||
response = session1.get(uri)
|
|
||||||
verify_status(response, 200)
|
|
||||||
|
|
||||||
connections = pool.instance_variable_get(:@connections)
|
|
||||||
pool_size = connections.size
|
|
||||||
assert pool_size == initial_size + 1
|
|
||||||
|
|
||||||
HTTPX.wrap do |s|
|
|
||||||
response = s.get(uri)
|
|
||||||
verify_status(response, 200)
|
|
||||||
wrapped_connections = pool.instance_variable_get(:@connections)
|
|
||||||
pool_size = wrapped_connections.size
|
|
||||||
assert pool_size == 1
|
|
||||||
assert (connections - wrapped_connections) == connections
|
|
||||||
end
|
|
||||||
|
|
||||||
final_connections = pool.instance_variable_get(:@connections)
|
|
||||||
pool_size = final_connections.size
|
|
||||||
assert pool_size == initial_size + 1
|
|
||||||
assert (connections - final_connections).empty?
|
|
||||||
ensure
|
|
||||||
session1.close
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_persistent_retry_http2_goaway
|
def test_persistent_retry_http2_goaway
|
||||||
return unless origin.start_with?("https")
|
return unless origin.start_with?("https")
|
||||||
|
|
||||||
|
|||||||
@ -130,17 +130,11 @@ module WSTestPlugin
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
module ConnectionMethods
|
||||||
def find_connection(request, *)
|
def send(request)
|
||||||
return super if request.websocket
|
request.init_websocket(self) unless request.websocket || @upgrade_protocol
|
||||||
|
|
||||||
conn = super
|
super
|
||||||
|
|
||||||
return conn unless conn && !conn.upgrade_protocol
|
|
||||||
|
|
||||||
request.init_websocket(conn)
|
|
||||||
|
|
||||||
conn
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user