patient_http 1.6.1 → 1.7.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 +8 -7
- data/CHANGELOG.md +29 -0
- data/README.md +539 -515
- 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 +28 -22
- data/lib/patient_http/client_pool.rb +134 -39
- data/lib/patient_http/completion_executor.rb +20 -20
- data/lib/patient_http/configuration.rb +367 -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 +70 -46
- data/lib/patient_http/request_error.rb +47 -42
- data/lib/patient_http/request_helper.rb +142 -119
- data/lib/patient_http/request_preparer.rb +13 -10
- data/lib/patient_http/request_task.rb +113 -84
- data/lib/patient_http/request_template.rb +87 -64
- data/lib/patient_http/response.rb +58 -52
- data/lib/patient_http/response_reader.rb +66 -65
- 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 +67 -95
- data/lib/patient_http/task_handler.rb +23 -19
- data/lib/patient_http/time_helper.rb +8 -8
- data/lib/patient_http.rb +311 -186
- data/patient_http.gemspec +3 -2
- metadata +21 -5
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# HTTP headers with case-insensitive names.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# key access. Header names are normalized to lowercase for storage and lookup.
|
|
6
|
+
# The interface is like a Hash. Names are stored in lowercase.
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
# empty string
|
|
8
|
+
# A header with an empty value isn't stored. If you set a header to `nil` or
|
|
9
|
+
# an empty string, the header is removed. As a result, a request never sends
|
|
10
|
+
# a header with no value.
|
|
11
11
|
class HttpHeaders
|
|
12
12
|
include Enumerable
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
# are skipped.
|
|
14
|
+
# Creates a set of headers. Entries with a `nil` or empty value are skipped.
|
|
16
15
|
#
|
|
17
|
-
# @param headers [Hash, HttpHeaders]
|
|
16
|
+
# @param headers [Hash, HttpHeaders] Initial headers to set.
|
|
18
17
|
def initialize(headers = {})
|
|
19
18
|
@headers = {}
|
|
20
19
|
headers&.each do |key, value|
|
|
@@ -24,17 +23,17 @@ module PatientHttp
|
|
|
24
23
|
|
|
25
24
|
# Retrieves the value for a header (case insensitive).
|
|
26
25
|
#
|
|
27
|
-
# @param key [String, Symbol]
|
|
28
|
-
# @return [String, nil]
|
|
26
|
+
# @param key [String, Symbol] Header name.
|
|
27
|
+
# @return [String, nil] Header value or nil if not found.
|
|
29
28
|
def [](key)
|
|
30
29
|
@headers[key.to_s.downcase]
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
# Sets the value for a header (case insensitive). Setting a header to nil
|
|
32
|
+
# Sets the value for a header (case insensitive). Setting a header to `nil`
|
|
34
33
|
# or an empty string removes it.
|
|
35
34
|
#
|
|
36
|
-
# @param key [String, Symbol]
|
|
37
|
-
# @param value [String, nil]
|
|
35
|
+
# @param key [String, Symbol] Header name.
|
|
36
|
+
# @param value [String, nil] Header value.
|
|
38
37
|
def []=(key, value)
|
|
39
38
|
name = key.to_s.downcase
|
|
40
39
|
if empty_value?(value)
|
|
@@ -46,25 +45,25 @@ module PatientHttp
|
|
|
46
45
|
|
|
47
46
|
# Removes a header (case insensitive).
|
|
48
47
|
#
|
|
49
|
-
# @param key [String, Symbol]
|
|
50
|
-
# @return [String, nil]
|
|
48
|
+
# @param key [String, Symbol] Header name.
|
|
49
|
+
# @return [String, nil] The removed value or nil if not found.
|
|
51
50
|
def delete(key)
|
|
52
51
|
@headers.delete(key.to_s.downcase)
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
# Fetches the value for a header with an optional default.
|
|
56
55
|
#
|
|
57
|
-
# @param key [String, Symbol]
|
|
58
|
-
# @param default [Object]
|
|
59
|
-
# @return [String, Object]
|
|
56
|
+
# @param key [String, Symbol] Header name.
|
|
57
|
+
# @param default [Object] Default value if header not found.
|
|
58
|
+
# @return [String, Object] Header value or default.
|
|
60
59
|
def fetch(key, default = nil)
|
|
61
60
|
@headers.fetch(key.to_s.downcase, default)
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
# Merges another set of headers into a new HttpHeaders instance.
|
|
65
64
|
#
|
|
66
|
-
# @param other_headers [Hash, HttpHeaders]
|
|
67
|
-
# @return [HttpHeaders]
|
|
65
|
+
# @param other_headers [Hash, HttpHeaders] Headers to merge.
|
|
66
|
+
# @return [HttpHeaders] New instance with merged headers.
|
|
68
67
|
def merge(other_headers)
|
|
69
68
|
new_headers = dup
|
|
70
69
|
other_headers.each do |key, value|
|
|
@@ -75,8 +74,8 @@ module PatientHttp
|
|
|
75
74
|
|
|
76
75
|
# Returns a new HttpHeaders without the specified keys (case-insensitive).
|
|
77
76
|
#
|
|
78
|
-
# @param keys [Array<String, Symbol>]
|
|
79
|
-
# @return [HttpHeaders]
|
|
77
|
+
# @param keys [Array<String, Symbol>] Header names to exclude.
|
|
78
|
+
# @return [HttpHeaders] New instance without the specified headers.
|
|
80
79
|
def except(*keys)
|
|
81
80
|
normalized = keys.map { |k| k.to_s.downcase }
|
|
82
81
|
filtered_headers = @headers.reject { |key, _value| normalized.include?(key) } # rubocop:disable Style/HashExcept
|
|
@@ -85,38 +84,45 @@ module PatientHttp
|
|
|
85
84
|
|
|
86
85
|
# Converts to a regular hash with lowercase keys.
|
|
87
86
|
#
|
|
88
|
-
# @return [Hash]
|
|
87
|
+
# @return [Hash] Hash representation.
|
|
89
88
|
def to_h
|
|
90
89
|
@headers.dup
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
# Iterates over each header.
|
|
94
93
|
#
|
|
95
|
-
# @yield [key, value]
|
|
96
|
-
# @return [Enumerator]
|
|
94
|
+
# @yield [key, value] Yields each header key-value pair.
|
|
95
|
+
# @return [Enumerator] If no block given.
|
|
97
96
|
def each(&block)
|
|
98
97
|
@headers.each(&block)
|
|
99
98
|
end
|
|
100
99
|
|
|
101
|
-
#
|
|
100
|
+
# Returns whether a header exists (case insensitive).
|
|
102
101
|
#
|
|
103
|
-
# @param name [String, Symbol]
|
|
104
|
-
# @return [Boolean] true if header exists
|
|
102
|
+
# @param name [String, Symbol] Header name.
|
|
103
|
+
# @return [Boolean] `true` if header exists.
|
|
105
104
|
def include?(name)
|
|
106
105
|
@headers.include?(name.to_s.downcase)
|
|
107
106
|
end
|
|
108
107
|
|
|
109
|
-
#
|
|
108
|
+
# Ensures copies do not share the underlying storage so that mutating a
|
|
110
109
|
# copy (for example, via #merge) does not modify the original.
|
|
111
110
|
def initialize_copy(other)
|
|
112
111
|
super
|
|
113
112
|
@headers = @headers.dup
|
|
114
113
|
end
|
|
115
114
|
|
|
115
|
+
# Returns whether another object has the same headers.
|
|
116
|
+
#
|
|
117
|
+
# @param other [Object] The object to compare.
|
|
118
|
+
# @return [Boolean] `true` if the headers are equal.
|
|
116
119
|
def eql?(other)
|
|
117
120
|
other.is_a?(HttpHeaders) && @headers.eql?(other.to_h)
|
|
118
121
|
end
|
|
119
122
|
|
|
123
|
+
# Returns a hash code based on the headers.
|
|
124
|
+
#
|
|
125
|
+
# @return [Integer] The hash code.
|
|
120
126
|
def hash
|
|
121
127
|
@headers.hash
|
|
122
128
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
# Retries a request at once when its failure is known to be safe to retry.
|
|
5
|
+
#
|
|
6
|
+
# Only failures raised before any response byte arrives are considered; a
|
|
7
|
+
# failure while reading the body is never retried. The pooled clients make a
|
|
8
|
+
# single attempt per call, so this module is the only layer that retries and
|
|
9
|
+
# one limit bounds how many times a request is sent.
|
|
10
|
+
#
|
|
11
|
+
# A connection failure evicts the host's pooled client before the retry, so
|
|
12
|
+
# the retry opens a new connection instead of taking another idle connection
|
|
13
|
+
# that may have failed the same way. Each attempt looks up the host's client
|
|
14
|
+
# in the pool, so a retry never runs on a client a concurrent failure evicted.
|
|
15
|
+
#
|
|
16
|
+
# The including class must provide a private `config` method returning the
|
|
17
|
+
# {Configuration}, which supplies the logger.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
module ImmediateRetries
|
|
21
|
+
# Minimum number of attempts made after a failure that is safe to retry. A
|
|
22
|
+
# pool `retries` setting above IMMEDIATE_RETRY_LIMIT + 1 allows
|
|
23
|
+
# `retries - 1` attempts instead.
|
|
24
|
+
IMMEDIATE_RETRY_LIMIT = 2
|
|
25
|
+
|
|
26
|
+
# Errors from a connection that failed before delivering any response byte.
|
|
27
|
+
# They are ambiguous for a non-idempotent request: the server may have
|
|
28
|
+
# processed it and then died, or it may never have received it. IOError
|
|
29
|
+
# covers EOFError and the IO::TimeoutError raised by the connection timeout.
|
|
30
|
+
# EPIPE means a write failed, but the server may already have read enough of
|
|
31
|
+
# the request to act on it before closing. ETIMEDOUT is the kernel giving up
|
|
32
|
+
# on unacknowledged data (see the TCP user timeout), not the request
|
|
33
|
+
# timeout, which is never retried.
|
|
34
|
+
CONNECTION_ERRORS = [
|
|
35
|
+
IOError, SocketError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
|
|
36
|
+
::Protocol::HTTP::RemoteError
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Sends the request through the pool, retrying safe failures at once.
|
|
42
|
+
#
|
|
43
|
+
# @param client_pool [ClientPool] The pool to send through.
|
|
44
|
+
# @param request [Request] The request being sent, used for its method and URL.
|
|
45
|
+
# @param endpoint [Async::HTTP::Endpoint] The endpoint parsed from the prepared URL.
|
|
46
|
+
# @param headers [Hash] The prepared request headers.
|
|
47
|
+
# @param body [Protocol::HTTP::Body::Buffered, nil] The request body.
|
|
48
|
+
# @yield [client] Each pooled client before a request is sent through it.
|
|
49
|
+
# @return [Protocol::HTTP::Response] The response with its headers read.
|
|
50
|
+
def request_with_immediate_retries(client_pool, request, endpoint, headers, body)
|
|
51
|
+
limit = [client_pool.retries - 1, IMMEDIATE_RETRY_LIMIT].max
|
|
52
|
+
attempt = 1
|
|
53
|
+
|
|
54
|
+
loop do
|
|
55
|
+
client = client_pool.client_for(endpoint)
|
|
56
|
+
yield client if block_given?
|
|
57
|
+
return client_pool.request(request.http_method, endpoint, headers, body, client: client)
|
|
58
|
+
rescue => e
|
|
59
|
+
unless attempt <= limit && immediately_retryable?(request, e)
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if client && connection_failure?(e)
|
|
64
|
+
client_pool.evict(endpoint.url.to_s, client)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attempt += 1
|
|
68
|
+
body&.rewind
|
|
69
|
+
# The request URL is logged rather than the prepared URL, which carries
|
|
70
|
+
# resolved secret params.
|
|
71
|
+
config.logger&.info(
|
|
72
|
+
"[PatientHttp] Request to #{request.url} failed before a response " \
|
|
73
|
+
"(#{e.class.name}: #{e.message}); retrying (attempt #{attempt})"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A refused request (an HTTP/2 GOAWAY, a pooled connection closed after it was
|
|
79
|
+
# acquired, or a rejected stream) was never processed by the server, so it is
|
|
80
|
+
# safe to retry whatever the method. A connection that fails in any other way
|
|
81
|
+
# before responding may have processed the request, so that failure is
|
|
82
|
+
# retried only for idempotent methods.
|
|
83
|
+
def immediately_retryable?(request, error)
|
|
84
|
+
case error
|
|
85
|
+
when ::Protocol::HTTP::RefusedError
|
|
86
|
+
true
|
|
87
|
+
when *CONNECTION_ERRORS
|
|
88
|
+
request.idempotent?
|
|
89
|
+
else
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def connection_failure?(error)
|
|
95
|
+
CONNECTION_ERRORS.any? { |error_class| error.is_a?(error_class) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# The task handler for requests that run inline.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# have no job queue, so retrying is not supported.
|
|
6
|
+
# {SynchronousExecutor} calls the callback service directly, so
|
|
7
|
+
# {#on_complete} and {#on_error} do nothing. Inline requests have no job
|
|
8
|
+
# queue, so they can't be retried.
|
|
10
9
|
#
|
|
11
10
|
# @api private
|
|
12
11
|
class InlineTaskHandler < TaskHandler
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# Does nothing.
|
|
13
|
+
#
|
|
14
|
+
# @param response [Response] The HTTP response.
|
|
15
|
+
# @param callback [String] The callback service class name.
|
|
15
16
|
# @return [void]
|
|
16
17
|
def on_complete(response, callback)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
20
|
+
# Does nothing.
|
|
21
|
+
#
|
|
22
|
+
# @param error [Error] The error.
|
|
23
|
+
# @param callback [String] The callback service class name.
|
|
21
24
|
# @return [void]
|
|
22
25
|
def on_error(error, callback)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
#
|
|
28
|
+
# Raises an error, because inline requests can't be retried.
|
|
29
|
+
#
|
|
30
|
+
# @raise [NotImplementedError] Always.
|
|
26
31
|
def retry
|
|
27
32
|
raise NotImplementedError, "Inline requests cannot be retried"
|
|
28
33
|
end
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
# Manages the
|
|
4
|
+
# Manages the state of a {Processor}. The state changes are thread-safe.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# Thread-safe state management using Concurrent::AtomicReference.
|
|
6
|
+
# @api private
|
|
8
7
|
class LifecycleManager
|
|
9
8
|
include TimeHelper
|
|
10
9
|
|
|
11
|
-
# Valid processor states
|
|
10
|
+
# Valid processor states.
|
|
12
11
|
STATES = %i[stopped starting running draining stopping].freeze
|
|
13
12
|
|
|
14
|
-
# Polling interval during wait operations
|
|
13
|
+
# Polling interval during wait operations.
|
|
15
14
|
POLL_INTERVAL = 0.01
|
|
16
15
|
|
|
17
|
-
#
|
|
16
|
+
# Creates the lifecycle manager.
|
|
18
17
|
#
|
|
19
18
|
# @return [void]
|
|
20
19
|
def initialize
|
|
@@ -24,53 +23,53 @@ module PatientHttp
|
|
|
24
23
|
@lock = Mutex.new
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
#
|
|
26
|
+
# Returns the current state.
|
|
28
27
|
#
|
|
29
|
-
# @return [Symbol]
|
|
28
|
+
# @return [Symbol] The current state.
|
|
30
29
|
def state
|
|
31
30
|
@state.get
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
#
|
|
33
|
+
# Returns whether the processor is starting.
|
|
35
34
|
#
|
|
36
|
-
# @return [Boolean] true if starting
|
|
35
|
+
# @return [Boolean] `true` if starting.
|
|
37
36
|
def starting?
|
|
38
37
|
state == :starting
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
#
|
|
40
|
+
# Returns whether the processor is running.
|
|
42
41
|
#
|
|
43
|
-
# @return [Boolean] true if running
|
|
42
|
+
# @return [Boolean] `true` if running.
|
|
44
43
|
def running?
|
|
45
44
|
state == :running
|
|
46
45
|
end
|
|
47
46
|
|
|
48
|
-
#
|
|
47
|
+
# Returns whether the processor is stopped.
|
|
49
48
|
#
|
|
50
|
-
# @return [Boolean] true if stopped
|
|
49
|
+
# @return [Boolean] `true` if stopped.
|
|
51
50
|
def stopped?
|
|
52
51
|
state == :stopped
|
|
53
52
|
end
|
|
54
53
|
|
|
55
|
-
#
|
|
54
|
+
# Returns whether the processor is draining.
|
|
56
55
|
#
|
|
57
|
-
# @return [Boolean] true if draining
|
|
56
|
+
# @return [Boolean] `true` if draining.
|
|
58
57
|
def draining?
|
|
59
58
|
state == :draining
|
|
60
59
|
end
|
|
61
60
|
|
|
62
|
-
#
|
|
61
|
+
# Returns whether the processor is stopping.
|
|
63
62
|
#
|
|
64
|
-
# @return [Boolean] true if stopping
|
|
63
|
+
# @return [Boolean] `true` if stopping.
|
|
65
64
|
def stopping?
|
|
66
65
|
state == :stopping
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
#
|
|
68
|
+
# Transitions to starting state. The processor can only be started from
|
|
70
69
|
# the stopped state; in particular, starting a draining processor would
|
|
71
70
|
# spawn a second reactor alongside the one still finishing its drain.
|
|
72
71
|
#
|
|
73
|
-
# @return [Boolean] true if transition was successful
|
|
72
|
+
# @return [Boolean] `true` if transition was successful.
|
|
74
73
|
def start!
|
|
75
74
|
@lock.synchronize do
|
|
76
75
|
return false unless stopped?
|
|
@@ -83,12 +82,12 @@ module PatientHttp
|
|
|
83
82
|
true
|
|
84
83
|
end
|
|
85
84
|
|
|
86
|
-
#
|
|
85
|
+
# Transitions to running state.
|
|
87
86
|
#
|
|
88
87
|
# The transition only occurs from the starting state so that a reactor
|
|
89
88
|
# that already failed and transitioned to stopped is not overwritten.
|
|
90
89
|
#
|
|
91
|
-
# @return [Boolean] true if transition was successful
|
|
90
|
+
# @return [Boolean] `true` if transition was successful.
|
|
92
91
|
def running!
|
|
93
92
|
@lock.synchronize do
|
|
94
93
|
return false unless starting?
|
|
@@ -99,9 +98,9 @@ module PatientHttp
|
|
|
99
98
|
true
|
|
100
99
|
end
|
|
101
100
|
|
|
102
|
-
#
|
|
101
|
+
# Transitions to draining state.
|
|
103
102
|
#
|
|
104
|
-
# @return [Boolean] true if transition was successful
|
|
103
|
+
# @return [Boolean] `true` if transition was successful.
|
|
105
104
|
def drain!
|
|
106
105
|
@lock.synchronize do
|
|
107
106
|
return false unless running?
|
|
@@ -112,9 +111,9 @@ module PatientHttp
|
|
|
112
111
|
true
|
|
113
112
|
end
|
|
114
113
|
|
|
115
|
-
#
|
|
114
|
+
# Transitions to stopping state.
|
|
116
115
|
#
|
|
117
|
-
# @return [Boolean] true if transition was successful
|
|
116
|
+
# @return [Boolean] `true` if transition was successful.
|
|
118
117
|
def stop!
|
|
119
118
|
@lock.synchronize do
|
|
120
119
|
return false if stopped? || stopping? || starting?
|
|
@@ -126,7 +125,7 @@ module PatientHttp
|
|
|
126
125
|
true
|
|
127
126
|
end
|
|
128
127
|
|
|
129
|
-
#
|
|
128
|
+
# Transitions to stopped state.
|
|
130
129
|
#
|
|
131
130
|
# Also signals the reactor_ready event to unblock any thread
|
|
132
131
|
# waiting in {#wait_for_reactor} in case the reactor failed
|
|
@@ -138,41 +137,41 @@ module PatientHttp
|
|
|
138
137
|
@reactor_ready.set
|
|
139
138
|
end
|
|
140
139
|
|
|
141
|
-
#
|
|
140
|
+
# Signals that the reactor is ready.
|
|
142
141
|
#
|
|
143
142
|
# @return [void]
|
|
144
143
|
def reactor_ready!
|
|
145
144
|
@reactor_ready.set
|
|
146
145
|
end
|
|
147
146
|
|
|
148
|
-
#
|
|
147
|
+
# Waits for the reactor to be ready.
|
|
149
148
|
#
|
|
150
|
-
# @param timeout [Numeric, nil]
|
|
151
|
-
# @return [Boolean] true if the reactor is ready, false if the timeout was reached
|
|
149
|
+
# @param timeout [Numeric, nil] Maximum time to wait in seconds (nil waits forever).
|
|
150
|
+
# @return [Boolean] `true` if the reactor is ready, false if the timeout was reached.
|
|
152
151
|
def wait_for_reactor(timeout: nil)
|
|
153
152
|
@reactor_ready.wait(timeout)
|
|
154
153
|
end
|
|
155
154
|
|
|
156
|
-
#
|
|
155
|
+
# Returns whether shutdown has been signaled.
|
|
157
156
|
#
|
|
158
|
-
# @return [Boolean] true if shutdown is signaled
|
|
157
|
+
# @return [Boolean] `true` if shutdown is signaled.
|
|
159
158
|
def shutdown_signaled?
|
|
160
159
|
@shutdown_barrier.set?
|
|
161
160
|
end
|
|
162
161
|
|
|
163
|
-
#
|
|
162
|
+
# Waits for running state.
|
|
164
163
|
#
|
|
165
|
-
# @param timeout [Numeric]
|
|
166
|
-
# @return [Boolean] true if running, false if timeout reached
|
|
164
|
+
# @param timeout [Numeric] Maximum time to wait in seconds.
|
|
165
|
+
# @return [Boolean] `true` if running, false if timeout reached.
|
|
167
166
|
def wait_for_running(timeout: 5)
|
|
168
167
|
wait_for_condition(timeout: timeout) { running? }
|
|
169
168
|
end
|
|
170
169
|
|
|
171
|
-
#
|
|
170
|
+
# Waits for a condition to be met.
|
|
172
171
|
#
|
|
173
|
-
# @param timeout [Numeric]
|
|
174
|
-
# @yield
|
|
175
|
-
# @return [Boolean] true if the condition is met, false if timeout reached
|
|
172
|
+
# @param timeout [Numeric] Maximum time to wait in seconds.
|
|
173
|
+
# @yield The block that checks the condition.
|
|
174
|
+
# @return [Boolean] `true` if the condition is met, false if timeout reached.
|
|
176
175
|
def wait_for_condition(timeout: 1)
|
|
177
176
|
deadline = monotonic_time + timeout
|
|
178
177
|
while monotonic_time <= deadline
|
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
# A
|
|
5
|
-
#
|
|
6
|
-
# user-agent) have been set.
|
|
4
|
+
# A request immediately before it's sent. Secret references are resolved, and
|
|
5
|
+
# the `x-request-id` and default `user-agent` headers are set.
|
|
7
6
|
#
|
|
8
|
-
# Preprocessors
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# Preprocessors receive this object. They can change the headers and add
|
|
8
|
+
# query parameters, for example to sign the request. The HTTP method, URL,
|
|
9
|
+
# and body are read-only. Change the headers in place, and add query
|
|
10
|
+
# parameters with {#add_param}.
|
|
12
11
|
#
|
|
13
12
|
# @see Configuration#register_preprocessor
|
|
14
13
|
class OutgoingRequest
|
|
15
|
-
# @return [Symbol] HTTP method
|
|
14
|
+
# @return [Symbol] The HTTP method: `:get`, `:head`, `:post`, `:put`,
|
|
15
|
+
# `:patch`, `:delete`, or `:query`.
|
|
16
16
|
attr_reader :http_method
|
|
17
17
|
|
|
18
|
-
# @return [String]
|
|
18
|
+
# @return [String] The request URL, with the secret query parameters
|
|
19
|
+
# resolved.
|
|
19
20
|
attr_reader :url
|
|
20
21
|
|
|
21
|
-
# @return [String, nil]
|
|
22
|
+
# @return [String, nil] The request body.
|
|
22
23
|
attr_reader :body
|
|
23
24
|
|
|
24
|
-
# @return [HttpHeaders]
|
|
25
|
+
# @return [HttpHeaders] The request headers. You can change them. Names are
|
|
26
|
+
# case insensitive.
|
|
25
27
|
attr_reader :headers
|
|
26
28
|
|
|
27
|
-
#
|
|
29
|
+
# Creates an outgoing request.
|
|
28
30
|
#
|
|
29
|
-
# @param http_method [Symbol]
|
|
30
|
-
# @param url [String]
|
|
31
|
-
# @param headers [HttpHeaders]
|
|
32
|
-
# @param body [String, nil]
|
|
31
|
+
# @param http_method [Symbol] The HTTP method.
|
|
32
|
+
# @param url [String] The request URL, with secrets resolved.
|
|
33
|
+
# @param headers [HttpHeaders] The request headers, with secrets resolved.
|
|
34
|
+
# @param body [String, nil] The request body.
|
|
33
35
|
def initialize(http_method:, url:, headers:, body:)
|
|
34
36
|
@http_method = http_method
|
|
35
37
|
@url = url.to_s
|
|
@@ -37,11 +39,11 @@ module PatientHttp
|
|
|
37
39
|
@body = body
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
#
|
|
42
|
+
# Adds a query parameter to the request URL.
|
|
41
43
|
#
|
|
42
|
-
# @param name [String, Symbol]
|
|
43
|
-
# @param value [Object]
|
|
44
|
-
# @return [String]
|
|
44
|
+
# @param name [String, Symbol] The parameter name.
|
|
45
|
+
# @param value [Object] The parameter value.
|
|
46
|
+
# @return [String] The new URL.
|
|
45
47
|
def add_param(name, value)
|
|
46
48
|
serialized_param = URI.encode_www_form([[name.to_s, value]])
|
|
47
49
|
uri = URI(@url)
|
|
@@ -49,10 +51,10 @@ module PatientHttp
|
|
|
49
51
|
@url = uri.to_s
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
+
# Returns a description of the request. It doesn't show the header values,
|
|
55
|
+
# the query string, or the body, because they can contain secrets.
|
|
54
56
|
#
|
|
55
|
-
# @return [String]
|
|
57
|
+
# @return [String] The description.
|
|
56
58
|
def inspect
|
|
57
59
|
"#<#{self.class.name} #{http_method.to_s.upcase} #{redacted_url} headers=#{headers.to_h.keys.inspect}>"
|
|
58
60
|
end
|