patient_http 1.0.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 +7 -0
- data/ARCHITECTURE.md +322 -0
- data/CHANGELOG.md +30 -0
- data/MIT-LICENSE +20 -0
- data/README.md +653 -0
- data/VERSION +1 -0
- data/db/migrate/20250101000000_create_patient_http_payloads.rb +15 -0
- data/lib/patient_http/callback_args.rb +176 -0
- data/lib/patient_http/callback_validator.rb +52 -0
- data/lib/patient_http/class_helper.rb +26 -0
- data/lib/patient_http/client.rb +80 -0
- data/lib/patient_http/client_pool.rb +178 -0
- data/lib/patient_http/configuration.rb +365 -0
- data/lib/patient_http/encryptor.rb +69 -0
- data/lib/patient_http/error.rb +76 -0
- data/lib/patient_http/external_storage.rb +134 -0
- data/lib/patient_http/http_error.rb +106 -0
- data/lib/patient_http/http_headers.rb +99 -0
- data/lib/patient_http/lifecycle_manager.rb +174 -0
- data/lib/patient_http/payload.rb +160 -0
- data/lib/patient_http/payload_store/active_record_store.rb +102 -0
- data/lib/patient_http/payload_store/base.rb +150 -0
- data/lib/patient_http/payload_store/file_store.rb +92 -0
- data/lib/patient_http/payload_store/redis_store.rb +98 -0
- data/lib/patient_http/payload_store/s3_store.rb +94 -0
- data/lib/patient_http/payload_store.rb +11 -0
- data/lib/patient_http/processor.rb +538 -0
- data/lib/patient_http/processor_observer.rb +48 -0
- data/lib/patient_http/rails/engine.rb +21 -0
- data/lib/patient_http/redirect_error.rb +136 -0
- data/lib/patient_http/redirect_helper.rb +90 -0
- data/lib/patient_http/request.rb +158 -0
- data/lib/patient_http/request_error.rb +150 -0
- data/lib/patient_http/request_helper.rb +230 -0
- data/lib/patient_http/request_task.rb +308 -0
- data/lib/patient_http/request_template.rb +114 -0
- data/lib/patient_http/response.rb +183 -0
- data/lib/patient_http/response_reader.rb +135 -0
- data/lib/patient_http/synchronous_executor.rb +241 -0
- data/lib/patient_http/task_handler.rb +55 -0
- data/lib/patient_http/time_helper.rb +32 -0
- data/lib/patient_http.rb +313 -0
- data/patient_http.gemspec +48 -0
- metadata +161 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
# Container for callback arguments that are passed to completion and error callbacks.
|
|
5
|
+
#
|
|
6
|
+
# CallbackArgs provides a structured way to access arguments passed from the original
|
|
7
|
+
# job to the callback workers. Arguments are stored with string keys internally
|
|
8
|
+
# (for JSON serialization compatibility) but can be accessed using either strings
|
|
9
|
+
# or symbols. All hash keys, including nested hashes and hashes within arrays, are
|
|
10
|
+
# deeply converted to strings.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# args = CallbackArgs.new(user_id: 123, action: "fetch")
|
|
14
|
+
# args[:user_id] # => 123
|
|
15
|
+
# args["user_id"] # => 123
|
|
16
|
+
# args.fetch(:missing, "default") # => "default"
|
|
17
|
+
# args.include?(:user_id) # => true
|
|
18
|
+
# args.to_h # => {user_id: 123, action: "fetch"}
|
|
19
|
+
#
|
|
20
|
+
# @example Nested hashes
|
|
21
|
+
# args = CallbackArgs.new(metadata: {tags: ["a", "b"], level: 1})
|
|
22
|
+
# args[:metadata] # => {"tags" => ["a", "b"], "level" => 1}
|
|
23
|
+
#
|
|
24
|
+
# @example From a response object
|
|
25
|
+
# response.callback_args[:user_id]
|
|
26
|
+
class CallbackArgs
|
|
27
|
+
# JSON-native types that are allowed as values
|
|
28
|
+
ALLOWED_TYPES = [NilClass, TrueClass, FalseClass, String, Integer, Float].freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Reconstruct a CallbackArgs from a hash (used during deserialization).
|
|
32
|
+
#
|
|
33
|
+
# @param hash [Hash, nil] hash with string keys
|
|
34
|
+
# @return [CallbackArgs] reconstructed CallbackArgs
|
|
35
|
+
def load(hash)
|
|
36
|
+
new(hash || {}, validate: false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Validate that a value is a JSON-native type (recursively for arrays and hashes).
|
|
40
|
+
#
|
|
41
|
+
# @param value [Object] the value to validate
|
|
42
|
+
# @param path [String] the path to the value (for error messages)
|
|
43
|
+
# @raise [ArgumentError] if the value is not a JSON-native type
|
|
44
|
+
# @return [void]
|
|
45
|
+
def validate_value!(value, path = "value")
|
|
46
|
+
case value
|
|
47
|
+
when *ALLOWED_TYPES
|
|
48
|
+
# Valid primitive type
|
|
49
|
+
when Array
|
|
50
|
+
value.each_with_index do |element, index|
|
|
51
|
+
validate_value!(element, "#{path}[#{index}]")
|
|
52
|
+
end
|
|
53
|
+
when Hash
|
|
54
|
+
value.each do |key, val|
|
|
55
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
56
|
+
raise ArgumentError.new("#{path} hash key must be a String or Symbol, got #{key.class.name}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
validate_value!(val, "#{path}[#{key.inspect}]")
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
raise ArgumentError.new("#{path} must be a JSON-native type (nil, true, false, String, Integer, Float, Array, or Hash), got #{value.class.name}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Deep convert all hash keys to strings, including nested hashes and hashes in arrays.
|
|
67
|
+
#
|
|
68
|
+
# @param value [Object] the value to convert
|
|
69
|
+
# @return [Object] the converted value with all hash keys as strings
|
|
70
|
+
def deep_stringify_keys(value)
|
|
71
|
+
case value
|
|
72
|
+
when Hash
|
|
73
|
+
value.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
|
74
|
+
when Array
|
|
75
|
+
value.map { |element| deep_stringify_keys(element) }
|
|
76
|
+
else
|
|
77
|
+
value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Initialize a CallbackArgs with a hash.
|
|
83
|
+
#
|
|
84
|
+
# @param args [Hash, nil] arguments to store (keys will be deeply converted to strings)
|
|
85
|
+
# @param validate [Boolean] whether to validate values are JSON-native types
|
|
86
|
+
# @raise [ArgumentError] if args is not nil and doesn't respond to to_h
|
|
87
|
+
# @raise [ArgumentError] if any value is not a JSON-native type (when validate is true)
|
|
88
|
+
def initialize(args = nil, validate: true)
|
|
89
|
+
if args.nil?
|
|
90
|
+
@data = {}
|
|
91
|
+
elsif args.respond_to?(:to_h)
|
|
92
|
+
hash = args.to_h
|
|
93
|
+
if validate
|
|
94
|
+
hash.each do |key, value|
|
|
95
|
+
self.class.validate_value!(value, key.to_s)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
@data = self.class.deep_stringify_keys(hash)
|
|
99
|
+
else
|
|
100
|
+
raise ArgumentError.new("callback_args must respond to to_h, got #{args.class.name}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Access an argument by key.
|
|
105
|
+
#
|
|
106
|
+
# @param key [String, Symbol] the key to access
|
|
107
|
+
# @return [Object] the value
|
|
108
|
+
# @raise [KeyError] if the key does not exist
|
|
109
|
+
def [](key)
|
|
110
|
+
string_key = key.to_s
|
|
111
|
+
unless @data.include?(string_key)
|
|
112
|
+
raise KeyError.new("key not found: #{key.inspect}. Available keys: #{@data.keys.join(", ")}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@data[string_key]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Access an argument by key with an optional default.
|
|
119
|
+
#
|
|
120
|
+
# @param key [String, Symbol] the key to access
|
|
121
|
+
# @param default [Object] the default value to return if key doesn't exist
|
|
122
|
+
# @return [Object] the value or default
|
|
123
|
+
def fetch(key, default = nil)
|
|
124
|
+
@data.fetch(key.to_s, default)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check if a key exists.
|
|
128
|
+
#
|
|
129
|
+
# @param key [String, Symbol] the key to check
|
|
130
|
+
# @return [Boolean] true if the key exists
|
|
131
|
+
def include?(key)
|
|
132
|
+
@data.include?(key.to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Convert to a hash with symbol keys (shallow).
|
|
136
|
+
#
|
|
137
|
+
# Only top-level keys are symbolized. Nested hash keys remain as strings.
|
|
138
|
+
#
|
|
139
|
+
# @return [Hash] hash with symbol keys
|
|
140
|
+
def to_h
|
|
141
|
+
@data.transform_keys(&:to_sym)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Convert to hash with string keys for serialization.
|
|
145
|
+
#
|
|
146
|
+
# @return [Hash] hash with string keys
|
|
147
|
+
def as_json
|
|
148
|
+
@data.dup
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
alias_method :dump, :as_json
|
|
152
|
+
|
|
153
|
+
# Check if there are no arguments.
|
|
154
|
+
#
|
|
155
|
+
# @return [Boolean] true if empty
|
|
156
|
+
def empty?
|
|
157
|
+
@data.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Return the number of arguments.
|
|
161
|
+
#
|
|
162
|
+
# @return [Integer] the count
|
|
163
|
+
def size
|
|
164
|
+
@data.size
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
alias_method :length, :size
|
|
168
|
+
|
|
169
|
+
# Return the keys.
|
|
170
|
+
#
|
|
171
|
+
# @return [Array<String>] the keys
|
|
172
|
+
def keys
|
|
173
|
+
@data.keys
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
module CallbackValidator
|
|
5
|
+
class << self
|
|
6
|
+
# Validate that the callback class defines the required methods.
|
|
7
|
+
#
|
|
8
|
+
# @param callback [Class, String] the callback class or its name
|
|
9
|
+
# @return [void]
|
|
10
|
+
# @raise [ArgumentError] if the callback class is invalid
|
|
11
|
+
def validate!(callback)
|
|
12
|
+
callback_class = callback.is_a?(Class) ? callback : ClassHelper.resolve_class_name(callback)
|
|
13
|
+
|
|
14
|
+
validate_callback_method!(callback_class, :on_complete)
|
|
15
|
+
validate_callback_method!(callback_class, :on_error)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Validate callback_args and convert to a hash with string keys.
|
|
19
|
+
#
|
|
20
|
+
# @param callback_args [#to_h, nil] the callback arguments
|
|
21
|
+
# @return [Hash, nil] validated hash with string keys, or nil
|
|
22
|
+
# @raise [ArgumentError] if callback_args is invalid
|
|
23
|
+
def validate_callback_args(callback_args)
|
|
24
|
+
return nil if callback_args.nil?
|
|
25
|
+
|
|
26
|
+
unless callback_args.respond_to?(:to_h)
|
|
27
|
+
raise ArgumentError.new("callback_args must respond to to_h, got #{callback_args.class.name}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
hash = callback_args.to_h
|
|
31
|
+
hash.each do |key, value|
|
|
32
|
+
CallbackArgs.validate_value!(value, key.to_s)
|
|
33
|
+
end
|
|
34
|
+
hash.transform_keys(&:to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate_callback_method!(callback_class, method_name)
|
|
40
|
+
unless callback_class.method_defined?(method_name)
|
|
41
|
+
raise ArgumentError.new("callback class must define ##{method_name} instance method")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
method = callback_class.instance_method(method_name)
|
|
45
|
+
# arity of 1 = exactly 1 required arg, -1 = any args (*args), -2 = 1 required + splat
|
|
46
|
+
unless method.arity == 1 || method.arity == -1 || method.arity == -2
|
|
47
|
+
raise ArgumentError.new("callback class ##{method_name} must accept exactly 1 positional argument")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
# Helper module for class-related operations.
|
|
5
|
+
#
|
|
6
|
+
# Provides utilities for resolving class names to class objects,
|
|
7
|
+
# which is useful for dynamic class loading.
|
|
8
|
+
module ClassHelper
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# Resolve a class from its name class name to the class object.
|
|
12
|
+
#
|
|
13
|
+
# @param class_name [String] the fully qualified class name
|
|
14
|
+
# @return [Class, nil] the class object or nil if no class_name given
|
|
15
|
+
# @raise [NameError] if class cannot be found
|
|
16
|
+
def resolve_class_name(class_name)
|
|
17
|
+
return class_name if class_name.is_a?(Class)
|
|
18
|
+
return nil if class_name.nil? || class_name.empty?
|
|
19
|
+
|
|
20
|
+
hierarchy = class_name.split("::")
|
|
21
|
+
hierarchy.shift if hierarchy.first.to_s.empty? # strip leading :: for absolute names
|
|
22
|
+
|
|
23
|
+
hierarchy.reduce(Object) { |mod, name| mod.const_get(name) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
class Client
|
|
5
|
+
def initialize(processor)
|
|
6
|
+
@processor = processor
|
|
7
|
+
@client_pool = ClientPool.new(
|
|
8
|
+
max_size: config.connection_pool_size,
|
|
9
|
+
connection_timeout: config.connection_timeout,
|
|
10
|
+
proxy_url: config.proxy_url,
|
|
11
|
+
retries: config.retries
|
|
12
|
+
)
|
|
13
|
+
@response_reader = ResponseReader.new(@processor)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Make an asynchronous HTTP request.
|
|
17
|
+
#
|
|
18
|
+
# @param request [Request] the request to make
|
|
19
|
+
# @param request_id [String] unique request identifier
|
|
20
|
+
# @return [Hash] the response data with keys for :status, :headers, and :body
|
|
21
|
+
def make_request(request, request_id)
|
|
22
|
+
async_response = nil
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
headers = request_headers(request, request_id)
|
|
26
|
+
body = Protocol::HTTP::Body::Buffered.wrap([request.body.to_s]) if request.body
|
|
27
|
+
timeout = request.timeout || config.request_timeout
|
|
28
|
+
|
|
29
|
+
Async::Task.current.with_timeout(timeout) do
|
|
30
|
+
async_response = @client_pool.request(request.http_method, request.url, headers, body)
|
|
31
|
+
headers_hash = async_response.headers.to_h.transform_values(&:to_s)
|
|
32
|
+
body = @response_reader.read_body(async_response, headers_hash)
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
status: async_response.status,
|
|
36
|
+
headers: headers_hash,
|
|
37
|
+
body: body
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
rescue => e
|
|
41
|
+
# Close the response and evict the client for this host to ensure the
|
|
42
|
+
# stale connection is not reused for subsequent requests.
|
|
43
|
+
async_response&.close
|
|
44
|
+
if connection_error?(e)
|
|
45
|
+
@client_pool.evict(request.url)
|
|
46
|
+
end
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Close all clients and release resources.
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
54
|
+
def close
|
|
55
|
+
@client_pool.close
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def config
|
|
61
|
+
@processor.config
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def connection_error?(exception)
|
|
65
|
+
case exception
|
|
66
|
+
when Async::TimeoutError, Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED,
|
|
67
|
+
Errno::EHOSTUNREACH, SocketError, IOError
|
|
68
|
+
true
|
|
69
|
+
else
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def request_headers(request, request_id)
|
|
75
|
+
headers = request.headers.to_h.merge("x-request-id" => request_id)
|
|
76
|
+
headers["user-agent"] ||= config.user_agent if config.user_agent
|
|
77
|
+
headers
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
# Pool of HTTP clients with LRU eviction.
|
|
5
|
+
#
|
|
6
|
+
# Maintains a pool of clients lazily instantiated for each host. The pool
|
|
7
|
+
# is capped with an LRU algorithm - when a new client is needed and the
|
|
8
|
+
# pool is at capacity, the least recently used client is closed and removed.
|
|
9
|
+
class ClientPool
|
|
10
|
+
def initialize(max_size:, connection_timeout: nil, proxy_url: nil, retries: 3)
|
|
11
|
+
@clients = {}
|
|
12
|
+
@max_size = max_size
|
|
13
|
+
@connection_timeout = connection_timeout
|
|
14
|
+
@proxy_url = proxy_url
|
|
15
|
+
@retries = retries
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@proxy_client = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :max_size, :connection_timeout, :proxy_url, :retries
|
|
21
|
+
|
|
22
|
+
# Get or create a client for the given endpoint.
|
|
23
|
+
#
|
|
24
|
+
# @param endpoint [Async::HTTP::Endpoint] the target endpoint
|
|
25
|
+
# @return [Protocol::HTTP::AcceptEncoding] wrapped client
|
|
26
|
+
def client_for(endpoint)
|
|
27
|
+
key = host_key(endpoint)
|
|
28
|
+
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
if @clients.key?(key)
|
|
31
|
+
# Move to end (most recently used) by re-inserting
|
|
32
|
+
client = @clients.delete(key)
|
|
33
|
+
@clients[key] = client
|
|
34
|
+
return client
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
evict_lru if @clients.size >= @max_size
|
|
38
|
+
@clients[key] = make_client(endpoint)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Make a request.
|
|
43
|
+
#
|
|
44
|
+
# @param http_method [String, Symbol] HTTP method
|
|
45
|
+
# @param url [String] request URL
|
|
46
|
+
# @param headers [Hash] request headers
|
|
47
|
+
# @param body [String, nil] request body
|
|
48
|
+
# @param block [Proc] optional block to process the response
|
|
49
|
+
# @return [Protocol::HTTP::Response] the response
|
|
50
|
+
def request(http_method, url, headers, body, &block)
|
|
51
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
52
|
+
client = client_for(endpoint)
|
|
53
|
+
|
|
54
|
+
verb = http_method.to_s.upcase
|
|
55
|
+
|
|
56
|
+
options = {
|
|
57
|
+
headers: headers,
|
|
58
|
+
body: body,
|
|
59
|
+
scheme: endpoint.scheme,
|
|
60
|
+
authority: endpoint.authority
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
request = ::Protocol::HTTP::Request[verb, endpoint.path, **options]
|
|
64
|
+
response = client.call(request)
|
|
65
|
+
|
|
66
|
+
return response unless block_given?
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
yield response
|
|
70
|
+
ensure
|
|
71
|
+
response.close
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Close all clients and release resources.
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
def close
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
@clients.each_value do |client|
|
|
81
|
+
client.close
|
|
82
|
+
rescue
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
@clients.clear
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
@proxy_client&.close
|
|
89
|
+
rescue
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
@proxy_client = nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Evict and close the client for the given URL.
|
|
97
|
+
#
|
|
98
|
+
# This forces a new connection to be established on the next request to this host.
|
|
99
|
+
#
|
|
100
|
+
# @param url [String] the request URL whose host client should be evicted
|
|
101
|
+
# @return [void]
|
|
102
|
+
def evict(url)
|
|
103
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
104
|
+
key = host_key(endpoint)
|
|
105
|
+
|
|
106
|
+
@mutex.synchronize do
|
|
107
|
+
client = @clients.delete(key)
|
|
108
|
+
begin
|
|
109
|
+
client&.close
|
|
110
|
+
rescue
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [Integer] number of clients in the pool
|
|
117
|
+
def size
|
|
118
|
+
@mutex.synchronize { @clients.size }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def evict_lru
|
|
124
|
+
lru_key, lru_client = @clients.first
|
|
125
|
+
return unless lru_key
|
|
126
|
+
|
|
127
|
+
@clients.delete(lru_key)
|
|
128
|
+
begin
|
|
129
|
+
lru_client.close
|
|
130
|
+
rescue
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def host_key(endpoint)
|
|
136
|
+
url = endpoint.url.dup
|
|
137
|
+
url.path = ""
|
|
138
|
+
url.fragment = nil
|
|
139
|
+
url.query = nil
|
|
140
|
+
url
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def make_client(endpoint)
|
|
144
|
+
client = @proxy_url ? make_proxied_client(endpoint) : make_direct_client(endpoint)
|
|
145
|
+
::Protocol::HTTP::AcceptEncoding.new(client)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def make_direct_client(endpoint)
|
|
149
|
+
configured_endpoint = configure_endpoint(endpoint)
|
|
150
|
+
Async::HTTP::Client.new(configured_endpoint, retries: @retries)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def make_proxied_client(endpoint)
|
|
154
|
+
require "async/http/proxy"
|
|
155
|
+
|
|
156
|
+
@proxy_client ||= create_proxy_client
|
|
157
|
+
configured_endpoint = configure_endpoint(endpoint)
|
|
158
|
+
|
|
159
|
+
proxy = @proxy_client.proxy(configured_endpoint)
|
|
160
|
+
Async::HTTP::Client.new(proxy.wrap_endpoint(configured_endpoint), retries: @retries)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def create_proxy_client
|
|
164
|
+
proxy_endpoint = Async::HTTP::Endpoint.parse(@proxy_url)
|
|
165
|
+
proxy_endpoint = configure_endpoint(proxy_endpoint) if @connection_timeout
|
|
166
|
+
Async::HTTP::Client.new(proxy_endpoint)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def configure_endpoint(endpoint)
|
|
170
|
+
return endpoint unless @connection_timeout
|
|
171
|
+
|
|
172
|
+
Async::HTTP::Endpoint.new(
|
|
173
|
+
endpoint.url,
|
|
174
|
+
timeout: @connection_timeout
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|