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:
HoneyryderChuck 2024-08-16 11:32:47 +01:00
parent 12fbca468b
commit 11d197ff24
18 changed files with 794 additions and 362 deletions

View File

@ -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

View File

@ -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)

View File

@ -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"])

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = []

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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