httpx 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +47 -0
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/sentry.rb +1 -1
  8. data/lib/httpx/altsvc.rb +3 -1
  9. data/lib/httpx/connection/http1.rb +14 -15
  10. data/lib/httpx/connection/http2.rb +16 -15
  11. data/lib/httpx/connection.rb +118 -110
  12. data/lib/httpx/domain_name.rb +1 -1
  13. data/lib/httpx/extensions.rb +0 -14
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/ssl.rb +1 -1
  16. data/lib/httpx/loggable.rb +14 -2
  17. data/lib/httpx/options.rb +60 -17
  18. data/lib/httpx/plugins/auth/digest.rb +44 -4
  19. data/lib/httpx/plugins/auth.rb +87 -4
  20. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  21. data/lib/httpx/plugins/callbacks.rb +15 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +162 -56
  30. data/lib/httpx/plugins/proxy/http.rb +37 -9
  31. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  32. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  33. data/lib/httpx/plugins/response_cache.rb +16 -9
  34. data/lib/httpx/plugins/retries.rb +55 -16
  35. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  36. data/lib/httpx/plugins/stream.rb +59 -8
  37. data/lib/httpx/plugins/stream_bidi.rb +87 -22
  38. data/lib/httpx/pool.rb +65 -21
  39. data/lib/httpx/request.rb +13 -14
  40. data/lib/httpx/resolver/https.rb +100 -34
  41. data/lib/httpx/resolver/multi.rb +12 -27
  42. data/lib/httpx/resolver/native.rb +68 -38
  43. data/lib/httpx/resolver/resolver.rb +46 -29
  44. data/lib/httpx/resolver/system.rb +63 -39
  45. data/lib/httpx/resolver.rb +97 -29
  46. data/lib/httpx/response/body.rb +2 -0
  47. data/lib/httpx/response.rb +22 -6
  48. data/lib/httpx/selector.rb +44 -20
  49. data/lib/httpx/session.rb +23 -33
  50. data/lib/httpx/transcoder/body.rb +1 -1
  51. data/lib/httpx/transcoder/deflate.rb +13 -8
  52. data/lib/httpx/transcoder/json.rb +1 -1
  53. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  54. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  55. data/lib/httpx/transcoder/multipart.rb +16 -8
  56. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  57. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  58. data/lib/httpx/transcoder.rb +4 -6
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/altsvc.rbs +3 -0
  61. data/sig/chainable.rbs +3 -3
  62. data/sig/connection.rbs +13 -6
  63. data/sig/loggable.rbs +5 -1
  64. data/sig/options.rbs +6 -2
  65. data/sig/plugins/auth/digest.rbs +6 -0
  66. data/sig/plugins/auth.rbs +28 -4
  67. data/sig/plugins/basic_auth.rbs +3 -3
  68. data/sig/plugins/callbacks.rbs +3 -0
  69. data/sig/plugins/digest_auth.rbs +2 -4
  70. data/sig/plugins/fiber_concurrency.rbs +6 -0
  71. data/sig/plugins/ntlm_auth.rbs +2 -2
  72. data/sig/plugins/oauth.rbs +46 -15
  73. data/sig/plugins/rate_limiter.rbs +1 -1
  74. data/sig/plugins/response_cache/file_store.rbs +2 -0
  75. data/sig/plugins/response_cache.rbs +4 -0
  76. data/sig/plugins/retries.rbs +8 -2
  77. data/sig/plugins/stream.rbs +13 -3
  78. data/sig/plugins/stream_bidi.rbs +5 -7
  79. data/sig/pool.rbs +1 -1
  80. data/sig/resolver/https.rbs +7 -0
  81. data/sig/resolver/multi.rbs +2 -9
  82. data/sig/resolver/native.rbs +1 -1
  83. data/sig/resolver/resolver.rbs +9 -8
  84. data/sig/resolver/system.rbs +4 -2
  85. data/sig/resolver.rbs +12 -3
  86. data/sig/response.rbs +3 -0
  87. data/sig/selector.rbs +2 -0
  88. data/sig/session.rbs +8 -8
  89. data/sig/transcoder/multipart.rbs +4 -2
  90. data/sig/transcoder.rbs +5 -1
  91. metadata +5 -1
data/lib/httpx/pool.rb CHANGED
@@ -8,7 +8,6 @@ require "httpx/resolver"
8
8
 
9
9
  module HTTPX
10
10
  class Pool
11
- using ArrayExtensions::FilterMap
12
11
  using URIExtensions
13
12
 
14
13
  POOL_TIMEOUT = 5
@@ -51,32 +50,53 @@ module HTTPX
51
50
  acquire_connection(uri, options) || begin
52
51
  if @connections_counter == @max_connections
53
52
  # this takes precedence over per-origin
54
- @max_connections_cond.wait(@connection_mtx, @pool_timeout)
55
53
 
56
- if (conn = acquire_connection(uri, options))
57
- return conn
58
- end
54
+ expires_at = Utils.now + @pool_timeout
59
55
 
60
- if @connections_counter == @max_connections
61
- # if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
62
- # this means that all of them are persistent or being used, so raise a timeout error.
63
- conn = @connections.find { |c| c.state == :closed }
56
+ loop do
57
+ @max_connections_cond.wait(@connection_mtx, @pool_timeout)
64
58
 
65
- raise PoolTimeoutError.new(@pool_timeout,
66
- "Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
59
+ if (conn = acquire_connection(uri, options))
60
+ return conn
61
+ end
62
+
63
+ # if one can afford to create a new connection, do it
64
+ break unless @connections_counter == @max_connections
65
+
66
+ # if no matching usable connection was found, the pool will make room and drop a closed connection.
67
+ if (conn = @connections.find { |c| c.state == :closed })
68
+ drop_connection(conn)
69
+ break
70
+ end
71
+
72
+ # happens when a condition was signalled, but another thread snatched the available connection before
73
+ # context was passed back here.
74
+ next if Utils.now < expires_at
67
75
 
68
- drop_connection(conn)
76
+ raise PoolTimeoutError.new(@pool_timeout,
77
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection")
69
78
  end
70
79
 
71
80
  end
72
81
 
73
82
  if @origin_counters[uri.origin] == @max_connections_per_origin
74
83
 
75
- @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
84
+ expires_at = Utils.now + @pool_timeout
85
+
86
+ loop do
87
+ @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
88
+
89
+ if (conn = acquire_connection(uri, options))
90
+ return conn
91
+ end
76
92
 
77
- return acquire_connection(uri, options) ||
78
- raise(PoolTimeoutError.new(@pool_timeout,
79
- "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
93
+ # happens when a condition was signalled, but another thread snatched the available connection before
94
+ # context was passed back here.
95
+ next if Utils.now < expires_at
96
+
97
+ raise(PoolTimeoutError.new(@pool_timeout,
98
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
99
+ end
80
100
  end
81
101
 
82
102
  @connections_counter += 1
@@ -91,10 +111,23 @@ module HTTPX
91
111
  return if connection.options.io
92
112
 
93
113
  @connection_mtx.synchronize do
114
+ if connection.coalesced? || connection.state == :idle
115
+ # when connections coalesce
116
+ drop_connection(connection)
117
+
118
+ return
119
+ end
120
+
94
121
  @connections << connection
95
122
 
96
123
  @max_connections_cond.signal
97
124
  @origin_conds[connection.origin.to_s].signal
125
+
126
+ # Observed situations where a session handling multiple requests in a loop
127
+ # across multiple threads checks the same connection in and out, while another
128
+ # thread which is waiting on the same connection never gets the chance to pick
129
+ # it up, because ruby's thread scheduler never switched on to it in the process.
130
+ Thread.pass
98
131
  end
99
132
  end
100
133
 
@@ -128,10 +161,16 @@ module HTTPX
128
161
  end
129
162
 
130
163
  def checkin_resolver(resolver)
131
- @resolver_mtx.synchronize do
132
- resolvers = @resolvers[resolver.class]
164
+ resolver_class = resolver.class
133
165
 
134
- resolver = resolver.multi
166
+ resolver = resolver.multi
167
+
168
+ # a multi requires all sub-resolvers being closed in order to be
169
+ # correctly checked back in.
170
+ return unless resolver.closed?
171
+
172
+ @resolver_mtx.synchronize do
173
+ resolvers = @resolvers[resolver_class]
135
174
 
136
175
  resolvers << resolver unless resolvers.include?(resolver)
137
176
  end
@@ -140,6 +179,7 @@ module HTTPX
140
179
  # :nocov:
141
180
  def inspect
142
181
  "#<#{self.class}:#{object_id} " \
182
+ "@max_connections=#{@max_connections} " \
143
183
  "@max_connections_per_origin=#{@max_connections_per_origin} " \
144
184
  "@pool_timeout=#{@pool_timeout} " \
145
185
  "@connections=#{@connections.size}>"
@@ -159,15 +199,19 @@ module HTTPX
159
199
  end
160
200
 
161
201
  def checkout_new_connection(uri, options)
162
- options.connection_class.new(uri, options)
202
+ connection = options.connection_class.new(uri, options)
203
+ connection.log(level: 2) { "created connection##{connection.object_id} in pool##{object_id}" }
204
+ connection
163
205
  end
164
206
 
165
207
  def checkout_new_resolver(resolver_type, options)
166
- if resolver_type.multi?
208
+ resolver = if resolver_type.multi?
167
209
  Resolver::Multi.new(resolver_type, options)
168
210
  else
169
211
  resolver_type.new(options)
170
212
  end
213
+ resolver.log(level: 2) { "created resolver##{resolver.object_id} in pool##{object_id}" }
214
+ resolver
171
215
  end
172
216
 
173
217
  # drops and returns the +connection+ from the connection pool; if +connection+ is <tt>nil</tt> (default),
data/lib/httpx/request.rb CHANGED
@@ -10,13 +10,11 @@ module HTTPX
10
10
  extend Forwardable
11
11
  include Loggable
12
12
  include Callbacks
13
+
13
14
  using URIExtensions
14
15
 
15
16
  ALLOWED_URI_SCHEMES = %w[https http].freeze
16
17
 
17
- # default value used for "user-agent" header, when not overridden.
18
- USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
19
-
20
18
  # the upcased string HTTP verb for this request.
21
19
  attr_reader :verb
22
20
 
@@ -75,16 +73,6 @@ module HTTPX
75
73
  @headers = options.headers.dup
76
74
  merge_headers(params.delete(:headers)) if params.key?(:headers)
77
75
 
78
- @headers["user-agent"] ||= USER_AGENT
79
- @headers["accept"] ||= "*/*"
80
-
81
- # forego compression in the Range request case
82
- if @headers.key?("range")
83
- @headers.delete("accept-encoding")
84
- else
85
- @headers["accept-encoding"] ||= options.supported_compression_formats
86
- end
87
-
88
76
  @query_params = params.delete(:params) if params.key?(:params)
89
77
 
90
78
  @body = options.request_body_class.new(@headers, options, **params)
@@ -103,12 +91,20 @@ module HTTPX
103
91
  raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
104
92
 
105
93
  @state = :idle
106
- @response = @peer_address = @context = @informational_status = nil
94
+ @response = @peer_address = @informational_status = nil
107
95
  @ping = false
108
96
  @persistent = @options.persistent
109
97
  @active_timeouts = []
110
98
  end
111
99
 
100
+ # dupped initialization
101
+ def initialize_dup(orig)
102
+ super
103
+ @uri = orig.instance_variable_get(:@uri).dup
104
+ @headers = orig.instance_variable_get(:@headers).dup
105
+ @body = orig.instance_variable_get(:@body).dup
106
+ end
107
+
112
108
  def complete!(response = @response)
113
109
  emit(:complete, response)
114
110
  end
@@ -166,6 +162,9 @@ module HTTPX
166
162
  # merges +h+ into the instance of HTTPX::Headers of the request.
167
163
  def merge_headers(h)
168
164
  @headers = @headers.merge(h)
165
+ return unless @headers.key?("range")
166
+
167
+ @headers.delete("accept-encoding")
169
168
  end
170
169
 
171
170
  # the URI scheme of the request +uri+.
@@ -12,6 +12,7 @@ module HTTPX
12
12
  #
13
13
  class Resolver::HTTPS < Resolver::Resolver
14
14
  extend Forwardable
15
+
15
16
  using URIExtensions
16
17
 
17
18
  module DNSExtensions
@@ -30,17 +31,20 @@ module HTTPX
30
31
  use_get: false,
31
32
  }.freeze
32
33
 
33
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?, :handle_socket_timeout
34
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close,
35
+ :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
34
36
 
35
37
  def initialize(_, options)
36
38
  super
37
39
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
38
40
  @queries = {}
39
41
  @requests = {}
42
+ @_timeouts = Array(@resolver_options[:timeouts])
43
+ @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
40
44
  @uri = URI(@resolver_options[:uri])
41
- @uri_addresses = nil
45
+ @name = @uri_addresses = nil
42
46
  @resolver = Resolv::DNS.new
43
- @resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
47
+ @resolver.timeouts = @_timeouts.empty? ? Resolver::RESOLVE_TIMEOUT : @_timeouts
44
48
  @resolver.lazy_initialize
45
49
  end
46
50
 
@@ -52,29 +56,24 @@ module HTTPX
52
56
  if @uri_addresses.empty?
53
57
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
54
58
  ex.set_backtrace(caller)
55
- connection.force_reset
59
+ connection.force_close
56
60
  throw(:resolve_error, ex)
57
61
  end
58
62
 
59
63
  resolve(connection)
60
64
  end
61
65
 
62
- # This is already indirectly monitored bt the HTTP connection. In order to skip
63
- # monitoring, this method returns <tt>true</tt>.
64
- def closed?
65
- true
66
- end
67
-
68
- def empty?
69
- true
70
- end
71
-
72
66
  def resolver_connection
73
67
  # TODO: leaks connection object into the pool
74
- @resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
75
- @options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
76
- emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
77
- end
68
+ @resolver_connection ||=
69
+ @current_session.find_connection(
70
+ @uri,
71
+ @current_selector,
72
+ @options.merge(resolver_class: :system, ssl: { alpn_protocols: %w[h2] })
73
+ ).tap do |conn|
74
+ emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
75
+ conn.on(:force_closed, &method(:force_close))
76
+ end
78
77
  end
79
78
 
80
79
  private
@@ -100,19 +99,26 @@ module HTTPX
100
99
  else
101
100
  @queries[hostname] = connection
102
101
  end
102
+
103
+ @name = hostname
104
+
103
105
  log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
104
106
 
105
- begin
106
- request = build_request(hostname)
107
- request.on(:response, &method(:on_response).curry(2)[request])
108
- request.on(:promise, &method(:on_promise))
109
- @requests[request] = hostname
110
- resolver_connection.send(request)
111
- @connections << connection
112
- rescue ResolveError, Resolv::DNS::EncodeError => e
113
- reset_hostname(hostname)
114
- emit_resolve_error(connection, connection.peer.host, e)
115
- end
107
+ send_request(hostname, connection)
108
+ end
109
+
110
+ def send_request(hostname, connection)
111
+ request = build_request(hostname)
112
+ request.on(:response, &method(:on_response).curry(2)[request])
113
+ request.on(:promise, &method(:on_promise))
114
+ @requests[request] = hostname
115
+ resolver_connection.send(request)
116
+ @connections << connection
117
+ rescue ResolveError, Resolv::DNS::EncodeError => e
118
+ reset_hostname(hostname)
119
+ throw(:resolve_error, e) if connection.pending.empty?
120
+ emit_resolve_error(connection, connection.peer.host, e)
121
+ close_or_resolve
116
122
  end
117
123
 
118
124
  def on_response(request, response)
@@ -121,8 +127,21 @@ module HTTPX
121
127
  hostname = @requests.delete(request)
122
128
  connection = reset_hostname(hostname)
123
129
  emit_resolve_error(connection, connection.peer.host, e)
130
+ close_or_resolve
124
131
  else
125
132
  # @type var response: HTTPX::Response
133
+ if response.status.between?(300, 399) && response.headers.key?("location")
134
+ hostname = @requests[request]
135
+ connection = @queries[hostname]
136
+ location_uri = URI(response.headers["location"])
137
+ location_uri = response.uri.merge(location_uri) if location_uri.relative?
138
+
139
+ # we assume that the DNS server URI changed permanently and move on
140
+ @uri = location_uri
141
+ send_request(hostname, connection)
142
+ return
143
+ end
144
+
126
145
  parse(request, response)
127
146
  ensure
128
147
  @requests.delete(request)
@@ -134,6 +153,10 @@ module HTTPX
134
153
  end
135
154
 
136
155
  def parse(request, response)
156
+ hostname = @name
157
+
158
+ @name = nil
159
+
137
160
  code, result = decode_response_body(response)
138
161
 
139
162
  case code
@@ -147,19 +170,39 @@ module HTTPX
147
170
 
148
171
  unless @queries.value?(connection)
149
172
  emit_resolve_error(connection)
173
+ close_or_resolve
150
174
  return
151
175
  end
152
176
 
153
177
  resolve
178
+ when :retriable_error
179
+ timeouts = @timeouts[hostname]
180
+
181
+ unless timeouts.empty?
182
+ log { "resolver #{FAMILY_TYPES[@record_type]}: failed, but will retry..." }
183
+
184
+ connection = @queries[hostname]
185
+
186
+ resolve(connection, hostname)
187
+ return
188
+ end
189
+
190
+ host = @requests.delete(request)
191
+ connection = reset_hostname(host)
192
+
193
+ emit_resolve_error(connection)
194
+ close_or_resolve
154
195
  when :dns_error
155
196
  host = @requests.delete(request)
156
197
  connection = reset_hostname(host)
157
198
 
158
199
  emit_resolve_error(connection)
200
+ close_or_resolve
159
201
  when :decode_error
160
202
  host = @requests.delete(request)
161
203
  connection = reset_hostname(host)
162
204
  emit_resolve_error(connection, connection.peer.host, result)
205
+ close_or_resolve
163
206
  end
164
207
  end
165
208
 
@@ -169,6 +212,7 @@ module HTTPX
169
212
  host = @requests.delete(request)
170
213
  connection = reset_hostname(host)
171
214
  emit_resolve_error(connection)
215
+ close_or_resolve
172
216
  return
173
217
 
174
218
  else
@@ -207,24 +251,25 @@ module HTTPX
207
251
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
208
252
  end
209
253
  end
210
- return if @connections.empty?
211
-
212
- resolve
254
+ close_or_resolve(true)
213
255
  end
214
256
 
215
257
  def build_request(hostname)
216
258
  uri = @uri.dup
217
259
  rklass = @options.request_class
218
260
  payload = Resolver.encode_dns_query(hostname, type: @record_type)
261
+ timeouts = @timeouts[hostname]
262
+ request_timeout = timeouts.shift
263
+ options = @options.merge(timeout: { request_timeout: request_timeout })
219
264
 
220
265
  if @resolver_options[:use_get]
221
266
  params = URI.decode_www_form(uri.query.to_s)
222
267
  params << ["type", FAMILY_TYPES[@record_type]]
223
268
  params << ["dns", Base64.urlsafe_encode64(payload, padding: false)]
224
269
  uri.query = URI.encode_www_form(params)
225
- request = rklass.new("GET", uri, @options)
270
+ request = rklass.new("GET", uri, options)
226
271
  else
227
- request = rklass.new("POST", uri, @options, body: [payload])
272
+ request = rklass.new("POST", uri, options, body: [payload])
228
273
  request.headers["content-type"] = "application/dns-message"
229
274
  end
230
275
  request.headers["accept"] = "application/dns-message"
@@ -242,6 +287,7 @@ module HTTPX
242
287
  end
243
288
 
244
289
  def reset_hostname(hostname, reset_candidates: true)
290
+ @timeouts.delete(hostname)
245
291
  connection = @queries.delete(hostname)
246
292
 
247
293
  return connection unless connection && reset_candidates
@@ -249,8 +295,28 @@ module HTTPX
249
295
  # eliminate other candidates
250
296
  candidates = @queries.select { |_, conn| connection == conn }.keys
251
297
  @queries.delete_if { |h, _| candidates.include?(h) }
298
+ # reset timeouts
299
+ @timeouts.delete_if { |h, _| candidates.include?(h) }
252
300
 
253
301
  connection
254
302
  end
303
+
304
+ def close_or_resolve(should_deactivate = false)
305
+ # drop already closed connections
306
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
307
+
308
+ if (@connections - @queries.values).empty?
309
+ # the same resolver connection may be serving different https resolvers (AAAA and A).
310
+ return if inflight?
311
+
312
+ if should_deactivate
313
+ deactivate
314
+ else
315
+ disconnect
316
+ end
317
+ else
318
+ resolve
319
+ end
320
+ end
255
321
  end
256
322
  end
@@ -5,9 +5,6 @@ require "resolv"
5
5
 
6
6
  module HTTPX
7
7
  class Resolver::Multi
8
- include Callbacks
9
- using ArrayExtensions::FilterMap
10
-
11
8
  attr_reader :resolvers, :options
12
9
 
13
10
  def initialize(resolver_type, options)
@@ -26,44 +23,28 @@ module HTTPX
26
23
  @errors = Hash.new { |hs, k| hs[k] = [] }
27
24
  end
28
25
 
26
+ def state
27
+ @resolvers.map(&:state).uniq.join(",")
28
+ end
29
+
29
30
  def current_selector=(s)
30
31
  @current_selector = s
31
- @resolvers.each { |r| r.__send__(__method__, s) }
32
+ @resolvers.each { |r| r.current_selector = s }
32
33
  end
33
34
 
34
35
  def current_session=(s)
35
36
  @current_session = s
36
- @resolvers.each { |r| r.__send__(__method__, s) }
37
+ @resolvers.each { |r| r.current_session = s }
37
38
  end
38
39
 
39
40
  def log(*args, **kwargs, &blk)
40
- @resolvers.each { |r| r.__send__(__method__, *args, **kwargs, &blk) }
41
+ @resolvers.each { |r| r.log(*args, **kwargs, &blk) }
41
42
  end
42
43
 
43
44
  def closed?
44
45
  @resolvers.all?(&:closed?)
45
46
  end
46
47
 
47
- def empty?
48
- @resolvers.all?(&:empty?)
49
- end
50
-
51
- def inflight?
52
- @resolvers.any(&:inflight?)
53
- end
54
-
55
- def timeout
56
- @resolvers.filter_map(&:timeout).min
57
- end
58
-
59
- def close
60
- @resolvers.each(&:close)
61
- end
62
-
63
- def connections
64
- @resolvers.filter_map { |r| r.resolver_connection if r.respond_to?(:resolver_connection) }
65
- end
66
-
67
48
  def early_resolve(connection)
68
49
  hostname = connection.peer.host
69
50
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
@@ -92,9 +73,13 @@ module HTTPX
92
73
 
93
74
  def lazy_resolve(connection)
94
75
  @resolvers.each do |resolver|
95
- resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
76
+ conn_to_resolve = @current_session.try_clone_connection(connection, @current_selector, resolver.family)
77
+ resolver << conn_to_resolve
78
+
96
79
  next if resolver.empty?
97
80
 
81
+ # both the resolver and the connection it's resolving must be pineed to the session
82
+ @current_session.pin(conn_to_resolve, @current_selector)
98
83
  @current_session.select_resolver(resolver, @current_selector)
99
84
  end
100
85
  end