httpx 1.6.3 → 1.7.1

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +2 -2
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/doc/release_notes/1_7_1.md +21 -0
  6. data/lib/httpx/adapters/datadog.rb +1 -1
  7. data/lib/httpx/adapters/faraday.rb +1 -1
  8. data/lib/httpx/adapters/webmock.rb +18 -9
  9. data/lib/httpx/altsvc.rb +4 -2
  10. data/lib/httpx/connection/http1.rb +9 -9
  11. data/lib/httpx/connection/http2.rb +2 -0
  12. data/lib/httpx/connection.rb +7 -9
  13. data/lib/httpx/domain_name.rb +1 -1
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/tcp.rb +1 -1
  16. data/lib/httpx/loggable.rb +2 -0
  17. data/lib/httpx/options.rb +118 -22
  18. data/lib/httpx/parser/http1.rb +1 -0
  19. data/lib/httpx/plugins/auth/digest.rb +44 -4
  20. data/lib/httpx/plugins/auth.rb +113 -4
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +156 -57
  30. data/lib/httpx/plugins/persistent.rb +3 -5
  31. data/lib/httpx/plugins/proxy/http.rb +0 -4
  32. data/lib/httpx/plugins/proxy.rb +3 -1
  33. data/lib/httpx/plugins/query.rb +1 -1
  34. data/lib/httpx/plugins/rate_limiter.rb +20 -15
  35. data/lib/httpx/plugins/response_cache.rb +3 -7
  36. data/lib/httpx/plugins/retries.rb +60 -24
  37. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  38. data/lib/httpx/plugins/stream.rb +60 -9
  39. data/lib/httpx/plugins/stream_bidi.rb +84 -16
  40. data/lib/httpx/pool.rb +12 -3
  41. data/lib/httpx/request/body.rb +1 -1
  42. data/lib/httpx/request.rb +10 -1
  43. data/lib/httpx/resolver/cache/base.rb +136 -0
  44. data/lib/httpx/resolver/cache/memory.rb +42 -0
  45. data/lib/httpx/resolver/cache.rb +18 -0
  46. data/lib/httpx/resolver/https.rb +74 -20
  47. data/lib/httpx/resolver/multi.rb +10 -2
  48. data/lib/httpx/resolver/native.rb +32 -6
  49. data/lib/httpx/resolver/resolver.rb +3 -3
  50. data/lib/httpx/resolver.rb +36 -114
  51. data/lib/httpx/response/body.rb +5 -3
  52. data/lib/httpx/response.rb +22 -6
  53. data/lib/httpx/selector.rb +14 -3
  54. data/lib/httpx/session.rb +6 -6
  55. data/lib/httpx/timers.rb +6 -12
  56. data/lib/httpx/transcoder/body.rb +1 -1
  57. data/lib/httpx/transcoder/gzip.rb +7 -2
  58. data/lib/httpx/transcoder/json.rb +1 -1
  59. data/lib/httpx/transcoder/multipart/decoder.rb +5 -5
  60. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  61. data/lib/httpx/transcoder/multipart.rb +17 -9
  62. data/lib/httpx/transcoder.rb +4 -6
  63. data/lib/httpx/utils.rb +13 -0
  64. data/lib/httpx/version.rb +1 -1
  65. data/sig/altsvc.rbs +9 -3
  66. data/sig/chainable.rbs +3 -3
  67. data/sig/connection.rbs +1 -3
  68. data/sig/loggable.rbs +1 -1
  69. data/sig/options.rbs +12 -4
  70. data/sig/plugins/auth/digest.rbs +6 -0
  71. data/sig/plugins/auth.rbs +37 -4
  72. data/sig/plugins/basic_auth.rbs +3 -3
  73. data/sig/plugins/digest_auth.rbs +2 -4
  74. data/sig/plugins/fiber_concurrency.rbs +6 -0
  75. data/sig/plugins/ntlm_auth.rbs +2 -2
  76. data/sig/plugins/oauth.rbs +44 -15
  77. data/sig/plugins/rate_limiter.rbs +4 -2
  78. data/sig/plugins/response_cache/file_store.rbs +2 -0
  79. data/sig/plugins/response_cache.rbs +4 -0
  80. data/sig/plugins/retries.rbs +12 -4
  81. data/sig/plugins/stream.rbs +13 -3
  82. data/sig/plugins/stream_bidi.rbs +2 -2
  83. data/sig/pool.rbs +1 -1
  84. data/sig/resolver/cache/base.rbs +28 -0
  85. data/sig/resolver/cache/memory.rbs +13 -0
  86. data/sig/resolver/cache.rbs +16 -0
  87. data/sig/resolver/https.rbs +24 -0
  88. data/sig/resolver/multi.rbs +8 -0
  89. data/sig/resolver/native.rbs +2 -0
  90. data/sig/resolver.rbs +5 -20
  91. data/sig/response.rbs +3 -0
  92. data/sig/session.rbs +3 -5
  93. data/sig/timers.rbs +1 -1
  94. data/sig/transcoder/multipart.rbs +4 -2
  95. data/sig/transcoder.rbs +5 -1
  96. data/sig/utils.rbs +2 -0
  97. metadata +11 -1
@@ -55,9 +55,9 @@ module HTTPX
55
55
  line << chunk
56
56
 
57
57
  while (idx = line.index("\n"))
58
- yield line.byteslice(0..idx - 1)
58
+ yield line.byteslice(0..(idx - 1))
59
59
 
60
- line = line.byteslice(idx + 1..-1)
60
+ line = line.byteslice((idx + 1)..-1)
61
61
  end
62
62
  end
63
63
 
@@ -121,20 +121,71 @@ module HTTPX
121
121
  # https://gitlab.com/os85/httpx/wikis/Stream
122
122
  #
123
123
  module Stream
124
+ STREAM_REQUEST_OPTIONS = { timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 }.freeze }.freeze
125
+
124
126
  def self.extra_options(options)
125
- options.merge(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
127
+ options.merge(
128
+ stream: false,
129
+ timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 },
130
+ stream_response_class: Class.new(StreamResponse, &Options::SET_TEMPORARY_NAME).freeze
131
+ )
132
+ end
133
+
134
+ # adds support for the following options:
135
+ #
136
+ # :stream :: whether the request to process should be handled as a stream (defaults to <tt>false</tt>).
137
+ # :stream_response_class :: Class used to build the stream response object.
138
+ module OptionsMethods
139
+ def option_stream(val)
140
+ val
141
+ end
142
+
143
+ def option_stream_response_class(value)
144
+ value
145
+ end
146
+
147
+ def extend_with_plugin_classes(pl)
148
+ return super unless defined?(pl::StreamResponseMethods)
149
+
150
+ @stream_response_class = @stream_response_class.dup
151
+ Options::SET_TEMPORARY_NAME[@stream_response_class, pl]
152
+ @stream_response_class.__send__(:include, pl::StreamResponseMethods) if defined?(pl::StreamResponseMethods)
153
+
154
+ super
155
+ end
126
156
  end
127
157
 
128
158
  module InstanceMethods
129
- def request(*args, stream: false, **options)
130
- return super(*args, **options) unless stream
159
+ def request(*args, **options)
160
+ if args.first.is_a?(Request)
161
+ requests = args
162
+
163
+ request = requests.first
164
+
165
+ unless request.options.stream && !request.stream
166
+ if options[:stream]
167
+ warn "passing `stream: true` with a request object is not supported anymore. " \
168
+ "You can instead build the request object with `stream :true`"
169
+ end
170
+ return super
171
+ end
172
+ else
173
+ return super unless options[:stream]
174
+
175
+ requests = build_requests(*args, options)
176
+
177
+ request = requests.first
178
+ end
131
179
 
132
- requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
133
180
  raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
134
181
 
135
- request = requests.first
182
+ @options.stream_response_class.new(request, self)
183
+ end
184
+
185
+ def build_request(verb, uri, params = EMPTY_HASH, options = @options)
186
+ return super unless params[:stream]
136
187
 
137
- StreamResponse.new(request, self)
188
+ super(verb, uri, params, options.merge(STREAM_REQUEST_OPTIONS.merge(stream: true)))
138
189
  end
139
190
  end
140
191
 
@@ -166,7 +217,7 @@ module HTTPX
166
217
 
167
218
  @stream.on_chunk(chunk.dup)
168
219
 
169
- chunk.size
220
+ chunk.bytesize
170
221
  end
171
222
 
172
223
  private
@@ -26,6 +26,8 @@ module HTTPX
26
26
  class_eval(<<-METH, __FILE__, __LINE__ + 1)
27
27
  # lock.aware version of +#{lock_meth}+
28
28
  def #{lock_meth}(*) # def close(*)
29
+ return super unless @options.stream
30
+
29
31
  return super if @lock.owned?
30
32
 
31
33
  # small race condition between
@@ -43,6 +45,8 @@ module HTTPX
43
45
  class_eval(<<-METH, __FILE__, __LINE__ + 1)
44
46
  # lock.aware version of +#{lock_meth}+
45
47
  private def #{lock_meth}(*) # private def join_headers(*)
48
+ return super unless @options.stream
49
+
46
50
  return super if @lock.owned?
47
51
 
48
52
  # small race condition between
@@ -55,6 +59,8 @@ module HTTPX
55
59
  end
56
60
 
57
61
  def handle_stream(stream, request)
62
+ return super unless @options.stream
63
+
58
64
  request.on(:body) do
59
65
  next unless request.headers_sent
60
66
 
@@ -67,6 +73,8 @@ module HTTPX
67
73
 
68
74
  # when there ain't more chunks, it makes the buffer as full.
69
75
  def send_chunk(request, stream, chunk, next_chunk)
76
+ return super unless @options.stream
77
+
70
78
  super
71
79
 
72
80
  return if next_chunk
@@ -77,39 +85,60 @@ module HTTPX
77
85
 
78
86
  # sets end-stream flag when the request is closed.
79
87
  def end_stream?(request, next_chunk)
88
+ return super unless @options.stream
89
+
80
90
  request.closed? && next_chunk.nil?
81
91
  end
82
92
  end
83
93
 
84
- # BidiBuffer is a Buffer which can be receive data from threads othr
85
- # than the thread of the corresponding Connection/Session.
94
+ # BidiBuffer is a thread-safe Buffer which can receive data from any thread.
86
95
  #
87
- # It synchronizes access to a secondary internal +@oob_buffer+, which periodically
88
- # is reconciled to the main internal +@buffer+.
96
+ # It uses a dual-buffer strategy with mutex protection:
97
+ # - +@buffer+ is the main buffer, protected by +@buffer_mutex+
98
+ # - +@oob_buffer+ receives data when +@buffer_mutex+ is contended
99
+ #
100
+ # This allows non-blocking writes from any thread while maintaining thread safety.
89
101
  class BidiBuffer < Buffer
90
102
  def initialize(*)
91
103
  super
92
- @parent_thread = Thread.current
104
+ @buffer_mutex = Thread::Mutex.new
93
105
  @oob_mutex = Thread::Mutex.new
94
106
  @oob_buffer = "".b
95
107
  end
96
108
 
97
- # buffers the +chunk+ to be sent
109
+ # buffers the +chunk+ to be sent (thread-safe, non-blocking)
98
110
  def <<(chunk)
99
- return super if Thread.current == @parent_thread
100
-
101
- @oob_mutex.synchronize { @oob_buffer << chunk }
111
+ if @buffer_mutex.try_lock
112
+ begin
113
+ super
114
+ ensure
115
+ @buffer_mutex.unlock
116
+ end
117
+ else
118
+ # another thread holds the lock, use OOB buffer to avoid blocking
119
+ @oob_mutex.synchronize { @oob_buffer << chunk }
120
+ end
102
121
  end
103
122
 
104
- # reconciles the main and secondary buffer (which receives data from other threads).
123
+ # reconciles the main and secondary buffer (thread-safe, callable from any thread).
105
124
  def rebuffer
106
- raise Error, "can only rebuffer while waiting on a response" unless Thread.current == @parent_thread
125
+ @buffer_mutex.synchronize do
126
+ @oob_mutex.synchronize do
127
+ return if @oob_buffer.empty?
107
128
 
108
- @oob_mutex.synchronize do
109
- @buffer << @oob_buffer
110
- @oob_buffer.clear
129
+ @buffer << @oob_buffer
130
+ @oob_buffer.clear
131
+ end
111
132
  end
112
133
  end
134
+
135
+ Buffer.instance_methods - Object.instance_methods - %i[<<].each do |meth|
136
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
137
+ def #{meth}(*) # def empty?
138
+ @buffer_mutex.synchronize { super }
139
+ end
140
+ MOD
141
+ end
113
142
  end
114
143
 
115
144
  # Proxy to wake up the session main loop when one
@@ -156,7 +185,13 @@ module HTTPX
156
185
 
157
186
  def timeout; end
158
187
 
188
+ def inflight?
189
+ !@closed
190
+ end
191
+
159
192
  def terminate
193
+ return if @closed
194
+
160
195
  @pipe_write.close
161
196
  @pipe_read.close
162
197
  @closed = true
@@ -190,16 +225,20 @@ module HTTPX
190
225
  def close(selector = Selector.new)
191
226
  @signal.terminate
192
227
  selector.deregister(@signal)
193
- super(selector)
228
+ super
194
229
  end
195
230
 
196
231
  def select_connection(connection, selector)
232
+ return super unless connection.options.stream
233
+
197
234
  super
198
235
  selector.register(@signal)
199
236
  connection.signal = @signal
200
237
  end
201
238
 
202
239
  def deselect_connection(connection, *)
240
+ return super unless connection.options.stream
241
+
203
242
  super
204
243
 
205
244
  connection.signal = nil
@@ -219,10 +258,14 @@ module HTTPX
219
258
  end
220
259
 
221
260
  def closed?
261
+ return super unless @options.stream
262
+
222
263
  @closed
223
264
  end
224
265
 
225
266
  def can_buffer?
267
+ return super unless @options.stream
268
+
226
269
  super && @state != :waiting_for_chunk
227
270
  end
228
271
 
@@ -230,9 +273,13 @@ module HTTPX
230
273
  # +:waiting_for_chunk+ state, which the request transitions to once payload
231
274
  # is buffered.
232
275
  def transition(nextstate)
276
+ return super unless @options.stream
277
+
233
278
  headers_sent = @headers_sent
234
279
 
235
280
  case nextstate
281
+ when :idle
282
+ headers_sent = false
236
283
  when :waiting_for_chunk
237
284
  return unless @state == :body
238
285
  when :body
@@ -264,6 +311,8 @@ module HTTPX
264
311
  end
265
312
 
266
313
  def close
314
+ return super unless @options.stream
315
+
267
316
  @mutex.synchronize do
268
317
  return if @closed
269
318
 
@@ -278,10 +327,22 @@ module HTTPX
278
327
  module RequestBodyMethods
279
328
  def initialize(*, **)
280
329
  super
330
+
331
+ return unless @options.stream
332
+
281
333
  @headers.delete("content-length")
334
+
335
+ return unless @body
336
+
337
+ return if @body.is_a?(Transcoder::Body::Encoder)
338
+
339
+ raise Error, "bidirectional streams only allow the usage of the `:body` param to set request bodies." \
340
+ "You must encode it yourself if you wish to do so."
282
341
  end
283
342
 
284
343
  def empty?
344
+ return super unless @options.stream
345
+
285
346
  false
286
347
  end
287
348
  end
@@ -293,18 +354,23 @@ module HTTPX
293
354
 
294
355
  def initialize(*)
295
356
  super
357
+
358
+ return unless @options.stream
359
+
296
360
  @write_buffer = BidiBuffer.new(@options.buffer_size)
297
361
  end
298
362
 
299
363
  # rebuffers the +@write_buffer+ before calculating interests.
300
364
  def interests
365
+ return super unless @options.stream
366
+
301
367
  @write_buffer.rebuffer
302
368
 
303
369
  super
304
370
  end
305
371
 
306
372
  def call
307
- return super unless (error = @signal.error)
373
+ return super unless @options.stream && (error = @signal.error)
308
374
 
309
375
  on_error(error)
310
376
  end
@@ -312,6 +378,8 @@ module HTTPX
312
378
  private
313
379
 
314
380
  def set_parser_callbacks(parser)
381
+ return super unless @options.stream
382
+
315
383
  super
316
384
  parser.on(:flush_buffer) do
317
385
  @signal.wakeup if @signal
data/lib/httpx/pool.rb CHANGED
@@ -122,6 +122,12 @@ module HTTPX
122
122
 
123
123
  @max_connections_cond.signal
124
124
  @origin_conds[connection.origin.to_s].signal
125
+
126
+ # Observed situations where a session handling multiple requests in a loop
127
+ # across multiple threads checks the same connection in and out, while another
128
+ # thread which is waiting on the same connection never gets the chance to pick
129
+ # it up, because ruby's thread scheduler never switched on to it in the process.
130
+ Thread.pass
125
131
  end
126
132
  end
127
133
 
@@ -142,7 +148,6 @@ module HTTPX
142
148
 
143
149
  def checkout_resolver(options)
144
150
  resolver_type = options.resolver_class
145
- resolver_type = Resolver.resolver_for(resolver_type, options)
146
151
 
147
152
  @resolver_mtx.synchronize do
148
153
  resolvers = @resolvers[resolver_type]
@@ -193,15 +198,19 @@ module HTTPX
193
198
  end
194
199
 
195
200
  def checkout_new_connection(uri, options)
196
- options.connection_class.new(uri, options)
201
+ connection = options.connection_class.new(uri, options)
202
+ connection.log(level: 2) { "created connection##{connection.object_id} in pool##{object_id}" }
203
+ connection
197
204
  end
198
205
 
199
206
  def checkout_new_resolver(resolver_type, options)
200
- if resolver_type.multi?
207
+ resolver = if resolver_type.multi?
201
208
  Resolver::Multi.new(resolver_type, options)
202
209
  else
203
210
  resolver_type.new(options)
204
211
  end
212
+ resolver.log(level: 2) { "created resolver##{resolver.object_id} in pool##{object_id}" }
213
+ resolver
205
214
  end
206
215
 
207
216
  # drops and returns the +connection+ from the connection pool; if +connection+ is <tt>nil</tt> (default),
@@ -128,7 +128,7 @@ module HTTPX
128
128
  Transcoder::Body.encode(body)
129
129
  elsif (form = params.delete(:form))
130
130
  if Transcoder::Multipart.multipart?(form)
131
- # @type var form: Transcoder::multipart_input
131
+ # @type var form: Transcoder::Multipart::multipart_input
132
132
  Transcoder::Multipart.encode(form)
133
133
  else
134
134
  # @type var form: Transcoder::urlencoded_input
data/lib/httpx/request.rb CHANGED
@@ -10,6 +10,7 @@ module HTTPX
10
10
  extend Forwardable
11
11
  include Loggable
12
12
  include Callbacks
13
+
13
14
  using URIExtensions
14
15
 
15
16
  ALLOWED_URI_SCHEMES = %w[https http].freeze
@@ -90,12 +91,20 @@ module HTTPX
90
91
  raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
91
92
 
92
93
  @state = :idle
93
- @response = @peer_address = @context = @informational_status = nil
94
+ @response = @peer_address = @informational_status = nil
94
95
  @ping = false
95
96
  @persistent = @options.persistent
96
97
  @active_timeouts = []
97
98
  end
98
99
 
100
+ # dupped initialization
101
+ def initialize_dup(orig)
102
+ super
103
+ @uri = orig.instance_variable_get(:@uri).dup
104
+ @headers = orig.instance_variable_get(:@headers).dup
105
+ @body = orig.instance_variable_get(:@body).dup
106
+ end
107
+
99
108
  def complete!(response = @response)
100
109
  emit(:complete, response)
101
110
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+
5
+ module HTTPX
6
+ module Resolver::Cache
7
+ # Base class of the Resolver Cache adapter implementations.
8
+ #
9
+ # While resolver caches are not required to inherit from this class, it nevertheless provides
10
+ # common useful functions for desired functionality, such as singleton object ractor-safe access,
11
+ # or a default #resolve implementation which deals with IPs and the system hosts file.
12
+ #
13
+ class Base
14
+ MAX_CACHE_SIZE = 512
15
+ CACHE_MUTEX = Thread::Mutex.new
16
+ HOSTS = Resolv::Hosts.new
17
+ @cache = nil
18
+
19
+ class << self
20
+ attr_reader :hosts_resolver
21
+
22
+ # returns the singleton instance to be used within the current ractor.
23
+ def cache(label)
24
+ return Ractor.store_if_absent(:"httpx_resolver_cache_#{label}") { new } if Utils.in_ractor?
25
+
26
+ @cache ||= CACHE_MUTEX.synchronize do
27
+ @cache || new
28
+ end
29
+ end
30
+ end
31
+
32
+ # resolves +hostname+ into an instance of HTTPX::Resolver::Entry if +hostname+ is an IP,
33
+ # or can be found in the cache, or can be found in the system hosts file.
34
+ def resolve(hostname)
35
+ ip_resolve(hostname) || get(hostname) || hosts_resolve(hostname)
36
+ end
37
+
38
+ private
39
+
40
+ # tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
41
+ def ip_resolve(hostname)
42
+ [Resolver::Entry.new(hostname)]
43
+ rescue ArgumentError
44
+ end
45
+
46
+ # matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
47
+ # found, or there is no hosts file.
48
+ def hosts_resolve(hostname)
49
+ ips = if Utils.in_ractor?
50
+ Ractor.store_if_absent(:httpx_hosts_resolver) { Resolv::Hosts.new }
51
+ else
52
+ HOSTS
53
+ end.getaddresses(hostname)
54
+
55
+ return if ips.empty?
56
+
57
+ ips.map { |ip| Resolver::Entry.new(ip) }
58
+ rescue IOError
59
+ end
60
+
61
+ # not to be used directly!
62
+ def _get(hostname, lookups, hostnames, ttl)
63
+ return unless lookups.key?(hostname)
64
+
65
+ entries = lookups[hostname]
66
+
67
+ return unless entries
68
+
69
+ entries.delete_if do |address|
70
+ address["TTL"] < ttl
71
+ end
72
+
73
+ if entries.empty?
74
+ lookups.delete(hostname)
75
+ hostnames.delete(hostname)
76
+ end
77
+
78
+ ips = entries.flat_map do |address|
79
+ if (als = address["alias"])
80
+ _get(als, lookups, hostnames, ttl)
81
+ else
82
+ Resolver::Entry.new(address["data"], address["TTL"])
83
+ end
84
+ end.compact
85
+
86
+ ips unless ips.empty?
87
+ end
88
+
89
+ def _set(hostname, family, entries, lookups, hostnames)
90
+ # lru cleanup
91
+ while lookups.size >= MAX_CACHE_SIZE
92
+ hs = hostnames.shift
93
+ lookups.delete(hs)
94
+ end
95
+ hostnames << hostname
96
+
97
+ lookups[hostname] ||= [] # when there's no default proc
98
+
99
+ case family
100
+ when Socket::AF_INET6
101
+ lookups[hostname].concat(entries)
102
+ when Socket::AF_INET
103
+ lookups[hostname].unshift(*entries)
104
+ end
105
+ entries.each do |entry|
106
+ name = entry["name"]
107
+ next unless name != hostname
108
+
109
+ lookups[name] ||= []
110
+
111
+ case family
112
+ when Socket::AF_INET6
113
+ lookups[name] << entry
114
+ when Socket::AF_INET
115
+ lookups[name].unshift(entry)
116
+ end
117
+ end
118
+ end
119
+
120
+ def _evict(hostname, ip, lookups, hostnames)
121
+ return unless lookups.key?(hostname)
122
+
123
+ entries = lookups[hostname]
124
+
125
+ return unless entries
126
+
127
+ entries.delete_if { |entry| entry["data"] == ip }
128
+
129
+ return unless entries.empty?
130
+
131
+ lookups.delete(hostname)
132
+ hostnames.delete(hostname)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Resolver::Cache
5
+ # Implementation of a thread-safe in-memory LRU resolver cache.
6
+ class Memory < Base
7
+ def initialize
8
+ super
9
+ @hostnames = []
10
+ @lookups = Hash.new { |h, k| h[k] = [] }
11
+ @lookup_mutex = Thread::Mutex.new
12
+ end
13
+
14
+ def get(hostname)
15
+ now = Utils.now
16
+ synchronize do |lookups, hostnames|
17
+ _get(hostname, lookups, hostnames, now)
18
+ end
19
+ end
20
+
21
+ def set(hostname, family, entries)
22
+ synchronize do |lookups, hostnames|
23
+ _set(hostname, family, entries, lookups, hostnames)
24
+ end
25
+ end
26
+
27
+ def evict(hostname, ip)
28
+ ip = ip.to_s
29
+
30
+ synchronize do |lookups, hostnames|
31
+ _evict(hostname, ip, lookups, hostnames)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def synchronize
38
+ @lookup_mutex.synchronize { yield(@lookups, @hostnames) }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx/resolver/cache/base"
4
+ require "httpx/resolver/cache/memory"
5
+
6
+ module HTTPX::Resolver
7
+ # The internal resolvers cache adapters are defined under this namespace.
8
+ #
9
+ # Adapters must comply with the Resolver Cache Adapter API and implement the following methods:
10
+ #
11
+ # * #resolve: (String hostname) -> Array[HTTPX::Entry]? => resolves hostname to a list of cached IPs (if found in cache or system)
12
+ # * #get: (String hostname) -> Array[HTTPX::Entry]? => resolves hostname to a list of cached IPs (if found in cache)
13
+ # * #set: (String hostname, Integer ip_family, Array[dns_result]) -> void => stores the set of results in the cache indexes for
14
+ # the hostname and the IP family
15
+ # * #evict: (String hostname, _ToS ip) -> void => evicts the ip for the hostname from the cache (usually done when no longer reachable)
16
+ module Cache
17
+ end
18
+ end