http 6.0.0-java

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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/CONTRIBUTING.md +26 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +263 -0
  6. data/SECURITY.md +17 -0
  7. data/UPGRADING.md +491 -0
  8. data/http.gemspec +48 -0
  9. data/lib/http/base64.rb +22 -0
  10. data/lib/http/chainable/helpers.rb +62 -0
  11. data/lib/http/chainable/verbs.rb +136 -0
  12. data/lib/http/chainable.rb +377 -0
  13. data/lib/http/client.rb +230 -0
  14. data/lib/http/connection/internals.rb +141 -0
  15. data/lib/http/connection.rb +265 -0
  16. data/lib/http/content_type.rb +89 -0
  17. data/lib/http/errors.rb +67 -0
  18. data/lib/http/feature.rb +86 -0
  19. data/lib/http/features/auto_deflate.rb +230 -0
  20. data/lib/http/features/auto_inflate.rb +64 -0
  21. data/lib/http/features/caching/entry.rb +178 -0
  22. data/lib/http/features/caching/in_memory_store.rb +63 -0
  23. data/lib/http/features/caching.rb +216 -0
  24. data/lib/http/features/digest_auth.rb +234 -0
  25. data/lib/http/features/instrumentation.rb +149 -0
  26. data/lib/http/features/logging.rb +231 -0
  27. data/lib/http/features/normalize_uri.rb +34 -0
  28. data/lib/http/features/raise_error.rb +37 -0
  29. data/lib/http/form_data/composite_io.rb +106 -0
  30. data/lib/http/form_data/file.rb +95 -0
  31. data/lib/http/form_data/multipart/param.rb +62 -0
  32. data/lib/http/form_data/multipart.rb +106 -0
  33. data/lib/http/form_data/part.rb +52 -0
  34. data/lib/http/form_data/readable.rb +58 -0
  35. data/lib/http/form_data/urlencoded.rb +175 -0
  36. data/lib/http/form_data/version.rb +8 -0
  37. data/lib/http/form_data.rb +102 -0
  38. data/lib/http/headers/known.rb +90 -0
  39. data/lib/http/headers/normalizer.rb +50 -0
  40. data/lib/http/headers.rb +343 -0
  41. data/lib/http/mime_type/adapter.rb +43 -0
  42. data/lib/http/mime_type/json.rb +41 -0
  43. data/lib/http/mime_type.rb +96 -0
  44. data/lib/http/options/definitions.rb +189 -0
  45. data/lib/http/options.rb +241 -0
  46. data/lib/http/redirector.rb +157 -0
  47. data/lib/http/request/body.rb +181 -0
  48. data/lib/http/request/builder.rb +184 -0
  49. data/lib/http/request/proxy.rb +83 -0
  50. data/lib/http/request/writer.rb +186 -0
  51. data/lib/http/request.rb +375 -0
  52. data/lib/http/response/body.rb +172 -0
  53. data/lib/http/response/inflater.rb +60 -0
  54. data/lib/http/response/parser.rb +223 -0
  55. data/lib/http/response/status/reasons.rb +79 -0
  56. data/lib/http/response/status.rb +263 -0
  57. data/lib/http/response.rb +350 -0
  58. data/lib/http/retriable/delay_calculator.rb +91 -0
  59. data/lib/http/retriable/errors.rb +35 -0
  60. data/lib/http/retriable/performer.rb +197 -0
  61. data/lib/http/session.rb +280 -0
  62. data/lib/http/timeout/global.rb +229 -0
  63. data/lib/http/timeout/null.rb +225 -0
  64. data/lib/http/timeout/per_operation.rb +197 -0
  65. data/lib/http/uri/normalizer.rb +82 -0
  66. data/lib/http/uri/parsing.rb +182 -0
  67. data/lib/http/uri.rb +376 -0
  68. data/lib/http/version.rb +6 -0
  69. data/lib/http.rb +36 -0
  70. data/sig/deps.rbs +122 -0
  71. data/sig/http.rbs +1619 -0
  72. data/test/http/base64_test.rb +28 -0
  73. data/test/http/client_test.rb +739 -0
  74. data/test/http/connection_test.rb +1533 -0
  75. data/test/http/content_type_test.rb +190 -0
  76. data/test/http/errors_test.rb +28 -0
  77. data/test/http/feature_test.rb +49 -0
  78. data/test/http/features/auto_deflate_test.rb +317 -0
  79. data/test/http/features/auto_inflate_test.rb +213 -0
  80. data/test/http/features/caching_test.rb +942 -0
  81. data/test/http/features/digest_auth_test.rb +996 -0
  82. data/test/http/features/instrumentation_test.rb +246 -0
  83. data/test/http/features/logging_test.rb +654 -0
  84. data/test/http/features/normalize_uri_test.rb +41 -0
  85. data/test/http/features/raise_error_test.rb +77 -0
  86. data/test/http/form_data/composite_io_test.rb +215 -0
  87. data/test/http/form_data/file_test.rb +255 -0
  88. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  89. data/test/http/form_data/multipart_test.rb +303 -0
  90. data/test/http/form_data/part_test.rb +90 -0
  91. data/test/http/form_data/urlencoded_test.rb +164 -0
  92. data/test/http/form_data_test.rb +232 -0
  93. data/test/http/headers/normalizer_test.rb +93 -0
  94. data/test/http/headers_test.rb +888 -0
  95. data/test/http/mime_type/json_test.rb +39 -0
  96. data/test/http/mime_type_test.rb +150 -0
  97. data/test/http/options/base_uri_test.rb +148 -0
  98. data/test/http/options/body_test.rb +21 -0
  99. data/test/http/options/features_test.rb +38 -0
  100. data/test/http/options/form_test.rb +21 -0
  101. data/test/http/options/headers_test.rb +32 -0
  102. data/test/http/options/json_test.rb +21 -0
  103. data/test/http/options/merge_test.rb +78 -0
  104. data/test/http/options/new_test.rb +37 -0
  105. data/test/http/options/proxy_test.rb +32 -0
  106. data/test/http/options_test.rb +575 -0
  107. data/test/http/redirector_test.rb +639 -0
  108. data/test/http/request/body_test.rb +318 -0
  109. data/test/http/request/builder_test.rb +623 -0
  110. data/test/http/request/writer_test.rb +391 -0
  111. data/test/http/request_test.rb +1733 -0
  112. data/test/http/response/body_test.rb +292 -0
  113. data/test/http/response/parser_test.rb +105 -0
  114. data/test/http/response/status_test.rb +322 -0
  115. data/test/http/response_test.rb +502 -0
  116. data/test/http/retriable/delay_calculator_test.rb +194 -0
  117. data/test/http/retriable/errors_test.rb +71 -0
  118. data/test/http/retriable/performer_test.rb +551 -0
  119. data/test/http/session_test.rb +424 -0
  120. data/test/http/timeout/global_test.rb +239 -0
  121. data/test/http/timeout/null_test.rb +218 -0
  122. data/test/http/timeout/per_operation_test.rb +220 -0
  123. data/test/http/uri/normalizer_test.rb +89 -0
  124. data/test/http/uri_test.rb +1140 -0
  125. data/test/http/version_test.rb +15 -0
  126. data/test/http_test.rb +818 -0
  127. data/test/regression_tests.rb +27 -0
  128. data/test/support/capture_warning.rb +10 -0
  129. data/test/support/dummy_server/encoding_routes.rb +47 -0
  130. data/test/support/dummy_server/routes.rb +201 -0
  131. data/test/support/dummy_server/servlet.rb +81 -0
  132. data/test/support/dummy_server.rb +200 -0
  133. data/test/support/fakeio.rb +21 -0
  134. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  135. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  136. data/test/support/http_handling_shared.rb +11 -0
  137. data/test/support/proxy_server.rb +207 -0
  138. data/test/support/servers/runner.rb +67 -0
  139. data/test/support/simplecov.rb +28 -0
  140. data/test/support/ssl_helper.rb +108 -0
  141. data/test/test_helper.rb +38 -0
  142. metadata +218 -0
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require "http/cookie_jar"
6
+ require "http/headers"
7
+ require "http/redirector"
8
+ require "http/request/builder"
9
+
10
+ module HTTP
11
+ # Thread-safe options builder for configuring HTTP requests.
12
+ #
13
+ # Session objects are returned by all chainable configuration methods
14
+ # (e.g., {Chainable#headers}, {Chainable#timeout}, {Chainable#cookies}).
15
+ # They hold an immutable {Options} object and create a new {Client}
16
+ # for each request, making them safe to share across threads.
17
+ #
18
+ # When configured for persistent connections (via {Chainable#persistent}),
19
+ # the session maintains a pool of {Client} instances keyed by origin,
20
+ # enabling connection reuse within the same origin and transparent
21
+ # cross-origin redirect handling.
22
+ #
23
+ # @example Reuse a configured session across threads
24
+ # session = HTTP.headers("Accept" => "application/json").timeout(10)
25
+ # threads = 5.times.map do
26
+ # Thread.new { session.get("https://example.com") }
27
+ # end
28
+ # threads.each(&:join)
29
+ #
30
+ # @example Persistent session with cross-origin redirects
31
+ # HTTP.persistent("https://example.com").follow do |http|
32
+ # http.get("/redirect-to-other-domain") # follows cross-origin redirect
33
+ # end
34
+ #
35
+ # @see Chainable
36
+ # @see Client
37
+ class Session
38
+ extend Forwardable
39
+ include Chainable
40
+
41
+ # @!method persistent?
42
+ # Indicate whether the session has persistent connection options
43
+ #
44
+ # @example
45
+ # session = HTTP::Session.new(persistent: "http://example.com")
46
+ # session.persistent?
47
+ #
48
+ # @see Options#persistent?
49
+ # @return [Boolean]
50
+ # @api public
51
+ def_delegator :default_options, :persistent?
52
+
53
+ # Initialize a new Session
54
+ #
55
+ # @example
56
+ # session = HTTP::Session.new(headers: {"Accept" => "application/json"})
57
+ #
58
+ # @param default_options [HTTP::Options, nil] existing options instance
59
+ # @param clients [Hash, nil] shared connection pool (internal use)
60
+ # @param options [Hash] keyword options (see HTTP::Options#initialize)
61
+ # @return [HTTP::Session] a new session instance
62
+ # @api public
63
+ def initialize(default_options = nil, clients: nil, **)
64
+ @default_options = HTTP::Options.new(default_options, **)
65
+ @clients = clients || {}
66
+ end
67
+
68
+ # Close all persistent connections held by this session
69
+ #
70
+ # When the session is persistent, this closes every pooled {Client}
71
+ # and clears the pool. Safe to call on non-persistent sessions (no-op).
72
+ #
73
+ # @example
74
+ # session = HTTP.persistent("https://example.com")
75
+ # session.get("/")
76
+ # session.close
77
+ #
78
+ # @return [void]
79
+ # @api public
80
+ def close
81
+ @clients.each_value(&:close)
82
+ @clients.clear
83
+ end
84
+
85
+ # Make an HTTP request
86
+ #
87
+ # For non-persistent sessions a fresh {Client} is created for each
88
+ # request, ensuring thread safety. For persistent sessions the pooled
89
+ # {Client} for the request's origin is reused.
90
+ #
91
+ # Manages cookies across redirect hops when following redirects.
92
+ #
93
+ # @example Without a block
94
+ # session = HTTP::Session.new
95
+ # session.request(:get, "https://example.com")
96
+ #
97
+ # @example With a block (auto-closes connection)
98
+ # session = HTTP::Session.new
99
+ # session.request(:get, "https://example.com") { |res| res.status }
100
+ #
101
+ # @param verb [Symbol] the HTTP method
102
+ # @param uri [#to_s] the URI to request
103
+ # @yieldparam response [HTTP::Response] the response
104
+ # @return [HTTP::Response, Object] the response, or block return value
105
+ # @api public
106
+ def request(verb, uri,
107
+ headers: nil, params: nil, form: nil, json: nil, body: nil,
108
+ response: nil, encoding: nil, follow: nil, ssl: nil, ssl_context: nil,
109
+ proxy: nil, nodelay: nil, features: nil, retriable: nil,
110
+ socket_class: nil, ssl_socket_class: nil, timeout_class: nil,
111
+ timeout_options: nil, keep_alive_timeout: nil, base_uri: nil, persistent: nil, &block)
112
+ merged = default_options.merge(
113
+ { headers: headers, params: params, form: form, json: json, body: body,
114
+ response: response, encoding: encoding, follow: follow, ssl: ssl,
115
+ ssl_context: ssl_context, proxy: proxy, nodelay: nodelay, features: features,
116
+ retriable: retriable, socket_class: socket_class, ssl_socket_class: ssl_socket_class,
117
+ timeout_class: timeout_class, timeout_options: timeout_options,
118
+ keep_alive_timeout: keep_alive_timeout, base_uri: base_uri, persistent: persistent }.compact
119
+ )
120
+ client = persistent? ? nil : make_client(default_options)
121
+ res = perform_request(client, verb, uri, merged)
122
+
123
+ return res unless block
124
+
125
+ yield res
126
+ ensure
127
+ if block
128
+ persistent? ? close : client&.close
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ # Create a new session with the given options
135
+ #
136
+ # When the current session is persistent, the child session shares the
137
+ # same connection pool so that chaining methods like {Chainable#headers}
138
+ # or {Chainable#auth} do not break connection reuse.
139
+ #
140
+ # @param options [HTTP::Options] options for the new session
141
+ # @return [HTTP::Session]
142
+ # @api private
143
+ def branch(options)
144
+ if persistent?
145
+ self.class.new(options, clients: @clients)
146
+ else
147
+ self.class.new(options)
148
+ end
149
+ end
150
+
151
+ # Execute a request with cookie management
152
+ #
153
+ # @param client [HTTP::Client, nil] the client (nil when persistent; looked up from pool)
154
+ # @param verb [Symbol] the HTTP method
155
+ # @param uri [#to_s] the URI to request
156
+ # @param merged [HTTP::Options] the merged options
157
+ # @return [HTTP::Response] the response
158
+ # @api private
159
+ def perform_request(client, verb, uri, merged)
160
+ cookie_jar = CookieJar.new
161
+ builder = Request::Builder.new(merged)
162
+ req = builder.build(verb, uri)
163
+ client ||= client_for_origin(req.uri.origin)
164
+ load_cookies(cookie_jar, req)
165
+ res = client.perform(req, merged)
166
+ store_cookies(cookie_jar, res)
167
+
168
+ return res unless merged.follow
169
+
170
+ perform_redirects(cookie_jar, client, req, res, merged)
171
+ end
172
+
173
+ # Follow redirects with cookie management
174
+ #
175
+ # For persistent sessions, each redirect hop may target a different
176
+ # origin. The session looks up (or creates) a pooled {Client} for
177
+ # the redirect target's origin, allowing cross-origin redirects
178
+ # without raising {StateError}.
179
+ #
180
+ # @param jar [HTTP::CookieJar] the cookie jar
181
+ # @param client [HTTP::Client] the client for the initial request
182
+ # @param req [HTTP::Request] the original request
183
+ # @param res [HTTP::Response] the initial redirect response
184
+ # @param opts [HTTP::Options] the merged options
185
+ # @return [HTTP::Response] the final non-redirect response
186
+ # @api private
187
+ def perform_redirects(jar, client, req, res, opts)
188
+ builder = Request::Builder.new(opts)
189
+ follow = opts.follow || {} #: Hash[untyped, untyped]
190
+ Redirector.new(**follow).perform(req, res) do |redirect_req|
191
+ wrapped = builder.wrap(redirect_req)
192
+ apply_cookies(jar, wrapped)
193
+ apply_cookies(jar, redirect_req)
194
+ response = redirect_client(client, wrapped).perform(wrapped, opts)
195
+ store_cookies(jar, response)
196
+ response
197
+ end
198
+ end
199
+
200
+ # Return the appropriate client for a redirect hop
201
+ #
202
+ # @param client [HTTP::Client] the client for the original request
203
+ # @param request [HTTP::Request] the redirect request
204
+ # @return [HTTP::Client] the client for the redirect target
205
+ # @api private
206
+ def redirect_client(client, request)
207
+ persistent? ? client_for_origin(request.uri.origin) : client
208
+ end
209
+
210
+ # Return a pooled persistent {Client} for the given origin
211
+ #
212
+ # Creates a new {Client} if one does not already exist for this origin.
213
+ # For the session's primary persistent origin, the default options are
214
+ # used directly. For other origins (e.g. redirect targets), the
215
+ # persistent origin is overridden and base_uri is cleared.
216
+ #
217
+ # @param origin [String] the URI origin (scheme + host + port)
218
+ # @return [HTTP::Client] a persistent client for the origin
219
+ # @api private
220
+ def client_for_origin(origin)
221
+ @clients[origin] ||= make_client(options_for_origin(origin))
222
+ end
223
+
224
+ # Build {Options} for a persistent client targeting the given origin
225
+ #
226
+ # @param origin [String] the URI origin
227
+ # @return [HTTP::Options] options configured for this origin
228
+ # @api private
229
+ def options_for_origin(origin)
230
+ return default_options if origin == default_options.persistent
231
+
232
+ default_options.merge(persistent: origin, base_uri: nil)
233
+ end
234
+
235
+ # Load cookies from the request's Cookie header into the jar
236
+ #
237
+ # @param jar [HTTP::CookieJar] the cookie jar
238
+ # @param request [HTTP::Request] the request
239
+ # @return [void]
240
+ # @api private
241
+ def load_cookies(jar, request)
242
+ header = request.headers[Headers::COOKIE]
243
+ cookies = HTTP::Cookie.cookie_value_to_hash(header.to_s)
244
+
245
+ cookies.each do |name, value|
246
+ jar.add(HTTP::Cookie.new(name, value, path: request.uri.path, domain: request.host))
247
+ end
248
+ end
249
+
250
+ # Store cookies from the response's Set-Cookie headers into the jar
251
+ #
252
+ # @param jar [HTTP::CookieJar] the cookie jar
253
+ # @param response [HTTP::Response] the response
254
+ # @return [void]
255
+ # @api private
256
+ def store_cookies(jar, response)
257
+ response.cookies.each do |cookie|
258
+ if cookie.value == ""
259
+ jar.delete(cookie)
260
+ else
261
+ jar.add(cookie)
262
+ end
263
+ end
264
+ end
265
+
266
+ # Apply cookies from the jar to the request's Cookie header
267
+ #
268
+ # @param jar [HTTP::CookieJar] the cookie jar
269
+ # @param request [HTTP::Request] the request
270
+ # @return [void]
271
+ # @api private
272
+ def apply_cookies(jar, request)
273
+ if jar.empty?
274
+ request.headers.delete(Headers::COOKIE)
275
+ else
276
+ request.headers.set(Headers::COOKIE, jar.map { |c| "#{c.name}=#{c.value}" }.join("; "))
277
+ end
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require "io/wait"
5
+
6
+ require "http/timeout/null"
7
+
8
+ module HTTP
9
+ module Timeout
10
+ # Timeout handler with a single global timeout for the entire request
11
+ class Global < Null
12
+ # I/O wait result symbols returned by non-blocking operations
13
+ WAIT_RESULTS = %i[wait_readable wait_writable].freeze
14
+ # Initializes global timeout with options
15
+ #
16
+ # @example
17
+ # HTTP::Timeout::Global.new(global_timeout: 5)
18
+ #
19
+ # @param [Numeric] global_timeout Global timeout in seconds
20
+ # @param [Numeric, nil] read_timeout Read timeout in seconds
21
+ # @param [Numeric, nil] write_timeout Write timeout in seconds
22
+ # @param [Numeric, nil] connect_timeout Connect timeout in seconds
23
+ # @api public
24
+ # @return [HTTP::Timeout::Global]
25
+ def initialize(global_timeout:, read_timeout: nil, write_timeout: nil, connect_timeout: nil)
26
+ super
27
+
28
+ @timeout = @time_left = global_timeout
29
+ @read_timeout = read_timeout
30
+ @write_timeout = write_timeout
31
+ @connect_timeout = connect_timeout
32
+ end
33
+
34
+ # Resets the time left counter to initial timeout
35
+ #
36
+ # @example
37
+ # timeout.reset_counter
38
+ #
39
+ # @api public
40
+ # @return [Numeric]
41
+ def reset_counter
42
+ @time_left = @timeout
43
+ end
44
+
45
+ # Connects to a socket with global timeout
46
+ #
47
+ # @example
48
+ # timeout.connect(TCPSocket, "example.com", 80)
49
+ #
50
+ # @param [Class] socket_class
51
+ # @param [String] host
52
+ # @param [Integer] port
53
+ # @param [Boolean] nodelay
54
+ # @api public
55
+ # @return [void]
56
+ def connect(socket_class, host, port, nodelay: false)
57
+ reset_timer
58
+ @socket = open_socket(socket_class, host, port, connect_timeout: effective_timeout(@connect_timeout))
59
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
60
+
61
+ log_time
62
+ end
63
+
64
+ # Starts an SSL connection on a socket
65
+ #
66
+ # @example
67
+ # timeout.connect_ssl
68
+ #
69
+ # @api public
70
+ # @return [void]
71
+ def connect_ssl
72
+ reset_timer
73
+
74
+ begin
75
+ @socket.connect_nonblock
76
+ rescue IO::WaitReadable
77
+ wait_readable_or_timeout(@connect_timeout)
78
+ retry
79
+ rescue IO::WaitWritable
80
+ wait_writable_or_timeout(@connect_timeout)
81
+ retry
82
+ end
83
+ end
84
+
85
+ # Read from the socket
86
+ #
87
+ # @example
88
+ # timeout.readpartial(1024)
89
+ #
90
+ # @param [Integer] size
91
+ # @param [String, nil] buffer
92
+ # @api public
93
+ # @return [String, :eof]
94
+ def readpartial(size, buffer = nil)
95
+ perform_io(@read_timeout) { read_nonblock(size, buffer) }
96
+ end
97
+
98
+ # Write to the socket
99
+ #
100
+ # @example
101
+ # timeout.write("GET / HTTP/1.1")
102
+ #
103
+ # @param [String] data
104
+ # @api public
105
+ # @return [Integer, :eof]
106
+ def write(data)
107
+ perform_io(@write_timeout) { write_nonblock(data) }
108
+ end
109
+
110
+ alias << write
111
+
112
+ private
113
+
114
+ # Reads from socket in non-blocking mode
115
+ #
116
+ # @api private
117
+ # @return [String, Symbol]
118
+ def read_nonblock(size, buffer = nil)
119
+ @socket.read_nonblock(size, buffer, exception: false)
120
+ end
121
+
122
+ # Writes to socket in non-blocking mode
123
+ #
124
+ # @api private
125
+ # @return [Integer, Symbol]
126
+ def write_nonblock(data)
127
+ @socket.write_nonblock(data, exception: false)
128
+ end
129
+
130
+ # Performs I/O operation with timeout tracking
131
+ #
132
+ # @param [Numeric, nil] per_op_timeout per-operation timeout limit
133
+ # @api private
134
+ # @return [Object]
135
+ def perform_io(per_op_timeout = nil)
136
+ reset_timer
137
+
138
+ loop do
139
+ result = yield
140
+ return handle_io_result(result) unless WAIT_RESULTS.include?(result)
141
+
142
+ wait_for_io(result, per_op_timeout)
143
+ rescue IO::WaitReadable then wait_readable_or_timeout(per_op_timeout)
144
+ rescue IO::WaitWritable then wait_writable_or_timeout(per_op_timeout)
145
+ end
146
+ rescue EOFError
147
+ :eof
148
+ end
149
+
150
+ # Handles the result of an I/O operation
151
+ #
152
+ # @api private
153
+ # @return [Object, Symbol]
154
+ def handle_io_result(result)
155
+ result.nil? ? :eof : result
156
+ end
157
+
158
+ # Waits for an I/O readiness based on the result type
159
+ #
160
+ # @param [Symbol] result the I/O wait type
161
+ # @param [Numeric, nil] per_op_timeout per-operation timeout limit
162
+ # @api private
163
+ # @return [void]
164
+ def wait_for_io(result, per_op_timeout = nil)
165
+ if result == :wait_readable
166
+ wait_readable_or_timeout(per_op_timeout)
167
+ else
168
+ wait_writable_or_timeout(per_op_timeout)
169
+ end
170
+ end
171
+
172
+ # Waits for a socket to become readable
173
+ #
174
+ # @param [Numeric, nil] per_op per-operation timeout limit
175
+ # @api private
176
+ # @return [void]
177
+ def wait_readable_or_timeout(per_op = nil)
178
+ timeout = effective_timeout(per_op)
179
+ result = @socket.to_io.wait_readable(timeout)
180
+ log_time
181
+
182
+ raise TimeoutError, "Read timed out after #{per_op} seconds" if per_op && result.nil?
183
+ end
184
+
185
+ # Waits for a socket to become writable
186
+ #
187
+ # @param [Numeric, nil] per_op per-operation timeout limit
188
+ # @api private
189
+ # @return [void]
190
+ def wait_writable_or_timeout(per_op = nil)
191
+ timeout = effective_timeout(per_op)
192
+ result = @socket.to_io.wait_writable(timeout)
193
+ log_time
194
+
195
+ raise TimeoutError, "Write timed out after #{per_op} seconds" if per_op && result.nil?
196
+ end
197
+
198
+ # Computes the effective timeout as the minimum of global and per-operation
199
+ #
200
+ # @param [Numeric, nil] per_op_timeout per-operation timeout limit
201
+ # @api private
202
+ # @return [Numeric]
203
+ def effective_timeout(per_op_timeout)
204
+ return @time_left unless per_op_timeout
205
+
206
+ [per_op_timeout, @time_left].min
207
+ end
208
+
209
+ # Resets the I/O timer to current time
210
+ #
211
+ # @api private
212
+ # @return [Time]
213
+ def reset_timer
214
+ @started = Time.now
215
+ end
216
+
217
+ # Logs elapsed time and checks for timeout
218
+ #
219
+ # @api private
220
+ # @return [void]
221
+ def log_time
222
+ @time_left -= (Time.now - @started)
223
+ raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
224
+
225
+ reset_timer
226
+ end
227
+ end
228
+ end
229
+ end