httpx 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_0.md +43 -0
  3. data/lib/httpx/adapters/faraday.rb +2 -0
  4. data/lib/httpx/adapters/webmock.rb +11 -5
  5. data/lib/httpx/callbacks.rb +0 -5
  6. data/lib/httpx/chainable.rb +3 -1
  7. data/lib/httpx/connection/http2.rb +11 -7
  8. data/lib/httpx/connection.rb +128 -16
  9. data/lib/httpx/errors.rb +12 -0
  10. data/lib/httpx/loggable.rb +5 -5
  11. data/lib/httpx/options.rb +26 -16
  12. data/lib/httpx/plugins/aws_sigv4.rb +31 -16
  13. data/lib/httpx/plugins/callbacks.rb +12 -2
  14. data/lib/httpx/plugins/circuit_breaker.rb +0 -5
  15. data/lib/httpx/plugins/content_digest.rb +202 -0
  16. data/lib/httpx/plugins/expect.rb +4 -3
  17. data/lib/httpx/plugins/follow_redirects.rb +7 -8
  18. data/lib/httpx/plugins/h2c.rb +23 -20
  19. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  20. data/lib/httpx/plugins/persistent.rb +16 -0
  21. data/lib/httpx/plugins/proxy/http.rb +17 -19
  22. data/lib/httpx/plugins/proxy.rb +91 -93
  23. data/lib/httpx/plugins/retries.rb +5 -8
  24. data/lib/httpx/plugins/upgrade.rb +5 -10
  25. data/lib/httpx/plugins/webdav.rb +6 -0
  26. data/lib/httpx/plugins/xml.rb +76 -0
  27. data/lib/httpx/pool.rb +73 -244
  28. data/lib/httpx/request/body.rb +16 -12
  29. data/lib/httpx/request.rb +1 -1
  30. data/lib/httpx/resolver/https.rb +12 -19
  31. data/lib/httpx/resolver/multi.rb +34 -16
  32. data/lib/httpx/resolver/native.rb +36 -13
  33. data/lib/httpx/resolver/resolver.rb +49 -11
  34. data/lib/httpx/resolver/system.rb +29 -11
  35. data/lib/httpx/resolver.rb +21 -14
  36. data/lib/httpx/response.rb +5 -3
  37. data/lib/httpx/selector.rb +164 -95
  38. data/lib/httpx/session.rb +296 -139
  39. data/lib/httpx/transcoder/gzip.rb +0 -3
  40. data/lib/httpx/transcoder/json.rb +14 -2
  41. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  42. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  43. data/lib/httpx/transcoder.rb +0 -1
  44. data/lib/httpx/version.rb +1 -1
  45. data/lib/httpx.rb +19 -20
  46. data/sig/callbacks.rbs +0 -1
  47. data/sig/chainable.rbs +4 -0
  48. data/sig/connection/http2.rbs +1 -1
  49. data/sig/connection.rbs +14 -3
  50. data/sig/errors.rbs +6 -0
  51. data/sig/loggable.rbs +2 -0
  52. data/sig/options.rbs +7 -0
  53. data/sig/plugins/aws_sigv4.rbs +8 -2
  54. data/sig/plugins/content_digest.rbs +51 -0
  55. data/sig/plugins/cookies/cookie.rbs +9 -0
  56. data/sig/plugins/grpc/call.rbs +4 -0
  57. data/sig/plugins/persistent.rbs +4 -1
  58. data/sig/plugins/proxy/socks5.rbs +11 -3
  59. data/sig/plugins/proxy.rbs +18 -11
  60. data/sig/plugins/push_promise.rbs +3 -0
  61. data/sig/plugins/rate_limiter.rbs +2 -0
  62. data/sig/plugins/retries.rbs +1 -1
  63. data/sig/plugins/ssrf_filter.rbs +26 -0
  64. data/sig/plugins/webdav.rbs +23 -0
  65. data/sig/plugins/xml.rbs +37 -0
  66. data/sig/pool.rbs +25 -33
  67. data/sig/request/body.rbs +5 -1
  68. data/sig/resolver/multi.rbs +26 -1
  69. data/sig/resolver/native.rbs +0 -2
  70. data/sig/resolver/resolver.rbs +21 -2
  71. data/sig/resolver.rbs +5 -1
  72. data/sig/response/buffer.rbs +1 -1
  73. data/sig/selector.rbs +30 -4
  74. data/sig/session.rbs +45 -18
  75. data/sig/transcoder/body.rbs +1 -1
  76. data/sig/transcoder/chunker.rbs +1 -1
  77. data/sig/transcoder/deflate.rbs +1 -0
  78. data/sig/transcoder/form.rbs +8 -0
  79. data/sig/transcoder/gzip.rbs +4 -1
  80. data/sig/transcoder/utils/body_reader.rbs +2 -2
  81. data/sig/transcoder/utils/deflater.rbs +2 -2
  82. metadata +10 -4
  83. data/lib/httpx/transcoder/xml.rb +0 -52
  84. data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/pool.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "httpx/selector"
5
4
  require "httpx/connection"
6
5
  require "httpx/resolver"
@@ -8,110 +7,31 @@ require "httpx/resolver"
8
7
  module HTTPX
9
8
  class Pool
10
9
  using ArrayExtensions::FilterMap
11
- extend Forwardable
10
+ using URIExtensions
12
11
 
13
- def_delegator :@timers, :after
12
+ POOL_TIMEOUT = 5
14
13
 
15
- def initialize
16
- @resolvers = {}
17
- @timers = Timers.new
18
- @selector = Selector.new
19
- @connections = []
20
- end
21
-
22
- def wrap
23
- connections = @connections
14
+ # Sets up the connection pool with the given +options+, which can be the following:
15
+ #
16
+ # :max_connections_per_origin :: the maximum number of connections held in the pool pointing to a given origin.
17
+ # :pool_timeout :: the number of seconds to wait for a connection to a given origin (before raising HTTPX::PoolTimeoutError)
18
+ #
19
+ def initialize(options)
20
+ @max_connections_per_origin = options.fetch(:max_connections_per_origin, Float::INFINITY)
21
+ @pool_timeout = options.fetch(:pool_timeout, POOL_TIMEOUT)
22
+ @resolvers = Hash.new { |hs, resolver_type| hs[resolver_type] = [] }
23
+ @resolver_mtx = Thread::Mutex.new
24
24
  @connections = []
25
-
26
- begin
27
- yield self
28
- ensure
29
- @connections.unshift(*connections)
30
- end
31
- end
32
-
33
- def empty?
34
- @connections.empty?
35
- end
36
-
37
- def next_tick
38
- catch(:jump_tick) do
39
- timeout = next_timeout
40
- if timeout && timeout.negative?
41
- @timers.fire
42
- throw(:jump_tick)
43
- end
44
-
45
- begin
46
- @selector.select(timeout, &:call)
47
- @timers.fire
48
- rescue TimeoutError => e
49
- @timers.fire(e)
50
- end
51
- end
52
- rescue StandardError => e
53
- @connections.each do |connection|
54
- connection.emit(:error, e)
55
- end
56
- rescue Exception # rubocop:disable Lint/RescueException
57
- @connections.each(&:force_reset)
58
- raise
59
- end
60
-
61
- def close(connections = @connections)
62
- return if connections.empty?
63
-
64
- connections = connections.reject(&:inflight?)
65
- connections.each(&:terminate)
66
- next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
67
-
68
- # close resolvers
69
- outstanding_connections = @connections
70
- resolver_connections = @resolvers.each_value.flat_map(&:connections).compact
71
- outstanding_connections -= resolver_connections
72
-
73
- return unless outstanding_connections.empty?
74
-
75
- @resolvers.each_value do |resolver|
76
- resolver.close unless resolver.closed?
77
- end
78
- # for https resolver
79
- resolver_connections.each(&:terminate)
80
- next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
81
- end
82
-
83
- def init_connection(connection, _options)
84
- connection.timers = @timers
85
- connection.on(:activate) do
86
- select_connection(connection)
87
- end
88
- connection.on(:exhausted) do
89
- case connection.state
90
- when :closed
91
- connection.idling
92
- @connections << connection
93
- select_connection(connection)
94
- when :closing
95
- connection.once(:close) do
96
- connection.idling
97
- @connections << connection
98
- select_connection(connection)
99
- end
100
- end
101
- end
102
- connection.on(:close) do
103
- unregister_connection(connection)
104
- end
105
- connection.on(:terminate) do
106
- unregister_connection(connection, true)
107
- end
108
- resolve_connection(connection) unless connection.family
25
+ @connection_mtx = Thread::Mutex.new
26
+ @origin_counters = Hash.new(0)
27
+ @origin_conds = Hash.new { |hs, orig| hs[orig] = ConditionVariable.new }
109
28
  end
110
29
 
111
- def deactivate(*connections)
112
- connections.each do |connection|
113
- connection.deactivate
114
- deselect_connection(connection) if connection.state == :inactive
30
+ def pop_connection
31
+ @connection_mtx.synchronize do
32
+ conn = @connections.shift
33
+ @origin_conds.delete(conn.origin) if conn && (@origin_counters[conn.origin.to_s] -= 1).zero?
34
+ conn
115
35
  end
116
36
  end
117
37
 
@@ -119,185 +39,94 @@ module HTTPX
119
39
  # Many hostnames are reachable through the same IP, so we try to
120
40
  # maximize pipelining by opening as few connections as possible.
121
41
  #
122
- def find_connection(uri, options)
123
- conn = @connections.find do |connection|
124
- connection.match?(uri, options)
125
- end
42
+ def checkout_connection(uri, options)
43
+ return checkout_new_connection(uri, options) if options.io
126
44
 
127
- return unless conn
45
+ @connection_mtx.synchronize do
46
+ acquire_connection(uri, options) || begin
47
+ if @origin_counters[uri.origin] == @max_connections_per_origin
128
48
 
129
- case conn.state
130
- when :closed
131
- conn.idling
132
- select_connection(conn)
133
- when :closing
134
- conn.once(:close) do
135
- conn.idling
136
- select_connection(conn)
137
- end
138
- end
49
+ @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
139
50
 
140
- conn
141
- end
142
-
143
- private
144
-
145
- def resolve_connection(connection)
146
- @connections << connection unless @connections.include?(connection)
147
-
148
- if connection.addresses || connection.open?
149
- #
150
- # there are two cases in which we want to activate initialization of
151
- # connection immediately:
152
- #
153
- # 1. when the connection already has addresses, i.e. it doesn't need to
154
- # resolve a name (not the same as name being an IP, yet)
155
- # 2. when the connection is initialized with an external already open IO.
156
- #
157
- connection.once(:connect_error, &connection.method(:handle_error))
158
- on_resolver_connection(connection)
159
- return
160
- end
51
+ return acquire_connection(uri, options) || raise(PoolTimeoutError.new(uri.origin, @pool_timeout))
52
+ end
161
53
 
162
- find_resolver_for(connection) do |resolver|
163
- resolver << try_clone_connection(connection, resolver.family)
164
- next if resolver.empty?
54
+ @origin_counters[uri.origin] += 1
165
55
 
166
- select_connection(resolver)
56
+ checkout_new_connection(uri, options)
57
+ end
167
58
  end
168
59
  end
169
60
 
170
- def try_clone_connection(connection, family)
171
- connection.family ||= family
172
-
173
- return connection if connection.family == family
174
-
175
- new_connection = connection.class.new(connection.origin, connection.options)
176
- new_connection.family = family
61
+ def checkin_connection(connection)
62
+ return if connection.options.io
177
63
 
178
- connection.once(:tcp_open) { new_connection.force_reset }
179
- connection.once(:connect_error) do |err|
180
- if new_connection.connecting?
181
- new_connection.merge(connection)
182
- connection.emit(:cloned, new_connection)
183
- connection.force_reset
184
- else
185
- connection.__send__(:handle_error, err)
186
- end
187
- end
64
+ @connection_mtx.synchronize do
65
+ @connections << connection
188
66
 
189
- new_connection.once(:tcp_open) do |new_conn|
190
- if new_conn != connection
191
- new_conn.merge(connection)
192
- connection.force_reset
193
- end
194
- end
195
- new_connection.once(:connect_error) do |err|
196
- if connection.connecting?
197
- # main connection has the requests
198
- connection.merge(new_connection)
199
- new_connection.emit(:cloned, connection)
200
- new_connection.force_reset
201
- else
202
- new_connection.__send__(:handle_error, err)
203
- end
67
+ @origin_conds[connection.origin.to_s].signal
204
68
  end
205
-
206
- init_connection(new_connection, connection.options)
207
- new_connection
208
69
  end
209
70
 
210
- def on_resolver_connection(connection)
211
- @connections << connection unless @connections.include?(connection)
212
- found_connection = @connections.find do |ch|
213
- ch != connection && ch.mergeable?(connection)
214
- end
215
- return register_connection(connection) unless found_connection
71
+ def checkout_mergeable_connection(connection)
72
+ return if connection.options.io
216
73
 
217
- if found_connection.open?
218
- coalesce_connections(found_connection, connection)
219
- throw(:coalesced, found_connection) unless @connections.include?(connection)
220
- else
221
- found_connection.once(:open) do
222
- coalesce_connections(found_connection, connection)
74
+ @connection_mtx.synchronize do
75
+ idx = @connections.find_index do |ch|
76
+ ch != connection && ch.mergeable?(connection)
223
77
  end
78
+ @connections.delete_at(idx) if idx
224
79
  end
225
80
  end
226
81
 
227
- def on_resolver_error(connection, error)
228
- return connection.emit(:connect_error, error) if connection.connecting? && connection.callbacks_for?(:connect_error)
229
-
230
- connection.emit(:error, error)
82
+ def reset_resolvers
83
+ @resolver_mtx.synchronize { @resolvers.clear }
231
84
  end
232
85
 
233
- def on_resolver_close(resolver)
234
- resolver_type = resolver.class
235
- return if resolver.closed?
86
+ def checkout_resolver(options)
87
+ resolver_type = options.resolver_class
88
+ resolver_type = Resolver.resolver_for(resolver_type)
236
89
 
237
- @resolvers.delete(resolver_type)
90
+ @resolver_mtx.synchronize do
91
+ resolvers = @resolvers[resolver_type]
238
92
 
239
- deselect_connection(resolver)
240
- resolver.close unless resolver.closed?
93
+ idx = resolvers.find_index do |res|
94
+ res.options == options
95
+ end
96
+ resolvers.delete_at(idx) if idx
97
+ end || checkout_new_resolver(resolver_type, options)
241
98
  end
242
99
 
243
- def register_connection(connection)
244
- select_connection(connection)
245
- end
100
+ def checkin_resolver(resolver)
101
+ @resolver_mtx.synchronize do
102
+ resolvers = @resolvers[resolver.class]
246
103
 
247
- def unregister_connection(connection, cleanup = !connection.used?)
248
- @connections.delete(connection) if cleanup
249
- deselect_connection(connection)
250
- end
104
+ resolver = resolver.multi
251
105
 
252
- def select_connection(connection)
253
- @selector.register(connection)
106
+ resolvers << resolver unless resolvers.include?(resolver)
107
+ end
254
108
  end
255
109
 
256
- def deselect_connection(connection)
257
- @selector.deregister(connection)
258
- end
110
+ private
259
111
 
260
- def coalesce_connections(conn1, conn2)
261
- return register_connection(conn2) unless conn1.coalescable?(conn2)
112
+ def acquire_connection(uri, options)
113
+ idx = @connections.find_index do |connection|
114
+ connection.match?(uri, options)
115
+ end
262
116
 
263
- conn2.emit(:tcp_open, conn1)
264
- conn1.merge(conn2)
265
- @connections.delete(conn2)
117
+ @connections.delete_at(idx) if idx
266
118
  end
267
119
 
268
- def next_timeout
269
- [
270
- @timers.wait_interval,
271
- *@resolvers.values.reject(&:closed?).filter_map(&:timeout),
272
- *@connections.filter_map(&:timeout),
273
- ].compact.min
120
+ def checkout_new_connection(uri, options)
121
+ options.connection_class.new(uri, options)
274
122
  end
275
123
 
276
- def find_resolver_for(connection)
277
- connection_options = connection.options
278
- resolver_type = connection_options.resolver_class
279
- resolver_type = Resolver.resolver_for(resolver_type)
280
-
281
- @resolvers[resolver_type] ||= begin
282
- resolver_manager = if resolver_type.multi?
283
- Resolver::Multi.new(resolver_type, connection_options)
284
- else
285
- resolver_type.new(connection_options)
286
- end
287
- resolver_manager.on(:resolve, &method(:on_resolver_connection))
288
- resolver_manager.on(:error, &method(:on_resolver_error))
289
- resolver_manager.on(:close, &method(:on_resolver_close))
290
- resolver_manager
291
- end
292
-
293
- manager = @resolvers[resolver_type]
294
-
295
- (manager.is_a?(Resolver::Multi) && manager.early_resolve(connection)) || manager.resolvers.each do |resolver|
296
- resolver.pool = self
297
- yield resolver
124
+ def checkout_new_resolver(resolver_type, options)
125
+ if resolver_type.multi?
126
+ Resolver::Multi.new(resolver_type, options)
127
+ else
128
+ resolver_type.new(options)
298
129
  end
299
-
300
- manager
301
130
  end
302
131
  end
303
132
  end
@@ -25,20 +25,11 @@ module HTTPX
25
25
  # ..., form: { foo: Pathname.open("path/to/file") }) #=> multipart urlencoded encoder
26
26
  # ..., form: { foo: File.open("path/to/file") }) #=> multipart urlencoded encoder
27
27
  # ..., form: { body: "bla") }) #=> raw data encoder
28
- def initialize(headers, options, body: nil, form: nil, json: nil, xml: nil, **params)
29
- @headers = headers
28
+ def initialize(h, options, **params)
29
+ @headers = h
30
+ @body = self.class.initialize_body(params)
30
31
  @options = options.merge(params)
31
32
 
32
- @body = if body
33
- Transcoder::Body.encode(body)
34
- elsif form
35
- Transcoder::Form.encode(form)
36
- elsif json
37
- Transcoder::JSON.encode(json)
38
- elsif xml
39
- Transcoder::Xml.encode(xml)
40
- end
41
-
42
33
  if @body
43
34
  if @options.compress_request_body && @headers.key?("content-encoding")
44
35
 
@@ -123,6 +114,19 @@ module HTTPX
123
114
  # :nocov:
124
115
 
125
116
  class << self
117
+ def initialize_body(params)
118
+ if (body = params.delete(:body))
119
+ # @type var body: bodyIO
120
+ Transcoder::Body.encode(body)
121
+ elsif (form = params.delete(:form))
122
+ # @type var form: Transcoder::urlencoded_input
123
+ Transcoder::Form.encode(form)
124
+ elsif (json = params.delete(:json))
125
+ # @type var body: _ToJson
126
+ Transcoder::JSON.encode(json)
127
+ end
128
+ end
129
+
126
130
  # returns the +body+ wrapped with the correct deflater accordinng to the given +encodisng+.
127
131
  def initialize_deflater_body(body, encoding)
128
132
  case encoding
data/lib/httpx/request.rb CHANGED
@@ -12,7 +12,7 @@ module HTTPX
12
12
  using URIExtensions
13
13
 
14
14
  # default value used for "user-agent" header, when not overridden.
15
- USER_AGENT = "httpx.rb/#{VERSION}"
15
+ USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
16
16
 
17
17
  # the upcased string HTTP verb for this request.
18
18
  attr_reader :verb
@@ -27,7 +27,7 @@ module HTTPX
27
27
  use_get: false,
28
28
  }.freeze
29
29
 
30
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
30
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?
31
31
 
32
32
  def initialize(_, options)
33
33
  super
@@ -43,7 +43,7 @@ module HTTPX
43
43
  end
44
44
 
45
45
  def <<(connection)
46
- return if @uri.origin == connection.origin.to_s
46
+ return if @uri.origin == connection.peer.to_s
47
47
 
48
48
  @uri_addresses ||= HTTPX::Resolver.nolookup_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
49
49
 
@@ -66,30 +66,23 @@ module HTTPX
66
66
  end
67
67
 
68
68
  def resolver_connection
69
- @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
70
- @building_connection = true
71
- connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
72
- @pool.init_connection(connection, @options)
73
- # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
74
- catch(:coalesced) do
75
- @building_connection = false
76
- emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
77
- connection
78
- end
69
+ # TODO: leaks connection object into the pool
70
+ @resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
71
+ @options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
72
+ emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
79
73
  end
80
74
  end
81
75
 
82
76
  private
83
77
 
84
78
  def resolve(connection = @connections.first, hostname = nil)
85
- return if @building_connection
86
79
  return unless connection
87
80
 
88
81
  hostname ||= @queries.key(connection)
89
82
 
90
83
  if hostname.nil?
91
- hostname = connection.origin.host
92
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
84
+ hostname = connection.peer.host
85
+ log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
93
86
 
94
87
  hostname = @resolver.generate_candidates(hostname).each do |name|
95
88
  @queries[name.to_s] = connection
@@ -108,7 +101,7 @@ module HTTPX
108
101
  @connections << connection
109
102
  rescue ResolveError, Resolv::DNS::EncodeError => e
110
103
  reset_hostname(hostname)
111
- emit_resolve_error(connection, connection.origin.host, e)
104
+ emit_resolve_error(connection, connection.peer.host, e)
112
105
  end
113
106
  end
114
107
 
@@ -117,7 +110,7 @@ module HTTPX
117
110
  rescue StandardError => e
118
111
  hostname = @requests.delete(request)
119
112
  connection = reset_hostname(hostname)
120
- emit_resolve_error(connection, connection.origin.host, e)
113
+ emit_resolve_error(connection, connection.peer.host, e)
121
114
  else
122
115
  # @type var response: HTTPX::Response
123
116
  parse(request, response)
@@ -156,7 +149,7 @@ module HTTPX
156
149
  when :decode_error
157
150
  host = @requests.delete(request)
158
151
  connection = reset_hostname(host)
159
- emit_resolve_error(connection, connection.origin.host, result)
152
+ emit_resolve_error(connection, connection.peer.host, result)
160
153
  end
161
154
  end
162
155
 
@@ -176,7 +169,7 @@ module HTTPX
176
169
  alias_address = answers[address["alias"]]
177
170
  if alias_address.nil?
178
171
  reset_hostname(address["name"])
179
- if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
172
+ if early_resolve(connection, hostname: address["alias"])
180
173
  @connections.delete(connection)
181
174
  else
182
175
  resolve(connection, address["alias"])
@@ -8,27 +8,45 @@ module HTTPX
8
8
  include Callbacks
9
9
  using ArrayExtensions::FilterMap
10
10
 
11
- attr_reader :resolvers
11
+ attr_reader :resolvers, :options
12
12
 
13
13
  def initialize(resolver_type, options)
14
+ @current_selector = nil
15
+ @current_session = nil
14
16
  @options = options
15
17
  @resolver_options = @options.resolver_options
16
18
 
17
19
  @resolvers = options.ip_families.map do |ip_family|
18
20
  resolver = resolver_type.new(ip_family, options)
19
- resolver.on(:resolve, &method(:on_resolver_connection))
20
- resolver.on(:error, &method(:on_resolver_error))
21
- resolver.on(:close) { on_resolver_close(resolver) }
21
+ resolver.multi = self
22
22
  resolver
23
23
  end
24
24
 
25
25
  @errors = Hash.new { |hs, k| hs[k] = [] }
26
26
  end
27
27
 
28
+ def current_selector=(s)
29
+ @current_selector = s
30
+ @resolvers.each { |r| r.__send__(__method__, s) }
31
+ end
32
+
33
+ def current_session=(s)
34
+ @current_session = s
35
+ @resolvers.each { |r| r.__send__(__method__, s) }
36
+ end
37
+
28
38
  def closed?
29
39
  @resolvers.all?(&:closed?)
30
40
  end
31
41
 
42
+ def empty?
43
+ @resolvers.all?(&:empty?)
44
+ end
45
+
46
+ def inflight?
47
+ @resolvers.any(&:inflight?)
48
+ end
49
+
32
50
  def timeout
33
51
  @resolvers.filter_map(&:timeout).min
34
52
  end
@@ -42,10 +60,11 @@ module HTTPX
42
60
  end
43
61
 
44
62
  def early_resolve(connection)
45
- hostname = connection.origin.host
63
+ hostname = connection.peer.host
46
64
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
47
- return unless addresses
65
+ return false unless addresses
48
66
 
67
+ resolved = false
49
68
  addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
50
69
  # try to match the resolver by family. However, there are cases where that's not possible, as when
51
70
  # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
@@ -55,21 +74,20 @@ module HTTPX
55
74
 
56
75
  # it does not matter which resolver it is, as early-resolve code is shared.
57
76
  resolver.emit_addresses(connection, family, addrs, true)
58
- end
59
- end
60
77
 
61
- private
78
+ resolved = true
79
+ end
62
80
 
63
- def on_resolver_connection(connection)
64
- emit(:resolve, connection)
81
+ resolved
65
82
  end
66
83
 
67
- def on_resolver_error(connection, error)
68
- emit(:error, connection, error)
69
- end
84
+ def lazy_resolve(connection)
85
+ @resolvers.each do |resolver|
86
+ resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
87
+ next if resolver.empty?
70
88
 
71
- def on_resolver_close(resolver)
72
- emit(:close, resolver)
89
+ @current_session.select_resolver(resolver, @current_selector)
90
+ end
73
91
  end
74
92
  end
75
93
  end