patient_http 1.6.1 → 1.8.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/ARCHITECTURE.md +18 -8
- data/CHANGELOG.md +43 -0
- data/README.md +564 -508
- data/VERSION +1 -1
- data/lib/patient_http/callback_args.rb +53 -48
- data/lib/patient_http/callback_validator.rb +11 -7
- data/lib/patient_http/class_helper.rb +6 -7
- data/lib/patient_http/client.rb +53 -26
- data/lib/patient_http/client_pool.rb +134 -39
- data/lib/patient_http/completion_executor.rb +20 -20
- data/lib/patient_http/configuration.rb +405 -119
- data/lib/patient_http/connection_endpoint.rb +150 -0
- data/lib/patient_http/encryptor.rb +28 -18
- data/lib/patient_http/error.rb +24 -18
- data/lib/patient_http/external_storage.rb +42 -38
- data/lib/patient_http/http_error.rb +30 -26
- data/lib/patient_http/http_headers.rb +35 -29
- data/lib/patient_http/immediate_retries.rb +98 -0
- data/lib/patient_http/inline_task_handler.rb +15 -10
- data/lib/patient_http/lifecycle_manager.rb +39 -40
- data/lib/patient_http/outgoing_request.rb +25 -23
- data/lib/patient_http/payload.rb +28 -26
- data/lib/patient_http/payload_store/active_record_store.rb +31 -34
- data/lib/patient_http/payload_store/base.rb +42 -46
- data/lib/patient_http/payload_store/file_store.rb +22 -26
- data/lib/patient_http/payload_store/redis_store.rb +28 -34
- data/lib/patient_http/payload_store/s3_store.rb +25 -28
- data/lib/patient_http/payload_store.rb +2 -0
- data/lib/patient_http/processor.rb +111 -79
- data/lib/patient_http/processor_observer.rb +65 -59
- data/lib/patient_http/rails/engine.rb +13 -8
- data/lib/patient_http/redirect_error.rb +50 -41
- data/lib/patient_http/redirect_helper.rb +38 -38
- data/lib/patient_http/request.rb +117 -46
- data/lib/patient_http/request_error.rb +47 -42
- data/lib/patient_http/request_helper.rb +178 -119
- data/lib/patient_http/request_preparer.rb +13 -10
- data/lib/patient_http/request_task.rb +115 -84
- data/lib/patient_http/request_template.rb +119 -64
- data/lib/patient_http/response.rb +58 -52
- data/lib/patient_http/response_reader.rb +166 -118
- data/lib/patient_http/secret_manager.rb +34 -30
- data/lib/patient_http/secret_reference.rb +33 -26
- data/lib/patient_http/synchronous_executor.rb +84 -95
- data/lib/patient_http/task_handler.rb +23 -19
- data/lib/patient_http/time_helper.rb +8 -8
- data/lib/patient_http.rb +327 -186
- data/patient_http.gemspec +3 -2
- metadata +21 -5
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.8.0
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# The callback arguments that a request passes to its `on_complete` and
|
|
5
|
+
# `on_error` callbacks.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# or symbols. All hash keys, including nested hashes and hashes within arrays, are
|
|
10
|
-
# deeply converted to strings.
|
|
7
|
+
# The arguments are stored with string keys, so they can be serialized to
|
|
8
|
+
# JSON. You can read them with string or symbol keys. The keys of nested
|
|
9
|
+
# hashes, including hashes in arrays, are converted to strings.
|
|
11
10
|
#
|
|
12
|
-
# @example
|
|
11
|
+
# @example Read arguments
|
|
13
12
|
# args = CallbackArgs.new(user_id: 123, action: "fetch")
|
|
14
13
|
# args[:user_id] # => 123
|
|
15
14
|
# args["user_id"] # => 123
|
|
@@ -17,30 +16,34 @@ module PatientHttp
|
|
|
17
16
|
# args.include?(:user_id) # => true
|
|
18
17
|
# args.to_h # => {user_id: 123, action: "fetch"}
|
|
19
18
|
#
|
|
20
|
-
# @example
|
|
19
|
+
# @example Read a nested hash
|
|
21
20
|
# args = CallbackArgs.new(metadata: {tags: ["a", "b"], level: 1})
|
|
22
21
|
# args[:metadata] # => {"tags" => ["a", "b"], "level" => 1}
|
|
23
22
|
#
|
|
24
|
-
# @example
|
|
23
|
+
# @example Read arguments from a response
|
|
25
24
|
# response.callback_args[:user_id]
|
|
26
25
|
class CallbackArgs
|
|
27
|
-
# JSON-native types that are allowed as values
|
|
26
|
+
# The JSON-native types that are allowed as values, in addition to `Array`
|
|
27
|
+
# and `Hash`.
|
|
28
28
|
ALLOWED_TYPES = [NilClass, TrueClass, FalseClass, String, Integer, Float].freeze
|
|
29
29
|
|
|
30
30
|
class << self
|
|
31
|
-
#
|
|
31
|
+
# Creates callback arguments from their serialized form. The values aren't
|
|
32
|
+
# validated.
|
|
32
33
|
#
|
|
33
|
-
# @param hash [Hash, nil] hash
|
|
34
|
-
# @return [CallbackArgs]
|
|
34
|
+
# @param hash [Hash, nil] The hash from {#as_json}.
|
|
35
|
+
# @return [CallbackArgs] The callback arguments.
|
|
35
36
|
def load(hash)
|
|
36
37
|
new(hash || {}, validate: false)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
#
|
|
40
|
+
# Validates that a value is a JSON-native type. Arrays and hashes are
|
|
41
|
+
# validated recursively.
|
|
40
42
|
#
|
|
41
|
-
# @param value [Object]
|
|
42
|
-
# @param path [String]
|
|
43
|
-
# @raise [ArgumentError]
|
|
43
|
+
# @param value [Object] The value to validate.
|
|
44
|
+
# @param path [String] The path to the value, for the error message.
|
|
45
|
+
# @raise [ArgumentError] If the value isn't a JSON-native type, or if a hash
|
|
46
|
+
# key isn't a String or Symbol.
|
|
44
47
|
# @return [void]
|
|
45
48
|
def validate_value!(value, path = "value")
|
|
46
49
|
case value
|
|
@@ -63,10 +66,11 @@ module PatientHttp
|
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
#
|
|
69
|
+
# Converts all hash keys to strings, including the keys of nested hashes
|
|
70
|
+
# and of hashes in arrays.
|
|
67
71
|
#
|
|
68
|
-
# @param value [Object]
|
|
69
|
-
# @return [Object]
|
|
72
|
+
# @param value [Object] The value to convert.
|
|
73
|
+
# @return [Object] The value with string keys.
|
|
70
74
|
def deep_stringify_keys(value)
|
|
71
75
|
case value
|
|
72
76
|
when Hash
|
|
@@ -79,12 +83,14 @@ module PatientHttp
|
|
|
79
83
|
end
|
|
80
84
|
end
|
|
81
85
|
|
|
82
|
-
#
|
|
86
|
+
# Creates callback arguments.
|
|
83
87
|
#
|
|
84
|
-
# @param args [Hash, nil] arguments
|
|
85
|
-
# @param validate [Boolean]
|
|
86
|
-
#
|
|
87
|
-
# @raise [ArgumentError]
|
|
88
|
+
# @param args [Hash, nil] The arguments. All keys are converted to strings.
|
|
89
|
+
# @param validate [Boolean] Whether to validate that the values are JSON-native
|
|
90
|
+
# types.
|
|
91
|
+
# @raise [ArgumentError] If `args` isn't `nil` and doesn't respond to `to_h`.
|
|
92
|
+
# @raise [ArgumentError] If `validate` is `true` and a value isn't a
|
|
93
|
+
# JSON-native type.
|
|
88
94
|
def initialize(args = nil, validate: true)
|
|
89
95
|
if args.nil?
|
|
90
96
|
@data = {}
|
|
@@ -101,11 +107,11 @@ module PatientHttp
|
|
|
101
107
|
end
|
|
102
108
|
end
|
|
103
109
|
|
|
104
|
-
#
|
|
110
|
+
# Returns the value for a key.
|
|
105
111
|
#
|
|
106
|
-
# @param key [String, Symbol]
|
|
107
|
-
# @return [Object]
|
|
108
|
-
# @raise [KeyError]
|
|
112
|
+
# @param key [String, Symbol] The key.
|
|
113
|
+
# @return [Object] The value.
|
|
114
|
+
# @raise [KeyError] If the key doesn't exist.
|
|
109
115
|
def [](key)
|
|
110
116
|
string_key = key.to_s
|
|
111
117
|
unless @data.include?(string_key)
|
|
@@ -115,60 +121,59 @@ module PatientHttp
|
|
|
115
121
|
@data[string_key]
|
|
116
122
|
end
|
|
117
123
|
|
|
118
|
-
#
|
|
124
|
+
# Returns the value for a key, or a default value if the key doesn't exist.
|
|
119
125
|
#
|
|
120
|
-
# @param key [String, Symbol]
|
|
121
|
-
# @param default [Object]
|
|
122
|
-
# @return [Object]
|
|
126
|
+
# @param key [String, Symbol] The key.
|
|
127
|
+
# @param default [Object] The value to return if the key doesn't exist.
|
|
128
|
+
# @return [Object] The value, or the default value.
|
|
123
129
|
def fetch(key, default = nil)
|
|
124
130
|
@data.fetch(key.to_s, default)
|
|
125
131
|
end
|
|
126
132
|
|
|
127
|
-
#
|
|
133
|
+
# Returns whether a key exists.
|
|
128
134
|
#
|
|
129
|
-
# @param key [String, Symbol]
|
|
130
|
-
# @return [Boolean] true if the key exists
|
|
135
|
+
# @param key [String, Symbol] The key.
|
|
136
|
+
# @return [Boolean] `true` if the key exists.
|
|
131
137
|
def include?(key)
|
|
132
138
|
@data.include?(key.to_s)
|
|
133
139
|
end
|
|
134
140
|
|
|
135
|
-
#
|
|
141
|
+
# Returns the arguments as a hash with symbol keys. Only the top-level keys
|
|
142
|
+
# are symbols. The keys of nested hashes stay strings.
|
|
136
143
|
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
# @return [Hash] hash with symbol keys
|
|
144
|
+
# @return [Hash] The arguments.
|
|
140
145
|
def to_h
|
|
141
146
|
@data.transform_keys(&:to_sym)
|
|
142
147
|
end
|
|
143
148
|
|
|
144
|
-
#
|
|
149
|
+
# Returns the arguments as a JSON-compatible hash with string keys.
|
|
145
150
|
#
|
|
146
|
-
# @return [Hash]
|
|
151
|
+
# @return [Hash] The serialized arguments.
|
|
147
152
|
def as_json
|
|
148
153
|
@data.dup
|
|
149
154
|
end
|
|
150
155
|
|
|
151
156
|
alias_method :dump, :as_json
|
|
152
157
|
|
|
153
|
-
#
|
|
158
|
+
# Returns whether there are no arguments.
|
|
154
159
|
#
|
|
155
|
-
# @return [Boolean] true if
|
|
160
|
+
# @return [Boolean] `true` if there are no arguments.
|
|
156
161
|
def empty?
|
|
157
162
|
@data.empty?
|
|
158
163
|
end
|
|
159
164
|
|
|
160
|
-
#
|
|
165
|
+
# Returns the number of arguments.
|
|
161
166
|
#
|
|
162
|
-
# @return [Integer]
|
|
167
|
+
# @return [Integer] The number of top-level keys.
|
|
163
168
|
def size
|
|
164
169
|
@data.size
|
|
165
170
|
end
|
|
166
171
|
|
|
167
172
|
alias_method :length, :size
|
|
168
173
|
|
|
169
|
-
#
|
|
174
|
+
# Returns the top-level keys.
|
|
170
175
|
#
|
|
171
|
-
# @return [Array<String>]
|
|
176
|
+
# @return [Array<String>] The keys.
|
|
172
177
|
def keys
|
|
173
178
|
@data.keys
|
|
174
179
|
end
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
+
# Validates callback service classes and callback arguments.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
4
7
|
module CallbackValidator
|
|
5
8
|
class << self
|
|
6
|
-
#
|
|
9
|
+
# Validates that the callback class defines the required methods.
|
|
7
10
|
#
|
|
8
|
-
# @param callback [Class, String]
|
|
11
|
+
# @param callback [Class, String] The callback service class, or its name.
|
|
9
12
|
# @return [void]
|
|
10
|
-
# @raise [ArgumentError]
|
|
13
|
+
# @raise [ArgumentError] If the class doesn't define `on_complete` and
|
|
14
|
+
# `on_error` instance methods that take one argument.
|
|
11
15
|
def validate!(callback)
|
|
12
16
|
callback_class = callback.is_a?(Class) ? callback : ClassHelper.resolve_class_name(callback)
|
|
13
17
|
|
|
@@ -15,11 +19,11 @@ module PatientHttp
|
|
|
15
19
|
validate_callback_method!(callback_class, :on_error)
|
|
16
20
|
end
|
|
17
21
|
|
|
18
|
-
#
|
|
22
|
+
# Validates the callback arguments and converts them to a hash with string keys.
|
|
19
23
|
#
|
|
20
|
-
# @param callback_args [#to_h, nil]
|
|
21
|
-
# @return [Hash, nil]
|
|
22
|
-
# @raise [ArgumentError]
|
|
24
|
+
# @param callback_args [#to_h, nil] The callback arguments.
|
|
25
|
+
# @return [Hash, nil] Validated hash with string keys, or nil.
|
|
26
|
+
# @raise [ArgumentError] If callback_args is invalid.
|
|
23
27
|
def validate_callback_args(callback_args)
|
|
24
28
|
return nil if callback_args.nil?
|
|
25
29
|
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# Resolves class names to classes.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# which is useful for dynamic class loading.
|
|
6
|
+
# @api private
|
|
8
7
|
module ClassHelper
|
|
9
8
|
extend self
|
|
10
9
|
|
|
11
|
-
#
|
|
10
|
+
# Returns the class for a class name.
|
|
12
11
|
#
|
|
13
|
-
# @param class_name [String]
|
|
14
|
-
# @return [Class, nil]
|
|
15
|
-
# @raise [NameError]
|
|
12
|
+
# @param class_name [String] The fully qualified class name.
|
|
13
|
+
# @return [Class, nil] The class object or nil if no class_name given.
|
|
14
|
+
# @raise [NameError] If class cannot be found.
|
|
16
15
|
def resolve_class_name(class_name)
|
|
17
16
|
return class_name if class_name.is_a?(Class)
|
|
18
17
|
return nil if class_name.nil? || class_name.empty?
|
data/lib/patient_http/client.rb
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
+
# Sends the requests of a {Processor} through a pool of HTTP clients.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
4
7
|
class Client
|
|
8
|
+
include ImmediateRetries
|
|
9
|
+
|
|
10
|
+
# Creates a client for a processor.
|
|
11
|
+
#
|
|
12
|
+
# @param processor [Processor] The processor that owns the client.
|
|
5
13
|
def initialize(processor)
|
|
6
14
|
@processor = processor
|
|
7
|
-
@client_pool = ClientPool.
|
|
8
|
-
max_size: config.connection_pool_size,
|
|
9
|
-
connection_timeout: config.connection_timeout,
|
|
10
|
-
proxy_url: config.proxy_url,
|
|
11
|
-
retries: config.retries,
|
|
12
|
-
protocol: config.protocol,
|
|
13
|
-
connection_limit: config.max_connections_per_host
|
|
14
|
-
)
|
|
15
|
+
@client_pool = ClientPool.from_config(config)
|
|
15
16
|
@response_reader = ResponseReader.new(@processor)
|
|
16
17
|
@request_preparer = RequestPreparer.new(config)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
#
|
|
20
|
+
# Makes an asynchronous HTTP request.
|
|
20
21
|
#
|
|
21
22
|
# The returned body is the array of raw (possibly compressed) body chunks;
|
|
22
23
|
# use {#decode_response} to produce the final body string. Splitting the
|
|
23
24
|
# decode out keeps CPU-bound work off the reactor thread.
|
|
24
25
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# The request timeout limits the full exchange. The read timeout, if set,
|
|
27
|
+
# limits the wait for the response headers and the wait for each body
|
|
28
|
+
# chunk.
|
|
29
|
+
#
|
|
30
|
+
# @param request [Request] The request to make.
|
|
31
|
+
# @param request_id [String] Unique request identifier.
|
|
32
|
+
# @return [Hash] The response data with keys for :status, :headers, :body,
|
|
33
|
+
# and :max_response_size. The size limit goes with the data so that
|
|
34
|
+
# {#decode_response} applies the same limit to the inflated body.
|
|
28
35
|
def make_request(request, request_id)
|
|
29
36
|
async_response = nil
|
|
37
|
+
client = nil
|
|
30
38
|
|
|
31
39
|
begin
|
|
32
40
|
outgoing = @request_preparer.prepare(request, request_id)
|
|
@@ -34,32 +42,46 @@ module PatientHttp
|
|
|
34
42
|
headers = outgoing.headers.to_h
|
|
35
43
|
body = Protocol::HTTP::Body::Buffered.wrap([request.body.to_s]) if request.body
|
|
36
44
|
timeout = request.timeout || config.request_timeout
|
|
45
|
+
read_timeout = request.read_timeout || config.read_timeout
|
|
46
|
+
max_response_size = request.max_response_size || config.max_response_size
|
|
37
47
|
|
|
38
48
|
Async::Task.current.with_timeout(timeout) do
|
|
39
|
-
|
|
49
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
50
|
+
async_response = ResponseReader.with_read_timeout(read_timeout) do
|
|
51
|
+
request_with_immediate_retries(
|
|
52
|
+
@client_pool, request, endpoint, headers, body
|
|
53
|
+
) { |pooled_client| client = pooled_client }
|
|
54
|
+
end
|
|
40
55
|
# Note: headers that appear multiple times (e.g. set-cookie) are
|
|
41
56
|
# flattened to a single joined string value.
|
|
42
57
|
headers_hash = async_response.headers.to_h.transform_values(&:to_s)
|
|
43
|
-
body = @response_reader.read_raw_body(
|
|
58
|
+
body = @response_reader.read_raw_body(
|
|
59
|
+
async_response,
|
|
60
|
+
headers_hash,
|
|
61
|
+
max_response_size: max_response_size,
|
|
62
|
+
read_timeout: read_timeout
|
|
63
|
+
)
|
|
44
64
|
|
|
45
65
|
{
|
|
46
66
|
status: async_response.status,
|
|
47
67
|
headers: headers_hash,
|
|
48
|
-
body: body
|
|
68
|
+
body: body,
|
|
69
|
+
max_response_size: max_response_size
|
|
49
70
|
}
|
|
50
71
|
end
|
|
51
72
|
rescue => e
|
|
52
|
-
# Close the response and evict the client
|
|
53
|
-
#
|
|
73
|
+
# Close the response and evict the client that failed so its stale
|
|
74
|
+
# connections are not reused. Evicting by identity leaves a replacement
|
|
75
|
+
# client for the host alone.
|
|
54
76
|
async_response&.close
|
|
55
|
-
if connection_error?(e)
|
|
56
|
-
@client_pool.evict(
|
|
77
|
+
if client && connection_error?(e)
|
|
78
|
+
@client_pool.evict(url, client)
|
|
57
79
|
end
|
|
58
80
|
raise
|
|
59
81
|
end
|
|
60
82
|
end
|
|
61
83
|
|
|
62
|
-
#
|
|
84
|
+
# Decodes raw response data into deliverable response data.
|
|
63
85
|
#
|
|
64
86
|
# Joins and inflates the raw body chunks, applies the charset, and rewrites
|
|
65
87
|
# the content-encoding header to name only the encodings still applied to
|
|
@@ -68,18 +90,23 @@ module PatientHttp
|
|
|
68
90
|
# response always describes the body it carries. This is CPU-bound work
|
|
69
91
|
# intended to run on a completion worker thread.
|
|
70
92
|
#
|
|
71
|
-
# @param response_data [Hash]
|
|
72
|
-
# @return [Hash]
|
|
73
|
-
#
|
|
93
|
+
# @param response_data [Hash] Raw response data from {#make_request}.
|
|
94
|
+
# @return [Hash] Response data with the :status, :headers, and decoded
|
|
95
|
+
# :body keys.
|
|
96
|
+
# @raise [ResponseTooLargeError] If the inflated body exceeds max_response_size.
|
|
74
97
|
def decode_response(response_data)
|
|
75
98
|
headers = response_data[:headers]
|
|
76
|
-
body = @response_reader.decode_body(
|
|
99
|
+
body = @response_reader.decode_body(
|
|
100
|
+
response_data[:body],
|
|
101
|
+
headers,
|
|
102
|
+
max_response_size: response_data[:max_response_size]
|
|
103
|
+
)
|
|
77
104
|
headers = ResponseReader.rewrite_content_encoding(headers)
|
|
78
105
|
|
|
79
|
-
response_data.merge(headers: headers, body: body)
|
|
106
|
+
response_data.except(:max_response_size).merge(headers: headers, body: body)
|
|
80
107
|
end
|
|
81
108
|
|
|
82
|
-
#
|
|
109
|
+
# Closes all clients and releases their resources.
|
|
83
110
|
#
|
|
84
111
|
# @return [void]
|
|
85
112
|
def close
|