httpx 1.4.3 → 1.5.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_4.md +14 -0
  3. data/doc/release_notes/1_5_0.md +126 -0
  4. data/lib/httpx/adapters/datadog.rb +24 -3
  5. data/lib/httpx/adapters/webmock.rb +3 -0
  6. data/lib/httpx/buffer.rb +16 -5
  7. data/lib/httpx/connection/http1.rb +8 -9
  8. data/lib/httpx/connection/http2.rb +48 -24
  9. data/lib/httpx/connection.rb +40 -20
  10. data/lib/httpx/errors.rb +2 -11
  11. data/lib/httpx/headers.rb +24 -23
  12. data/lib/httpx/io/ssl.rb +8 -4
  13. data/lib/httpx/io/tcp.rb +9 -7
  14. data/lib/httpx/io/unix.rb +1 -1
  15. data/lib/httpx/loggable.rb +13 -1
  16. data/lib/httpx/options.rb +63 -48
  17. data/lib/httpx/parser/http1.rb +1 -1
  18. data/lib/httpx/plugins/aws_sigv4.rb +1 -0
  19. data/lib/httpx/plugins/callbacks.rb +19 -6
  20. data/lib/httpx/plugins/circuit_breaker.rb +4 -3
  21. data/lib/httpx/plugins/cookies/jar.rb +0 -2
  22. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +7 -4
  23. data/lib/httpx/plugins/cookies.rb +4 -4
  24. data/lib/httpx/plugins/follow_redirects.rb +4 -2
  25. data/lib/httpx/plugins/grpc/call.rb +1 -1
  26. data/lib/httpx/plugins/h2c.rb +7 -1
  27. data/lib/httpx/plugins/persistent.rb +22 -1
  28. data/lib/httpx/plugins/proxy/http.rb +3 -1
  29. data/lib/httpx/plugins/query.rb +35 -0
  30. data/lib/httpx/plugins/response_cache/file_store.rb +115 -15
  31. data/lib/httpx/plugins/response_cache/store.rb +7 -67
  32. data/lib/httpx/plugins/response_cache.rb +179 -29
  33. data/lib/httpx/plugins/retries.rb +27 -15
  34. data/lib/httpx/plugins/stream.rb +46 -20
  35. data/lib/httpx/plugins/stream_bidi.rb +315 -0
  36. data/lib/httpx/pool.rb +58 -5
  37. data/lib/httpx/request/body.rb +1 -1
  38. data/lib/httpx/request.rb +21 -5
  39. data/lib/httpx/resolver/https.rb +10 -4
  40. data/lib/httpx/resolver/native.rb +13 -13
  41. data/lib/httpx/resolver/resolver.rb +4 -0
  42. data/lib/httpx/resolver/system.rb +37 -14
  43. data/lib/httpx/resolver.rb +2 -2
  44. data/lib/httpx/response/body.rb +10 -21
  45. data/lib/httpx/response/buffer.rb +36 -12
  46. data/lib/httpx/response.rb +11 -1
  47. data/lib/httpx/selector.rb +16 -12
  48. data/lib/httpx/session.rb +80 -23
  49. data/lib/httpx/timers.rb +24 -16
  50. data/lib/httpx/transcoder/multipart/decoder.rb +4 -2
  51. data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
  52. data/lib/httpx/version.rb +1 -1
  53. data/sig/buffer.rbs +1 -1
  54. data/sig/chainable.rbs +5 -2
  55. data/sig/connection/http2.rbs +11 -2
  56. data/sig/connection.rbs +4 -4
  57. data/sig/errors.rbs +0 -3
  58. data/sig/headers.rbs +15 -10
  59. data/sig/httpx.rbs +5 -1
  60. data/sig/io/tcp.rbs +6 -0
  61. data/sig/loggable.rbs +2 -0
  62. data/sig/options.rbs +7 -1
  63. data/sig/plugins/cookies/cookie.rbs +1 -3
  64. data/sig/plugins/cookies/jar.rbs +4 -4
  65. data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
  66. data/sig/plugins/cookies.rbs +2 -0
  67. data/sig/plugins/h2c.rbs +4 -0
  68. data/sig/plugins/proxy/http.rbs +3 -0
  69. data/sig/plugins/proxy.rbs +4 -0
  70. data/sig/plugins/response_cache/file_store.rbs +19 -0
  71. data/sig/plugins/response_cache/store.rbs +13 -0
  72. data/sig/plugins/response_cache.rbs +41 -19
  73. data/sig/plugins/retries.rbs +4 -3
  74. data/sig/plugins/stream.rbs +8 -1
  75. data/sig/plugins/stream_bidi.rbs +68 -0
  76. data/sig/plugins/upgrade/h2.rbs +9 -0
  77. data/sig/plugins/upgrade.rbs +5 -0
  78. data/sig/pool.rbs +5 -0
  79. data/sig/punycode.rbs +5 -0
  80. data/sig/request.rbs +7 -0
  81. data/sig/resolver/https.rbs +3 -2
  82. data/sig/resolver/native.rbs +1 -2
  83. data/sig/resolver/resolver.rbs +11 -3
  84. data/sig/resolver/system.rbs +19 -2
  85. data/sig/resolver.rbs +11 -7
  86. data/sig/response/body.rbs +3 -4
  87. data/sig/response/buffer.rbs +2 -3
  88. data/sig/response.rbs +2 -2
  89. data/sig/selector.rbs +20 -10
  90. data/sig/session.rbs +14 -6
  91. data/sig/timers.rbs +5 -7
  92. data/sig/transcoder/multipart.rbs +4 -3
  93. metadata +14 -5
  94. data/lib/httpx/session2.rb +0 -23
  95. data/lib/httpx/transcoder/utils/inflater.rb +0 -21
  96. data/sig/transcoder/utils/inflater.rbs +0 -12
@@ -17,7 +17,9 @@ module HTTPX
17
17
  # TODO: pass max_retries in a configure/load block
18
18
 
19
19
  IDEMPOTENT_METHODS = %w[GET OPTIONS HEAD PUT DELETE].freeze
20
- RETRYABLE_ERRORS = [
20
+
21
+ # subset of retryable errors which are safe to retry when reconnecting
22
+ RECONNECTABLE_ERRORS = [
21
23
  IOError,
22
24
  EOFError,
23
25
  Errno::ECONNRESET,
@@ -25,12 +27,15 @@ module HTTPX
25
27
  Errno::EPIPE,
26
28
  Errno::EINVAL,
27
29
  Errno::ETIMEDOUT,
28
- Parser::Error,
29
- TLSError,
30
- TimeoutError,
31
30
  ConnectionError,
32
- Connection::HTTP2::GoawayError,
31
+ TLSError,
32
+ Connection::HTTP2::Error,
33
33
  ].freeze
34
+
35
+ RETRYABLE_ERRORS = (RECONNECTABLE_ERRORS + [
36
+ Parser::Error,
37
+ TimeoutError,
38
+ ]).freeze
34
39
  DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
35
40
 
36
41
  if ENV.key?("HTTPX_NO_JITTER")
@@ -88,6 +93,7 @@ module HTTPX
88
93
  end
89
94
 
90
95
  module InstanceMethods
96
+ # returns a `:retries` plugin enabled session with +n+ maximum retries per request setting.
91
97
  def max_retries(n)
92
98
  with(max_retries: n)
93
99
  end
@@ -99,18 +105,18 @@ module HTTPX
99
105
 
100
106
  if response &&
101
107
  request.retries.positive? &&
102
- __repeatable_request?(request, options) &&
108
+ repeatable_request?(request, options) &&
103
109
  (
104
110
  (
105
- response.is_a?(ErrorResponse) && __retryable_error?(response.error)
111
+ response.is_a?(ErrorResponse) && retryable_error?(response.error)
106
112
  ) ||
107
113
  (
108
114
  options.retry_on && options.retry_on.call(response)
109
115
  )
110
116
  )
111
- __try_partial_retry(request, response)
117
+ try_partial_retry(request, response)
112
118
  log { "failed to get response, #{request.retries} tries to go..." }
113
- request.retries -= 1
119
+ request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
114
120
  request.transition(:idle)
115
121
 
116
122
  retry_after = options.retry_after
@@ -125,9 +131,10 @@ module HTTPX
125
131
  retry_start = Utils.now
126
132
  log { "retrying after #{retry_after} secs..." }
127
133
  selector.after(retry_after) do
128
- if request.response
134
+ if (response = request.response)
135
+ response.finish!
129
136
  # request has terminated abruptly meanwhile
130
- request.emit(:response, request.response)
137
+ request.emit(:response, response)
131
138
  else
132
139
  log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
133
140
  send_request(request, selector, options)
@@ -142,11 +149,13 @@ module HTTPX
142
149
  response
143
150
  end
144
151
 
145
- def __repeatable_request?(request, options)
152
+ # returns whether +request+ can be retried.
153
+ def repeatable_request?(request, options)
146
154
  IDEMPOTENT_METHODS.include?(request.verb) || options.retry_change_requests
147
155
  end
148
156
 
149
- def __retryable_error?(ex)
157
+ # returns whether the +ex+ exception happend for a retriable request.
158
+ def retryable_error?(ex)
150
159
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
151
160
  end
152
161
 
@@ -155,11 +164,11 @@ module HTTPX
155
164
  end
156
165
 
157
166
  #
158
- # Atttempt to set the request to perform a partial range request.
167
+ # Attempt to set the request to perform a partial range request.
159
168
  # This happens if the peer server accepts byte-range requests, and
160
169
  # the last response contains some body payload.
161
170
  #
162
- def __try_partial_retry(request, response)
171
+ def try_partial_retry(request, response)
163
172
  response = response.response if response.is_a?(ErrorResponse)
164
173
 
165
174
  return unless response
@@ -180,10 +189,13 @@ module HTTPX
180
189
  end
181
190
 
182
191
  module RequestMethods
192
+ # number of retries left.
183
193
  attr_accessor :retries
184
194
 
195
+ # a response partially received before.
185
196
  attr_writer :partial_response
186
197
 
198
+ # initializes the request instance, sets the number of retries for the request.
187
199
  def initialize(*args)
188
200
  super
189
201
  @retries = @options.max_retries
@@ -2,29 +2,43 @@
2
2
 
3
3
  module HTTPX
4
4
  class StreamResponse
5
+ attr_reader :request
6
+
5
7
  def initialize(request, session)
6
8
  @request = request
9
+ @options = @request.options
7
10
  @session = session
8
- @response = nil
11
+ @response_enum = nil
12
+ @buffered_chunks = []
9
13
  end
10
14
 
11
15
  def each(&block)
12
16
  return enum_for(__method__) unless block
13
17
 
18
+ if (response_enum = @response_enum)
19
+ @response_enum = nil
20
+ # streaming already started, let's finish it
21
+
22
+ while (chunk = @buffered_chunks.shift)
23
+ block.call(chunk)
24
+ end
25
+
26
+ # consume enum til the end
27
+ begin
28
+ while (chunk = response_enum.next)
29
+ block.call(chunk)
30
+ end
31
+ rescue StopIteration
32
+ return
33
+ end
34
+ end
35
+
14
36
  @request.stream = self
15
37
 
16
38
  begin
17
39
  @on_chunk = block
18
40
 
19
- if @request.response
20
- # if we've already started collecting the payload, yield it first
21
- # before proceeding.
22
- body = @request.response.body
23
-
24
- body.each do |chunk|
25
- on_chunk(chunk)
26
- end
27
- end
41
+ response = @session.request(@request)
28
42
 
29
43
  response.raise_for_status
30
44
  ensure
@@ -59,38 +73,50 @@ module HTTPX
59
73
 
60
74
  # :nocov:
61
75
  def inspect
62
- "#<StreamResponse:#{object_id}>"
76
+ "#<#{self.class}:#{object_id}>"
63
77
  end
64
78
  # :nocov:
65
79
 
66
80
  def to_s
67
- response.to_s
81
+ if @request.response
82
+ @request.response.to_s
83
+ else
84
+ @buffered_chunks.join
85
+ end
68
86
  end
69
87
 
70
88
  private
71
89
 
72
90
  def response
73
- return @response if @response
74
-
75
91
  @request.response || begin
76
- @response = @session.request(@request)
92
+ response_enum = each
93
+ while (chunk = response_enum.next)
94
+ @buffered_chunks << chunk
95
+ break if @request.response
96
+ end
97
+ @response_enum = response_enum
98
+ @request.response
77
99
  end
78
100
  end
79
101
 
80
- def respond_to_missing?(meth, *args)
81
- response.respond_to?(meth, *args) || super
102
+ def respond_to_missing?(meth, include_private)
103
+ if (response = @request.response)
104
+ response.respond_to_missing?(meth, include_private)
105
+ else
106
+ @options.response_class.method_defined?(meth) || (include_private && @options.response_class.private_method_defined?(meth))
107
+ end || super
82
108
  end
83
109
 
84
- def method_missing(meth, *args, &block)
110
+ def method_missing(meth, *args, **kwargs, &block)
85
111
  return super unless response.respond_to?(meth)
86
112
 
87
- response.__send__(meth, *args, &block)
113
+ response.__send__(meth, *args, **kwargs, &block)
88
114
  end
89
115
  end
90
116
 
91
117
  module Plugins
92
118
  #
93
- # This plugin adds support for stream response (text/event-stream).
119
+ # This plugin adds support for streaming a response (useful for i.e. "text/event-stream" payloads).
94
120
  #
95
121
  # https://gitlab.com/os85/httpx/wikis/Stream
96
122
  #
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for bidirectional HTTP/2 streams.
7
+ #
8
+ # https://gitlab.com/os85/httpx/wikis/StreamBidi
9
+ #
10
+ # It is required that the request body allows chunk to be buffered, (i.e., responds to +#<<(chunk)+).
11
+ module StreamBidi
12
+ # Extension of the Connection::HTTP2 class, which adds functionality to
13
+ # deal with a request that can't be drained and must be interleaved with
14
+ # the response streams.
15
+ #
16
+ # The streams keeps send DATA frames while there's data; when they're ain't,
17
+ # the stream is kept open; it must be explicitly closed by the end user.
18
+ #
19
+ class HTTP2Bidi < Connection::HTTP2
20
+ def initialize(*)
21
+ super
22
+ @lock = Thread::Mutex.new
23
+ end
24
+
25
+ %i[close empty? exhausted? send <<].each do |lock_meth|
26
+ class_eval(<<-METH, __FILE__, __LINE__ + 1)
27
+ # lock.aware version of +#{lock_meth}+
28
+ def #{lock_meth}(*) # def close(*)
29
+ return super if @lock.owned?
30
+
31
+ # small race condition between
32
+ # checking for ownership and
33
+ # acquiring lock.
34
+ # TODO: fix this at the parser.
35
+ @lock.synchronize { super }
36
+ end
37
+ METH
38
+ end
39
+
40
+ private
41
+
42
+ %i[join_headers join_trailers join_body].each do |lock_meth|
43
+ class_eval(<<-METH, __FILE__, __LINE__ + 1)
44
+ # lock.aware version of +#{lock_meth}+
45
+ def #{lock_meth}(*) # def join_headers(*)
46
+ return super if @lock.owned?
47
+
48
+ # small race condition between
49
+ # checking for ownership and
50
+ # acquiring lock.
51
+ # TODO: fix this at the parser.
52
+ @lock.synchronize { super }
53
+ end
54
+ METH
55
+ end
56
+
57
+ def handle_stream(stream, request)
58
+ request.on(:body) do
59
+ next unless request.headers_sent
60
+
61
+ handle(request, stream)
62
+
63
+ emit(:flush_buffer)
64
+ end
65
+ super
66
+ end
67
+
68
+ # when there ain't more chunks, it makes the buffer as full.
69
+ def send_chunk(request, stream, chunk, next_chunk)
70
+ super
71
+
72
+ return if next_chunk
73
+
74
+ request.transition(:waiting_for_chunk)
75
+ throw(:buffer_full)
76
+ end
77
+
78
+ # sets end-stream flag when the request is closed.
79
+ def end_stream?(request, next_chunk)
80
+ request.closed? && next_chunk.nil?
81
+ end
82
+ end
83
+
84
+ # BidiBuffer is a Buffer which can be receive data from threads othr
85
+ # than the thread of the corresponding Connection/Session.
86
+ #
87
+ # It synchronizes access to a secondary internal +@oob_buffer+, which periodically
88
+ # is reconciled to the main internal +@buffer+.
89
+ class BidiBuffer < Buffer
90
+ def initialize(*)
91
+ super
92
+ @parent_thread = Thread.current
93
+ @oob_mutex = Thread::Mutex.new
94
+ @oob_buffer = "".b
95
+ end
96
+
97
+ # buffers the +chunk+ to be sent
98
+ def <<(chunk)
99
+ return super if Thread.current == @parent_thread
100
+
101
+ @oob_mutex.synchronize { @oob_buffer << chunk }
102
+ end
103
+
104
+ # reconciles the main and secondary buffer (which receives data from other threads).
105
+ def rebuffer
106
+ raise Error, "can only rebuffer while waiting on a response" unless Thread.current == @parent_thread
107
+
108
+ @oob_mutex.synchronize do
109
+ @buffer << @oob_buffer
110
+ @oob_buffer.clear
111
+ end
112
+ end
113
+ end
114
+
115
+ # Proxy to wake up the session main loop when one
116
+ # of the connections has buffered data to write. It abides by the HTTPX::_Selectable API,
117
+ # which allows it to be registered in the selector alongside actual HTTP-based
118
+ # HTTPX::Connection objects.
119
+ class Signal
120
+ def initialize
121
+ @closed = false
122
+ @pipe_read, @pipe_write = ::IO.pipe
123
+ end
124
+
125
+ def state
126
+ @closed ? :closed : :open
127
+ end
128
+
129
+ # noop
130
+ def log(**); end
131
+
132
+ def to_io
133
+ @pipe_read.to_io
134
+ end
135
+
136
+ def wakeup
137
+ return if @closed
138
+
139
+ @pipe_write.write("\0")
140
+ end
141
+
142
+ def call
143
+ return if @closed
144
+
145
+ @pipe_read.readpartial(1)
146
+ end
147
+
148
+ def interests
149
+ return if @closed
150
+
151
+ :r
152
+ end
153
+
154
+ def timeout; end
155
+
156
+ def terminate
157
+ @pipe_write.close
158
+ @pipe_read.close
159
+ @closed = true
160
+ end
161
+
162
+ # noop (the owner connection will take of it)
163
+ def handle_socket_timeout(interval); end
164
+ end
165
+
166
+ class << self
167
+ def load_dependencies(klass)
168
+ klass.plugin(:stream)
169
+ end
170
+
171
+ def extra_options(options)
172
+ options.merge(fallback_protocol: "h2")
173
+ end
174
+ end
175
+
176
+ module InstanceMethods
177
+ def initialize(*)
178
+ @signal = Signal.new
179
+ super
180
+ end
181
+
182
+ def close(selector = Selector.new)
183
+ @signal.terminate
184
+ selector.deregister(@signal)
185
+ super(selector)
186
+ end
187
+
188
+ def select_connection(connection, selector)
189
+ super
190
+ selector.register(@signal)
191
+ connection.signal = @signal
192
+ end
193
+
194
+ def deselect_connection(connection, *)
195
+ super
196
+ connection.signal = nil
197
+ end
198
+ end
199
+
200
+ # Adds synchronization to request operations which may buffer payloads from different
201
+ # threads.
202
+ module RequestMethods
203
+ attr_accessor :headers_sent
204
+
205
+ def initialize(*)
206
+ super
207
+ @headers_sent = false
208
+ @closed = false
209
+ @mutex = Thread::Mutex.new
210
+ end
211
+
212
+ def closed?
213
+ @closed
214
+ end
215
+
216
+ def can_buffer?
217
+ super && @state != :waiting_for_chunk
218
+ end
219
+
220
+ # overrides state management transitions to introduce an intermediate
221
+ # +:waiting_for_chunk+ state, which the request transitions to once payload
222
+ # is buffered.
223
+ def transition(nextstate)
224
+ headers_sent = @headers_sent
225
+
226
+ case nextstate
227
+ when :waiting_for_chunk
228
+ return unless @state == :body
229
+ when :body
230
+ case @state
231
+ when :headers
232
+ headers_sent = true
233
+ when :waiting_for_chunk
234
+ # HACK: to allow super to pass through
235
+ @state = :headers
236
+ end
237
+ end
238
+
239
+ super.tap do
240
+ # delay setting this up until after the first transition to :body
241
+ @headers_sent = headers_sent
242
+ end
243
+ end
244
+
245
+ def <<(chunk)
246
+ @mutex.synchronize do
247
+ if @drainer
248
+ @body.clear if @body.respond_to?(:clear)
249
+ @drainer = nil
250
+ end
251
+ @body << chunk
252
+
253
+ transition(:body)
254
+ end
255
+ end
256
+
257
+ def close
258
+ @mutex.synchronize do
259
+ return if @closed
260
+
261
+ @closed = true
262
+ end
263
+
264
+ # last chunk to send which ends the stream
265
+ self << ""
266
+ end
267
+ end
268
+
269
+ module RequestBodyMethods
270
+ def initialize(*, **)
271
+ super
272
+ @headers.delete("content-length")
273
+ end
274
+
275
+ def empty?
276
+ false
277
+ end
278
+ end
279
+
280
+ # overrides the declaration of +@write_buffer+, which is now a thread-safe buffer
281
+ # responding to the same API.
282
+ module ConnectionMethods
283
+ attr_writer :signal
284
+
285
+ def initialize(*)
286
+ super
287
+ @write_buffer = BidiBuffer.new(@options.buffer_size)
288
+ end
289
+
290
+ # rebuffers the +@write_buffer+ before calculating interests.
291
+ def interests
292
+ @write_buffer.rebuffer
293
+
294
+ super
295
+ end
296
+
297
+ private
298
+
299
+ def parser_type(protocol)
300
+ return HTTP2Bidi if protocol == "h2"
301
+
302
+ super
303
+ end
304
+
305
+ def set_parser_callbacks(parser)
306
+ super
307
+ parser.on(:flush_buffer) do
308
+ @signal.wakeup if @signal
309
+ end
310
+ end
311
+ end
312
+ end
313
+ register_plugin :stream_bidi, StreamBidi
314
+ end
315
+ end
data/lib/httpx/pool.rb CHANGED
@@ -13,25 +13,28 @@ module HTTPX
13
13
 
14
14
  # Sets up the connection pool with the given +options+, which can be the following:
15
15
  #
16
+ # :max_connections:: the maximum number of connections held in the pool.
16
17
  # :max_connections_per_origin :: the maximum number of connections held in the pool pointing to a given origin.
17
18
  # :pool_timeout :: the number of seconds to wait for a connection to a given origin (before raising HTTPX::PoolTimeoutError)
18
19
  #
19
20
  def initialize(options)
21
+ @max_connections = options.fetch(:max_connections, Float::INFINITY)
20
22
  @max_connections_per_origin = options.fetch(:max_connections_per_origin, Float::INFINITY)
21
23
  @pool_timeout = options.fetch(:pool_timeout, POOL_TIMEOUT)
22
24
  @resolvers = Hash.new { |hs, resolver_type| hs[resolver_type] = [] }
23
25
  @resolver_mtx = Thread::Mutex.new
24
26
  @connections = []
25
27
  @connection_mtx = Thread::Mutex.new
28
+ @connections_counter = 0
29
+ @max_connections_cond = ConditionVariable.new
26
30
  @origin_counters = Hash.new(0)
27
31
  @origin_conds = Hash.new { |hs, orig| hs[orig] = ConditionVariable.new }
28
32
  end
29
33
 
34
+ # connections returned by this function are not expected to return to the connection pool.
30
35
  def pop_connection
31
36
  @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
37
+ drop_connection
35
38
  end
36
39
  end
37
40
 
@@ -44,13 +47,34 @@ module HTTPX
44
47
 
45
48
  @connection_mtx.synchronize do
46
49
  acquire_connection(uri, options) || begin
50
+ if @connections_counter == @max_connections
51
+ # this takes precedence over per-origin
52
+ @max_connections_cond.wait(@connection_mtx, @pool_timeout)
53
+
54
+ acquire_connection(uri, options) || begin
55
+ if @connections_counter == @max_connections
56
+ # if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
57
+ # this means that all of them are persistent or being used, so raise a timeout error.
58
+ conn = @connections.find { |c| c.state == :closed }
59
+
60
+ raise PoolTimeoutError.new(@pool_timeout,
61
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
62
+
63
+ drop_connection(conn)
64
+ end
65
+ end
66
+ end
67
+
47
68
  if @origin_counters[uri.origin] == @max_connections_per_origin
48
69
 
49
70
  @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
50
71
 
51
- return acquire_connection(uri, options) || raise(PoolTimeoutError.new(uri.origin, @pool_timeout))
72
+ return acquire_connection(uri, options) ||
73
+ raise(PoolTimeoutError.new(@pool_timeout,
74
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
52
75
  end
53
76
 
77
+ @connections_counter += 1
54
78
  @origin_counters[uri.origin] += 1
55
79
 
56
80
  checkout_new_connection(uri, options)
@@ -64,6 +88,7 @@ module HTTPX
64
88
  @connection_mtx.synchronize do
65
89
  @connections << connection
66
90
 
91
+ @max_connections_cond.signal
67
92
  @origin_conds[connection.origin.to_s].signal
68
93
  end
69
94
  end
@@ -107,6 +132,15 @@ module HTTPX
107
132
  end
108
133
  end
109
134
 
135
+ # :nocov:
136
+ def inspect
137
+ "#<#{self.class}:#{object_id} " \
138
+ "@max_connections_per_origin=#{@max_connections_per_origin} " \
139
+ "@pool_timeout=#{@pool_timeout} " \
140
+ "@connections=#{@connections.size}>"
141
+ end
142
+ # :nocov:
143
+
110
144
  private
111
145
 
112
146
  def acquire_connection(uri, options)
@@ -114,7 +148,9 @@ module HTTPX
114
148
  connection.match?(uri, options)
115
149
  end
116
150
 
117
- @connections.delete_at(idx) if idx
151
+ return unless idx
152
+
153
+ @connections.delete_at(idx)
118
154
  end
119
155
 
120
156
  def checkout_new_connection(uri, options)
@@ -128,5 +164,22 @@ module HTTPX
128
164
  resolver_type.new(options)
129
165
  end
130
166
  end
167
+
168
+ # drops and returns the +connection+ from the connection pool; if +connection+ is <tt>nil</tt> (default),
169
+ # the first available connection from the pool will be dropped.
170
+ def drop_connection(connection = nil)
171
+ if connection
172
+ @connections.delete(connection)
173
+ else
174
+ connection = @connections.shift
175
+
176
+ return unless connection
177
+ end
178
+
179
+ @connections_counter -= 1
180
+ @origin_conds.delete(connection.origin) if (@origin_counters[connection.origin.to_s] -= 1).zero?
181
+
182
+ connection
183
+ end
131
184
  end
132
185
  end
@@ -116,7 +116,7 @@ module HTTPX
116
116
 
117
117
  # :nocov:
118
118
  def inspect
119
- "#<HTTPX::Request::Body:#{object_id} " \
119
+ "#<#{self.class}:#{object_id} " \
120
120
  "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
121
121
  end
122
122
  # :nocov: