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.
- checksums.yaml +4 -4
- data/doc/release_notes/1_4_4.md +14 -0
- data/doc/release_notes/1_5_0.md +126 -0
- data/lib/httpx/adapters/datadog.rb +24 -3
- data/lib/httpx/adapters/webmock.rb +3 -0
- data/lib/httpx/buffer.rb +16 -5
- data/lib/httpx/connection/http1.rb +8 -9
- data/lib/httpx/connection/http2.rb +48 -24
- data/lib/httpx/connection.rb +40 -20
- data/lib/httpx/errors.rb +2 -11
- data/lib/httpx/headers.rb +24 -23
- data/lib/httpx/io/ssl.rb +8 -4
- data/lib/httpx/io/tcp.rb +9 -7
- data/lib/httpx/io/unix.rb +1 -1
- data/lib/httpx/loggable.rb +13 -1
- data/lib/httpx/options.rb +63 -48
- data/lib/httpx/parser/http1.rb +1 -1
- data/lib/httpx/plugins/aws_sigv4.rb +1 -0
- data/lib/httpx/plugins/callbacks.rb +19 -6
- data/lib/httpx/plugins/circuit_breaker.rb +4 -3
- data/lib/httpx/plugins/cookies/jar.rb +0 -2
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +7 -4
- data/lib/httpx/plugins/cookies.rb +4 -4
- data/lib/httpx/plugins/follow_redirects.rb +4 -2
- data/lib/httpx/plugins/grpc/call.rb +1 -1
- data/lib/httpx/plugins/h2c.rb +7 -1
- data/lib/httpx/plugins/persistent.rb +22 -1
- data/lib/httpx/plugins/proxy/http.rb +3 -1
- data/lib/httpx/plugins/query.rb +35 -0
- data/lib/httpx/plugins/response_cache/file_store.rb +115 -15
- data/lib/httpx/plugins/response_cache/store.rb +7 -67
- data/lib/httpx/plugins/response_cache.rb +179 -29
- data/lib/httpx/plugins/retries.rb +27 -15
- data/lib/httpx/plugins/stream.rb +46 -20
- data/lib/httpx/plugins/stream_bidi.rb +315 -0
- data/lib/httpx/pool.rb +58 -5
- data/lib/httpx/request/body.rb +1 -1
- data/lib/httpx/request.rb +21 -5
- data/lib/httpx/resolver/https.rb +10 -4
- data/lib/httpx/resolver/native.rb +13 -13
- data/lib/httpx/resolver/resolver.rb +4 -0
- data/lib/httpx/resolver/system.rb +37 -14
- data/lib/httpx/resolver.rb +2 -2
- data/lib/httpx/response/body.rb +10 -21
- data/lib/httpx/response/buffer.rb +36 -12
- data/lib/httpx/response.rb +11 -1
- data/lib/httpx/selector.rb +16 -12
- data/lib/httpx/session.rb +80 -23
- data/lib/httpx/timers.rb +24 -16
- data/lib/httpx/transcoder/multipart/decoder.rb +4 -2
- data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +1 -1
- data/sig/chainable.rbs +5 -2
- data/sig/connection/http2.rbs +11 -2
- data/sig/connection.rbs +4 -4
- data/sig/errors.rbs +0 -3
- data/sig/headers.rbs +15 -10
- data/sig/httpx.rbs +5 -1
- data/sig/io/tcp.rbs +6 -0
- data/sig/loggable.rbs +2 -0
- data/sig/options.rbs +7 -1
- data/sig/plugins/cookies/cookie.rbs +1 -3
- data/sig/plugins/cookies/jar.rbs +4 -4
- data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/h2c.rbs +4 -0
- data/sig/plugins/proxy/http.rbs +3 -0
- data/sig/plugins/proxy.rbs +4 -0
- data/sig/plugins/response_cache/file_store.rbs +19 -0
- data/sig/plugins/response_cache/store.rbs +13 -0
- data/sig/plugins/response_cache.rbs +41 -19
- data/sig/plugins/retries.rbs +4 -3
- data/sig/plugins/stream.rbs +8 -1
- data/sig/plugins/stream_bidi.rbs +68 -0
- data/sig/plugins/upgrade/h2.rbs +9 -0
- data/sig/plugins/upgrade.rbs +5 -0
- data/sig/pool.rbs +5 -0
- data/sig/punycode.rbs +5 -0
- data/sig/request.rbs +7 -0
- data/sig/resolver/https.rbs +3 -2
- data/sig/resolver/native.rbs +1 -2
- data/sig/resolver/resolver.rbs +11 -3
- data/sig/resolver/system.rbs +19 -2
- data/sig/resolver.rbs +11 -7
- data/sig/response/body.rbs +3 -4
- data/sig/response/buffer.rbs +2 -3
- data/sig/response.rbs +2 -2
- data/sig/selector.rbs +20 -10
- data/sig/session.rbs +14 -6
- data/sig/timers.rbs +5 -7
- data/sig/transcoder/multipart.rbs +4 -3
- metadata +14 -5
- data/lib/httpx/session2.rb +0 -23
- data/lib/httpx/transcoder/utils/inflater.rb +0 -21
- 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
|
-
|
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
|
-
|
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
|
-
|
108
|
+
repeatable_request?(request, options) &&
|
103
109
|
(
|
104
110
|
(
|
105
|
-
response.is_a?(ErrorResponse) &&
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
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
|
data/lib/httpx/plugins/stream.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
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
|
-
"
|
76
|
+
"#<#{self.class}:#{object_id}>"
|
63
77
|
end
|
64
78
|
# :nocov:
|
65
79
|
|
66
80
|
def to_s
|
67
|
-
response
|
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
|
-
|
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,
|
81
|
-
response.
|
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
|
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
|
-
|
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) ||
|
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
|
-
|
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
|
data/lib/httpx/request/body.rb
CHANGED