httpx 1.4.0 → 1.4.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/doc/release_notes/1_4_2.md +20 -0
  5. data/lib/httpx/adapters/datadog.rb +55 -83
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/webmock.rb +7 -1
  8. data/lib/httpx/callbacks.rb +2 -2
  9. data/lib/httpx/connection/http2.rb +2 -2
  10. data/lib/httpx/connection.rb +106 -51
  11. data/lib/httpx/errors.rb +3 -0
  12. data/lib/httpx/loggable.rb +8 -1
  13. data/lib/httpx/plugins/callbacks.rb +1 -0
  14. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  15. data/lib/httpx/plugins/expect.rb +1 -1
  16. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  17. data/lib/httpx/request/body.rb +9 -14
  18. data/lib/httpx/request.rb +20 -0
  19. data/lib/httpx/resolver/https.rb +4 -2
  20. data/lib/httpx/resolver/native.rb +111 -55
  21. data/lib/httpx/resolver/resolver.rb +18 -11
  22. data/lib/httpx/resolver/system.rb +3 -5
  23. data/lib/httpx/selector.rb +33 -23
  24. data/lib/httpx/session.rb +17 -43
  25. data/lib/httpx/timers.rb +16 -1
  26. data/lib/httpx/transcoder/body.rb +15 -31
  27. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  28. data/lib/httpx/version.rb +1 -1
  29. data/lib/httpx.rb +1 -1
  30. data/sig/callbacks.rbs +2 -2
  31. data/sig/connection.rbs +19 -5
  32. data/sig/errors.rbs +3 -0
  33. data/sig/request/body.rbs +0 -8
  34. data/sig/request.rbs +3 -0
  35. data/sig/resolver/native.rbs +6 -1
  36. data/sig/selector.rbs +1 -0
  37. data/sig/session.rbs +2 -0
  38. data/sig/timers.rbs +15 -4
  39. data/sig/transcoder/body.rbs +1 -3
  40. data/sig/transcoder/utils/body_reader.rbs +1 -1
  41. data/sig/transcoder/utils/deflater.rbs +1 -1
  42. metadata +7 -3
@@ -35,6 +35,7 @@ module HTTPX
35
35
  @_timeouts = Array(@resolver_options[:timeouts])
36
36
  @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
37
37
  @connections = []
38
+ @name = nil
38
39
  @queries = {}
39
40
  @read_buffer = "".b
40
41
  @write_buffer = Buffer.new(@resolver_options[:packet_size])
@@ -58,19 +59,6 @@ module HTTPX
58
59
  when :open
59
60
  consume
60
61
  end
61
- nil
62
- rescue Errno::EHOSTUNREACH => e
63
- @ns_index += 1
64
- nameserver = @nameserver
65
- if nameserver && @ns_index < nameserver.size
66
- log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
67
- transition(:idle)
68
- @timeouts.clear
69
- else
70
- handle_error(e)
71
- end
72
- rescue NativeResolveError => e
73
- handle_error(e)
74
62
  end
75
63
 
76
64
  def interests
@@ -105,9 +93,7 @@ module HTTPX
105
93
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
106
94
  end
107
95
 
108
- def handle_socket_timeout(interval)
109
- do_retry(interval)
110
- end
96
+ def handle_socket_timeout(interval); end
111
97
 
112
98
  private
113
99
 
@@ -120,54 +106,94 @@ module HTTPX
120
106
  end
121
107
 
122
108
  def consume
123
- dread if calculate_interests == :r
124
- do_retry
125
- dwrite if calculate_interests == :w
109
+ loop do
110
+ dread if calculate_interests == :r
111
+
112
+ break unless calculate_interests == :w
113
+
114
+ # do_retry
115
+ dwrite
116
+
117
+ break unless calculate_interests == :r
118
+ end
119
+ rescue Errno::EHOSTUNREACH => e
120
+ @ns_index += 1
121
+ nameserver = @nameserver
122
+ if nameserver && @ns_index < nameserver.size
123
+ log do
124
+ "resolver #{FAMILY_TYPES[@record_type]}: " \
125
+ "failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
126
+ end
127
+ transition(:idle)
128
+ @timeouts.clear
129
+ retry
130
+ else
131
+ handle_error(e)
132
+ emit(:close, self)
133
+ end
134
+ rescue NativeResolveError => e
135
+ handle_error(e)
136
+ close_or_resolve
137
+ retry unless closed?
126
138
  end
127
139
 
128
- def do_retry(loop_time = nil)
129
- return if @queries.empty? || !@start_timeout
140
+ def schedule_retry
141
+ h = @name
130
142
 
131
- loop_time ||= Utils.elapsed_time(@start_timeout)
143
+ return unless h
132
144
 
133
- query = @queries.first
145
+ connection = @queries[h]
134
146
 
135
- return unless query
147
+ timeouts = @timeouts[h]
148
+ timeout = timeouts.shift
136
149
 
137
- h, connection = query
138
- host = connection.peer.host
139
- timeout = (@timeouts[host][0] -= loop_time)
150
+ @timer = @current_selector.after(timeout) do
151
+ next unless @connections.include?(connection)
140
152
 
141
- return unless timeout <= 0
153
+ do_retry(h, connection, timeout)
154
+ end
155
+ end
142
156
 
143
- @timeouts[host].shift
157
+ def do_retry(h, connection, interval)
158
+ timeouts = @timeouts[h]
144
159
 
145
- if !@timeouts[host].empty?
146
- log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
160
+ if !timeouts.empty?
161
+ log do
162
+ "resolver #{FAMILY_TYPES[@record_type]}: timeout after #{interval}s, retry (with #{timeouts.first}s) #{h}..."
163
+ end
147
164
  # must downgrade to tcp AND retry on same host as last
148
165
  downgrade_socket
149
166
  resolve(connection, h)
150
167
  elsif @ns_index + 1 < @nameserver.size
151
168
  # try on the next nameserver
152
169
  @ns_index += 1
153
- log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
170
+ log do
171
+ "resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{h} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)"
172
+ end
154
173
  transition(:idle)
155
174
  @timeouts.clear
156
175
  resolve(connection, h)
157
176
  else
158
177
 
159
- @timeouts.delete(host)
178
+ @timeouts.delete(h)
160
179
  reset_hostname(h, reset_candidates: false)
161
180
 
162
- return unless @queries.empty?
181
+ unless @queries.empty?
182
+ resolve(connection)
183
+ return
184
+ end
163
185
 
164
186
  @connections.delete(connection)
187
+
188
+ host = connection.peer.host
189
+
165
190
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
166
191
  # resolve timeout, including from the previous retries.
167
- ex = ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.peer.host}")
192
+ ex = ResolveTimeoutError.new(interval, "Timed out while resolving #{host}")
168
193
  ex.set_backtrace(ex ? ex.backtrace : caller)
169
194
  emit_resolve_error(connection, host, ex)
170
- emit(:close, self)
195
+
196
+ close_or_resolve
171
197
  end
172
198
  end
173
199
 
@@ -216,7 +242,7 @@ module HTTPX
216
242
  parse(@read_buffer)
217
243
  end
218
244
 
219
- return if @state == :closed
245
+ return if @state == :closed || !@write_buffer.empty?
220
246
  end
221
247
  end
222
248
 
@@ -234,11 +260,15 @@ module HTTPX
234
260
 
235
261
  return unless siz.positive?
236
262
 
263
+ schedule_retry if @write_buffer.empty?
264
+
237
265
  return if @state == :closed
238
266
  end
239
267
  end
240
268
 
241
269
  def parse(buffer)
270
+ @timer.cancel
271
+
242
272
  code, result = Resolver.decode_dns_answer(buffer)
243
273
 
244
274
  case code
@@ -249,15 +279,17 @@ module HTTPX
249
279
  hostname, connection = @queries.first
250
280
  reset_hostname(hostname, reset_candidates: false)
251
281
 
252
- unless @queries.value?(connection)
282
+ other_candidate, _ = @queries.find { |_, conn| conn == connection }
283
+
284
+ if other_candidate
285
+ resolve(connection, other_candidate)
286
+ else
253
287
  @connections.delete(connection)
254
288
  ex = NativeResolveError.new(connection, connection.peer.host, "name or service not known")
255
289
  ex.set_backtrace(ex ? ex.backtrace : caller)
256
290
  emit_resolve_error(connection, connection.peer.host, ex)
257
- emit(:close, self)
291
+ close_or_resolve
258
292
  end
259
-
260
- resolve
261
293
  when :message_truncated
262
294
  # TODO: what to do if it's already tcp??
263
295
  return if @socket_type == :tcp
@@ -312,8 +344,10 @@ module HTTPX
312
344
  connection = @queries.delete(name)
313
345
  end
314
346
 
315
- if address.key?("alias") # CNAME
316
- hostname_alias = address["alias"]
347
+ alias_addresses, addresses = addresses.partition { |addr| addr.key?("alias") }
348
+
349
+ if addresses.empty? && !alias_addresses.empty? # CNAME
350
+ hostname_alias = alias_addresses.first["alias"]
317
351
  # clean up intermediate queries
318
352
  @timeouts.delete(name) unless connection.peer.host == name
319
353
 
@@ -326,7 +360,7 @@ module HTTPX
326
360
  transition(:idle)
327
361
  transition(:open)
328
362
  end
329
- log { "resolver: ALIAS #{hostname_alias} for #{name}" }
363
+ log { "resolver #{FAMILY_TYPES[@record_type]}: ALIAS #{hostname_alias} for #{name}" }
330
364
  resolve(connection, hostname_alias)
331
365
  return
332
366
  end
@@ -338,12 +372,14 @@ module HTTPX
338
372
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
339
373
  end
340
374
  end
341
- return emit(:close, self) if @connections.empty?
342
-
343
- resolve
375
+ close_or_resolve
344
376
  end
345
377
 
346
- def resolve(connection = @connections.first, hostname = nil)
378
+ def resolve(connection = nil, hostname = nil)
379
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
380
+
381
+ connection ||= @connections.find { |c| !@queries.value?(c) }
382
+
347
383
  raise Error, "no URI to resolve" unless connection
348
384
 
349
385
  return unless @write_buffer.empty?
@@ -352,7 +388,10 @@ module HTTPX
352
388
 
353
389
  if hostname.nil?
354
390
  hostname = connection.peer.host
355
- log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
391
+ log do
392
+ "resolver #{FAMILY_TYPES[@record_type]}: " \
393
+ "resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
394
+ end if connection.peer.non_ascii_hostname
356
395
 
357
396
  hostname = generate_candidates(hostname).each do |name|
358
397
  @queries[name] = connection
@@ -360,14 +399,17 @@ module HTTPX
360
399
  else
361
400
  @queries[hostname] = connection
362
401
  end
363
- log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
402
+
403
+ @name = hostname
404
+
405
+ log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
364
406
  begin
365
407
  @write_buffer << encode_dns_query(hostname)
366
408
  rescue Resolv::DNS::EncodeError => e
367
409
  reset_hostname(hostname, connection: connection)
368
410
  @connections.delete(connection)
369
411
  emit_resolve_error(connection, hostname, e)
370
- emit(:close, self) if @connections.empty?
412
+ close_or_resolve
371
413
  end
372
414
  end
373
415
 
@@ -397,10 +439,10 @@ module HTTPX
397
439
 
398
440
  case @socket_type
399
441
  when :udp
400
- log { "resolver: server: udp://#{ip}:#{port}..." }
442
+ log { "resolver #{FAMILY_TYPES[@record_type]}: server: udp://#{ip}:#{port}..." }
401
443
  UDP.new(ip, port, @options)
402
444
  when :tcp
403
- log { "resolver: server: tcp://#{ip}:#{port}..." }
445
+ log { "resolver #{FAMILY_TYPES[@record_type]}: server: tcp://#{ip}:#{port}..." }
404
446
  origin = URI("tcp://#{ip}:#{port}")
405
447
  TCP.new(origin, [ip], @options)
406
448
  end
@@ -448,6 +490,7 @@ module HTTPX
448
490
  # these errors may happen during TCP handshake
449
491
  # treat them as resolve errors.
450
492
  handle_error(e)
493
+ emit(:close, self)
451
494
  end
452
495
 
453
496
  def handle_error(error)
@@ -462,13 +505,15 @@ module HTTPX
462
505
  @connections.delete(connection)
463
506
  emit_resolve_error(connection, host, error)
464
507
  end
508
+
509
+ while (connection = @connections.shift)
510
+ emit_resolve_error(connection, host, error)
511
+ end
465
512
  end
466
- emit(:close, self) if @connections.empty?
467
513
  end
468
514
 
469
515
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
470
516
  @timeouts.delete(hostname)
471
- @timeouts.delete(hostname)
472
517
 
473
518
  return unless connection && reset_candidates
474
519
 
@@ -478,5 +523,16 @@ module HTTPX
478
523
  # reset timeouts
479
524
  @timeouts.delete_if { |h, _| candidates.include?(h) }
480
525
  end
526
+
527
+ def close_or_resolve
528
+ # drop already closed connections
529
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
530
+
531
+ if (@connections - @queries.values).empty?
532
+ emit(:close, self)
533
+ else
534
+ resolve
535
+ end
536
+ end
481
537
  end
482
538
  end
@@ -72,17 +72,22 @@ module HTTPX
72
72
  # double emission check, but allow early resolution to work
73
73
  return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
74
74
 
75
- log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.peer.host}: #{addresses.inspect}" }
76
- if @current_selector && # if triggered by early resolve, session may not be here yet
77
- !connection.io &&
78
- connection.options.ip_families.size > 1 &&
79
- family == Socket::AF_INET &&
80
- addresses.first.to_s != connection.peer.host.to_s
81
- log { "resolver: A response, applying resolution delay..." }
75
+ log do
76
+ "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
77
+ "answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
78
+ end
79
+
80
+ if !early_resolve && # do not apply resolution delay for non-dns name resolution
81
+ @current_selector && # just in case...
82
+ family == Socket::AF_INET && # resolution delay only applies to IPv4
83
+ !connection.io && # connection already has addresses and initiated/ended handshake
84
+ connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
85
+ addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
86
+ log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
87
+
82
88
  @current_selector.after(0.05) do
83
- unless connection.state == :closed ||
84
- # double emission check
85
- (connection.addresses && addresses.intersect?(connection.addresses))
89
+ # double emission check
90
+ unless connection.addresses && addresses.intersect?(connection.addresses)
86
91
  emit_resolved_connection(connection, addresses, early_resolve)
87
92
  end
88
93
  end
@@ -97,6 +102,8 @@ module HTTPX
97
102
  begin
98
103
  connection.addresses = addresses
99
104
 
105
+ return if connection.state == :closed
106
+
100
107
  emit(:resolve, connection)
101
108
  rescue StandardError => e
102
109
  if early_resolve
@@ -146,7 +153,7 @@ module HTTPX
146
153
  end
147
154
 
148
155
  def emit_connection_error(connection, error)
149
- return connection.emit(:connect_error, error) if connection.connecting? && connection.callbacks_for?(:connect_error)
156
+ return connection.handle_connect_error(error) if connection.connecting?
150
157
 
151
158
  connection.emit(:error, error)
152
159
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "resolv"
5
4
 
6
5
  module HTTPX
7
6
  class Resolver::System < Resolver::Resolver
8
7
  using URIExtensions
9
- extend Forwardable
10
8
 
11
9
  RESOLV_ERRORS = [Resolv::ResolvError,
12
10
  Resolv::DNS::Requester::RequestError,
@@ -24,8 +22,6 @@ module HTTPX
24
22
 
25
23
  attr_reader :state
26
24
 
27
- def_delegator :@connections, :empty?
28
-
29
25
  def initialize(options)
30
26
  super(nil, options)
31
27
  @resolver_options = @options.resolver_options
@@ -162,7 +158,9 @@ module HTTPX
162
158
 
163
159
  hostname = connection.peer.host
164
160
  scheme = connection.origin.scheme
165
- log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
161
+ log do
162
+ "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
163
+ end if connection.peer.non_ascii_hostname
166
164
 
167
165
  transition(:open)
168
166
 
@@ -19,6 +19,7 @@ module HTTPX
19
19
  def initialize
20
20
  @timers = Timers.new
21
21
  @selectables = []
22
+ @is_timer_interval = false
22
23
  end
23
24
 
24
25
  def each(&blk)
@@ -43,7 +44,11 @@ module HTTPX
43
44
  rescue StandardError => e
44
45
  emit_error(e)
45
46
  rescue Exception # rubocop:disable Lint/RescueException
46
- each_connection(&:force_reset)
47
+ each_connection do |conn|
48
+ conn.force_reset
49
+ conn.disconnect
50
+ end
51
+
47
52
  raise
48
53
  end
49
54
 
@@ -92,10 +97,6 @@ module HTTPX
92
97
  end
93
98
  end
94
99
 
95
- def empty?
96
- @selectables.empty?
97
- end
98
-
99
100
  # deregisters +io+ from selectables.
100
101
  def deregister(io)
101
102
  @selectables.delete(io)
@@ -129,24 +130,22 @@ module HTTPX
129
130
  # first, we group IOs based on interest type. On call to #interests however,
130
131
  # things might already happen, and new IOs might be registered, so we might
131
132
  # have to start all over again. We do this until we group all selectables
132
- begin
133
- @selectables.delete_if do |io|
134
- interests = io.interests
133
+ @selectables.delete_if do |io|
134
+ interests = io.interests
135
135
 
136
- (r ||= []) << io if READABLE.include?(interests)
137
- (w ||= []) << io if WRITABLE.include?(interests)
136
+ (r ||= []) << io if READABLE.include?(interests)
137
+ (w ||= []) << io if WRITABLE.include?(interests)
138
138
 
139
- io.state == :closed
140
- end
139
+ io.state == :closed
140
+ end
141
141
 
142
- # TODO: what to do if there are no selectables?
142
+ # TODO: what to do if there are no selectables?
143
143
 
144
- readers, writers = IO.select(r, w, nil, interval)
144
+ readers, writers = IO.select(r, w, nil, interval)
145
145
 
146
- if readers.nil? && writers.nil? && interval
147
- [*r, *w].each { |io| io.handle_socket_timeout(interval) }
148
- return
149
- end
146
+ if readers.nil? && writers.nil? && interval
147
+ [*r, *w].each { |io| io.handle_socket_timeout(interval) }
148
+ return
150
149
  end
151
150
 
152
151
  if writers
@@ -178,7 +177,7 @@ module HTTPX
178
177
  end
179
178
 
180
179
  unless result || interval.nil?
181
- io.handle_socket_timeout(interval)
180
+ io.handle_socket_timeout(interval) unless @is_timer_interval
182
181
  return
183
182
  end
184
183
  # raise TimeoutError.new(interval, "timed out while waiting on select")
@@ -190,10 +189,21 @@ module HTTPX
190
189
  end
191
190
 
192
191
  def next_timeout
193
- [
194
- @timers.wait_interval,
195
- @selectables.filter_map(&:timeout).min,
196
- ].compact.min
192
+ @is_timer_interval = false
193
+
194
+ timer_interval = @timers.wait_interval
195
+
196
+ connection_interval = @selectables.filter_map(&:timeout).min
197
+
198
+ return connection_interval unless timer_interval
199
+
200
+ if connection_interval.nil? || timer_interval <= connection_interval
201
+ @is_timer_interval = true
202
+
203
+ return timer_interval
204
+ end
205
+
206
+ connection_interval
197
207
  end
198
208
 
199
209
  def emit_error(e)
data/lib/httpx/session.rb CHANGED
@@ -69,8 +69,6 @@ module HTTPX
69
69
  while (connection = @pool.pop_connection)
70
70
  next if connection.state == :closed
71
71
 
72
- connection.current_session = self
73
- connection.current_selector = selector
74
72
  select_connection(connection, selector)
75
73
  end
76
74
  begin
@@ -126,9 +124,15 @@ module HTTPX
126
124
  end
127
125
 
128
126
  def select_connection(connection, selector)
127
+ pin_connection(connection, selector)
129
128
  selector.register(connection)
130
129
  end
131
130
 
131
+ def pin_connection(connection, selector)
132
+ connection.current_session = self
133
+ connection.current_selector = selector
134
+ end
135
+
132
136
  alias_method :select_resolver, :select_connection
133
137
 
134
138
  def deselect_connection(connection, selector, cloned = false)
@@ -160,36 +164,8 @@ module HTTPX
160
164
  new_connection = connection.class.new(connection.origin, connection.options)
161
165
 
162
166
  new_connection.family = family
163
- new_connection.current_session = self
164
- new_connection.current_selector = selector
165
-
166
- connection.once(:tcp_open) { new_connection.force_reset(true) }
167
- connection.once(:connect_error) do |err|
168
- if new_connection.connecting?
169
- new_connection.merge(connection)
170
- connection.emit(:cloned, new_connection)
171
- connection.force_reset(true)
172
- else
173
- connection.__send__(:handle_error, err)
174
- end
175
- end
176
167
 
177
- new_connection.once(:tcp_open) do |new_conn|
178
- if new_conn != connection
179
- new_conn.merge(connection)
180
- connection.force_reset(true)
181
- end
182
- end
183
- new_connection.once(:connect_error) do |err|
184
- if connection.connecting?
185
- # main connection has the requests
186
- connection.merge(new_connection)
187
- new_connection.emit(:cloned, connection)
188
- new_connection.force_reset(true)
189
- else
190
- new_connection.__send__(:handle_error, err)
191
- end
192
- end
168
+ connection.sibling = new_connection
193
169
 
194
170
  do_init_connection(new_connection, selector)
195
171
  new_connection
@@ -203,14 +179,15 @@ module HTTPX
203
179
 
204
180
  connection = @pool.checkout_connection(request_uri, options)
205
181
 
206
- connection.current_session = self
207
- connection.current_selector = selector
208
-
209
182
  case connection.state
210
183
  when :idle
211
184
  do_init_connection(connection, selector)
212
185
  when :open
213
- select_connection(connection, selector) if options.io
186
+ if options.io
187
+ select_connection(connection, selector)
188
+ else
189
+ pin_connection(connection, selector)
190
+ end
214
191
  when :closed
215
192
  connection.idling
216
193
  select_connection(connection, selector)
@@ -219,6 +196,8 @@ module HTTPX
219
196
  connection.idling
220
197
  select_connection(connection, selector)
221
198
  end
199
+ else
200
+ pin_connection(connection, selector)
222
201
  end
223
202
 
224
203
  connection
@@ -261,11 +240,9 @@ module HTTPX
261
240
  end
262
241
  return unless error && error.is_a?(Exception)
263
242
 
264
- if error.is_a?(Error)
265
- request.emit(:response, ErrorResponse.new(request, error))
266
- else
267
- raise error if selector.empty?
268
- end
243
+ raise error unless error.is_a?(Error)
244
+
245
+ request.emit(:response, ErrorResponse.new(request, error))
269
246
  end
270
247
 
271
248
  # returns a set of HTTPX::Request objects built from the given +args+ and +options+.
@@ -372,7 +349,6 @@ module HTTPX
372
349
  # resolve a name (not the same as name being an IP, yet)
373
350
  # 2. when the connection is initialized with an external already open IO.
374
351
  #
375
- connection.once(:connect_error, &connection.method(:handle_error))
376
352
  on_resolver_connection(connection, selector)
377
353
  return
378
354
  end
@@ -428,8 +404,6 @@ module HTTPX
428
404
  return false
429
405
  end
430
406
 
431
- conn2.emit(:tcp_open, conn1)
432
- conn1.merge(conn2)
433
407
  conn2.coalesced_connection = conn1
434
408
  select_connection(conn1, selector) if from_pool
435
409
  deselect_connection(conn2, selector)
data/lib/httpx/timers.rb CHANGED
@@ -26,7 +26,7 @@ module HTTPX
26
26
 
27
27
  @next_interval_at = nil
28
28
 
29
- interval
29
+ Timer.new(interval, callback)
30
30
  end
31
31
 
32
32
  def wait_interval
@@ -48,6 +48,17 @@ module HTTPX
48
48
  @next_interval_at = nil if @intervals.empty?
49
49
  end
50
50
 
51
+ class Timer
52
+ def initialize(interval, callback)
53
+ @interval = interval
54
+ @callback = callback
55
+ end
56
+
57
+ def cancel
58
+ @interval.delete(@callback)
59
+ end
60
+ end
61
+
51
62
  class Interval
52
63
  include Comparable
53
64
 
@@ -63,6 +74,10 @@ module HTTPX
63
74
  @on_empty = blk
64
75
  end
65
76
 
77
+ def cancel
78
+ @on_empty.call
79
+ end
80
+
66
81
  def <=>(other)
67
82
  @interval <=> other.interval
68
83
  end