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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +322 -0
  3. data/CHANGELOG.md +30 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +653 -0
  6. data/VERSION +1 -0
  7. data/db/migrate/20250101000000_create_patient_http_payloads.rb +15 -0
  8. data/lib/patient_http/callback_args.rb +176 -0
  9. data/lib/patient_http/callback_validator.rb +52 -0
  10. data/lib/patient_http/class_helper.rb +26 -0
  11. data/lib/patient_http/client.rb +80 -0
  12. data/lib/patient_http/client_pool.rb +178 -0
  13. data/lib/patient_http/configuration.rb +365 -0
  14. data/lib/patient_http/encryptor.rb +69 -0
  15. data/lib/patient_http/error.rb +76 -0
  16. data/lib/patient_http/external_storage.rb +134 -0
  17. data/lib/patient_http/http_error.rb +106 -0
  18. data/lib/patient_http/http_headers.rb +99 -0
  19. data/lib/patient_http/lifecycle_manager.rb +174 -0
  20. data/lib/patient_http/payload.rb +160 -0
  21. data/lib/patient_http/payload_store/active_record_store.rb +102 -0
  22. data/lib/patient_http/payload_store/base.rb +150 -0
  23. data/lib/patient_http/payload_store/file_store.rb +92 -0
  24. data/lib/patient_http/payload_store/redis_store.rb +98 -0
  25. data/lib/patient_http/payload_store/s3_store.rb +94 -0
  26. data/lib/patient_http/payload_store.rb +11 -0
  27. data/lib/patient_http/processor.rb +538 -0
  28. data/lib/patient_http/processor_observer.rb +48 -0
  29. data/lib/patient_http/rails/engine.rb +21 -0
  30. data/lib/patient_http/redirect_error.rb +136 -0
  31. data/lib/patient_http/redirect_helper.rb +90 -0
  32. data/lib/patient_http/request.rb +158 -0
  33. data/lib/patient_http/request_error.rb +150 -0
  34. data/lib/patient_http/request_helper.rb +230 -0
  35. data/lib/patient_http/request_task.rb +308 -0
  36. data/lib/patient_http/request_template.rb +114 -0
  37. data/lib/patient_http/response.rb +183 -0
  38. data/lib/patient_http/response_reader.rb +135 -0
  39. data/lib/patient_http/synchronous_executor.rb +241 -0
  40. data/lib/patient_http/task_handler.rb +55 -0
  41. data/lib/patient_http/time_helper.rb +32 -0
  42. data/lib/patient_http.rb +313 -0
  43. data/patient_http.gemspec +48 -0
  44. metadata +161 -0
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/http"
5
+ require "concurrent"
6
+ require "monitor"
7
+ require "json"
8
+ require "uri"
9
+ require "zlib"
10
+ require "time"
11
+ require "socket"
12
+ require "securerandom"
13
+ require "logger"
14
+
15
+ # Generic async HTTP connection pool for Ruby applications.
16
+ #
17
+ # This module provides:
18
+ # - Async HTTP request processing using Ruby's Fiber scheduler
19
+ # - Connection pooling with HTTP/2 support
20
+ # - Configurable timeouts, retries, and proxy support
21
+ # - Error handling with typed errors
22
+ #
23
+ # This module can be used standalone or integrated with job systems
24
+ # like Sidekiq via adapters.
25
+ module PatientHttp
26
+ # Raised when trying to enqueue a request when the processor is not running
27
+ class NotRunningError < StandardError; end
28
+
29
+ class MaxCapacityError < StandardError; end
30
+
31
+ class ResponseTooLargeError < StandardError; end
32
+
33
+ # HTTP redirect status codes that should be followed
34
+ FOLLOWABLE_REDIRECT_STATUSES = [301, 302, 303, 307, 308].freeze
35
+
36
+ VERSION = File.read(File.join(__dir__, "../VERSION")).strip
37
+
38
+ # Autoload utility modules
39
+ autoload :ClassHelper, File.join(__dir__, "patient_http/class_helper")
40
+ autoload :TimeHelper, File.join(__dir__, "patient_http/time_helper")
41
+
42
+ # Autoload all components
43
+ autoload :CallbackArgs, File.join(__dir__, "patient_http/callback_args")
44
+ autoload :CallbackValidator, File.join(__dir__, "patient_http/callback_validator")
45
+ autoload :Client, File.join(__dir__, "patient_http/client")
46
+ autoload :ClientError, File.join(__dir__, "patient_http/http_error")
47
+ autoload :ClientPool, File.join(__dir__, "patient_http/client_pool")
48
+ autoload :Configuration, File.join(__dir__, "patient_http/configuration")
49
+ autoload :Encryptor, File.join(__dir__, "patient_http/encryptor")
50
+ autoload :Error, File.join(__dir__, "patient_http/error")
51
+ autoload :ExternalStorage, File.join(__dir__, "patient_http/external_storage")
52
+ autoload :HttpError, File.join(__dir__, "patient_http/http_error")
53
+ autoload :HttpHeaders, File.join(__dir__, "patient_http/http_headers")
54
+ autoload :LifecycleManager, File.join(__dir__, "patient_http/lifecycle_manager")
55
+ autoload :Payload, File.join(__dir__, "patient_http/payload")
56
+ autoload :PayloadStore, File.join(__dir__, "patient_http/payload_store")
57
+ autoload :Processor, File.join(__dir__, "patient_http/processor")
58
+ autoload :ProcessorObserver, File.join(__dir__, "patient_http/processor_observer")
59
+ autoload :RecursiveRedirectError, File.join(__dir__, "patient_http/redirect_error")
60
+ autoload :RedirectError, File.join(__dir__, "patient_http/redirect_error")
61
+ autoload :RedirectHelper, File.join(__dir__, "patient_http/redirect_helper")
62
+ autoload :Request, File.join(__dir__, "patient_http/request")
63
+ autoload :RequestError, File.join(__dir__, "patient_http/request_error")
64
+ autoload :RequestHelper, File.join(__dir__, "patient_http/request_helper")
65
+ autoload :RequestTask, File.join(__dir__, "patient_http/request_task")
66
+ autoload :RequestTemplate, File.join(__dir__, "patient_http/request_template")
67
+ autoload :Response, File.join(__dir__, "patient_http/response")
68
+ autoload :ResponseReader, File.join(__dir__, "patient_http/response_reader")
69
+ autoload :ServerError, File.join(__dir__, "patient_http/http_error")
70
+ autoload :SynchronousExecutor, File.join(__dir__, "patient_http/synchronous_executor")
71
+ autoload :TaskHandler, File.join(__dir__, "patient_http/task_handler")
72
+ autoload :TooManyRedirectsError, File.join(__dir__, "patient_http/redirect_error")
73
+
74
+ @testing = %w[RAILS_ENV RACK_ENV APP_ENV].any? { |var| ENV[var] == "test" }
75
+ @handler = nil
76
+ @handler_mutex = Monitor.new
77
+
78
+ class << self
79
+ # Check if running in testing mode.
80
+ #
81
+ # @api private
82
+ def testing?
83
+ @testing
84
+ end
85
+
86
+ # Set testing mode.
87
+ #
88
+ # @api private
89
+ def testing=(value)
90
+ @testing = !!value
91
+ end
92
+
93
+ # Registers a request handler that will be called to process each request.
94
+ # The handler must be a callable object (responds to `call`) or a block.
95
+ #
96
+ # The handler will receive keyword arguments: request, callback, callback_args,
97
+ # and raise_error_responses. It should return the request id for the enqueued request.
98
+ #
99
+ # @param callable [#call, nil] A callable object that will handle requests.
100
+ # @yield [request, callback, callback_args, raise_error_responses] If a block is given,
101
+ # it will be used as the request handler
102
+ # @raise [ArgumentError] if neither a callable nor a block is provided, or if both are provided
103
+ # @raise [ArgumentError] if the provided callable does not respond to `call`
104
+ # @raise [ArgumentError] if the handler does not support the required keyword arguments
105
+ # @return [#call] the registered handler
106
+ def register_handler(callable = nil, &block)
107
+ raise ArgumentError.new("Must provide a callable object or a block") unless callable || block_given?
108
+ raise ArgumentError.new("Cannot provide both a callable object and a block") if callable && block_given?
109
+
110
+ handler = callable || block
111
+ raise ArgumentError.new("Handler must be a callable object or a block") unless handler.respond_to?(:call)
112
+
113
+ validate_handler_parameters!(handler)
114
+
115
+ @handler_mutex.synchronize { @handler = handler }
116
+ end
117
+
118
+ # Registers a request handler, raising an error if one is already registered.
119
+ #
120
+ # This is a safer alternative to {.register_handler} that prevents accidental
121
+ # double-registration.
122
+ #
123
+ # @param callable [#call, nil] A callable object that will handle requests.
124
+ # @yield [request, callback, callback_args, raise_error_responses] If a block is given,
125
+ # it will be used as the request handler
126
+ # @raise [RuntimeError] if a handler is already registered
127
+ # @raise [ArgumentError] if neither a callable nor a block is provided, or if both are provided
128
+ # @raise [ArgumentError] if the provided callable does not respond to `call`
129
+ # @raise [ArgumentError] if the handler does not support the required keyword arguments
130
+ # @return [#call] the registered handler
131
+ def register_handler!(callable = nil, &block)
132
+ @handler_mutex.synchronize do
133
+ if @handler
134
+ raise "A PatientHttp handler is already registered. Unregister the existing handler before registering a new one."
135
+ end
136
+
137
+ register_handler(callable, &block)
138
+ end
139
+ end
140
+
141
+ # Unregisters the current request handler.
142
+ #
143
+ # @param handler [#call, nil] If provided, only unregisters if the given handler matches
144
+ # the current handler
145
+ # @return [void]
146
+ def unregister_handler(handler = nil)
147
+ @handler_mutex.synchronize do
148
+ @handler = nil if @handler == handler || handler.nil?
149
+ end
150
+ end
151
+
152
+ # Executes the registered request handler with the given request parameters.
153
+ #
154
+ # @param request [Request] the HTTP request to handle
155
+ # @param callback [Class, String] the callback class or name
156
+ # @param callback_args [Hash, nil] JSON-compatible callback arguments
157
+ # @param raise_error_responses [Boolean, nil] when true, non-success responses are
158
+ # reported as errors
159
+ # @raise [RuntimeError] if no handler is registered
160
+ # @return [Object] return value from the registered request handler
161
+ def execute(request:, callback:, callback_args: nil, raise_error_responses: nil)
162
+ handler = @handler_mutex.synchronize { @handler }
163
+
164
+ unless handler
165
+ raise "No request handler registered; you must register a PatientHttp handler before executing requests"
166
+ end
167
+
168
+ handler.call(
169
+ request: request,
170
+ callback: callback,
171
+ callback_args: callback_args,
172
+ raise_error_responses: raise_error_responses
173
+ )
174
+ end
175
+
176
+ # Enqueues an HTTP GET request.
177
+ #
178
+ # @param uri [String] absolute URL
179
+ # @param callback [Class, String] callback class to handle the response
180
+ # @param kwargs [Hash] forwarded to `request`
181
+ # @return [Object] return value from the registered request handler
182
+ def get(uri, callback:, **kwargs)
183
+ request(:get, uri, callback: callback, **kwargs)
184
+ end
185
+
186
+ # Enqueues an HTTP POST request.
187
+ #
188
+ # @param uri [String] absolute URL
189
+ # @param callback [Class, String] callback class to handle the response
190
+ # @param kwargs [Hash] forwarded to `request`
191
+ # @return [Object] return value from the registered request handler
192
+ def post(uri, callback:, **kwargs)
193
+ request(:post, uri, callback: callback, **kwargs)
194
+ end
195
+
196
+ # Enqueues an HTTP PUT request.
197
+ #
198
+ # @param uri [String] absolute URL
199
+ # @param callback [Class, String] callback class to handle the response
200
+ # @param kwargs [Hash] forwarded to `request`
201
+ # @return [Object] return value from the registered request handler
202
+ def put(uri, callback:, **kwargs)
203
+ request(:put, uri, callback: callback, **kwargs)
204
+ end
205
+
206
+ # Enqueues an HTTP PATCH request.
207
+ #
208
+ # @param uri [String] absolute URL
209
+ # @param callback [Class, String] callback class to handle the response
210
+ # @param kwargs [Hash] forwarded to `request`
211
+ # @return [Object] return value from the registered request handler
212
+ def patch(uri, callback:, **kwargs)
213
+ request(:patch, uri, callback: callback, **kwargs)
214
+ end
215
+
216
+ # Enqueues an HTTP DELETE request.
217
+ #
218
+ # @param uri [String] absolute URL
219
+ # @param callback [Class, String] callback class to handle the response
220
+ # @param kwargs [Hash] forwarded to `request`
221
+ # @return [Object] return value from the registered request handler
222
+ def delete(uri, callback:, **kwargs)
223
+ request(:delete, uri, callback: callback, **kwargs)
224
+ end
225
+
226
+ # Builds and dispatches an HTTP request.
227
+ #
228
+ # @param method [Symbol] HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`)
229
+ # @param url [String] absolute URL
230
+ # @param callback [Class, String] callback class to handle the response
231
+ # @param headers [Hash, nil] request headers
232
+ # @param body [String, nil] raw request body
233
+ # @param json [Hash, Array, nil] JSON payload encoded by the request layer
234
+ # @param params [Hash, nil] query parameters
235
+ # @param timeout [Numeric, nil] timeout in seconds for this request
236
+ # @param raise_error_responses [Boolean, nil] when true, non-success responses are
237
+ # reported as errors
238
+ # @param callback_args [Hash, nil] JSON-compatible callback arguments
239
+ # @return [Object] return value from the registered request handler
240
+ def request(
241
+ method,
242
+ url,
243
+ callback:,
244
+ headers: nil,
245
+ body: nil,
246
+ json: nil,
247
+ params: nil,
248
+ timeout: nil,
249
+ raise_error_responses: nil,
250
+ callback_args: nil
251
+ )
252
+ request = Request.new(method, url, body: body, json: json, headers: headers, params: params, timeout: timeout)
253
+ execute(
254
+ request: request,
255
+ callback: callback,
256
+ callback_args: callback_args,
257
+ raise_error_responses: raise_error_responses
258
+ )
259
+ end
260
+
261
+ private
262
+
263
+ # Validates that the handler accepts the required keyword arguments.
264
+ #
265
+ # @param handler [#call] the handler to validate
266
+ # @raise [ArgumentError] if the handler does not support the required keyword arguments
267
+ # @return [void]
268
+ def validate_handler_parameters!(handler)
269
+ required_keywords = %i[request callback callback_args raise_error_responses]
270
+
271
+ # Get the parameters of the handler's call method
272
+ method_obj = handler.is_a?(Proc) ? handler : handler.method(:call)
273
+ params = method_obj.parameters
274
+
275
+ # Check if handler has keyword rest parameter (**kwargs)
276
+ has_keyrest = params.any? { |type, _name| type == :keyrest }
277
+ return if has_keyrest
278
+
279
+ # rubocop:disable Style/HashSlice
280
+ positional_params = params.select { |type, _name| %i[req opt].include?(type) }
281
+ if positional_params.any?
282
+ raise ArgumentError.new(
283
+ "Handler must not accept positional parameters. " \
284
+ "Found: #{positional_params.map { |_type, name| name }.join(", ")}"
285
+ )
286
+ end
287
+
288
+ keyword_params = params.select { |type, _name| %i[keyreq key].include?(type) }
289
+ keyword_names = keyword_params.map { |_type, name| name }
290
+
291
+ missing_keywords = required_keywords - keyword_names
292
+ if missing_keywords.any?
293
+ raise ArgumentError.new(
294
+ "Handler must accept keyword arguments: " \
295
+ "#{required_keywords.map(&:to_s).join(", ")}. " \
296
+ "Missing: #{missing_keywords.map(&:to_s).join(", ")}"
297
+ )
298
+ end
299
+
300
+ required_keyword_names = keyword_params
301
+ .select { |type, _name| type == :keyreq }
302
+ .map { |_type, name| name }
303
+ # rubocop:enable Style/HashSlice
304
+ extra_required_keywords = required_keyword_names - required_keywords
305
+ return unless extra_required_keywords.any?
306
+
307
+ raise ArgumentError.new(
308
+ "Handler must not have extra required keyword parameters. " \
309
+ "Found: #{extra_required_keywords.map(&:to_s).join(", ")}"
310
+ )
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,48 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "patient_http"
3
+ spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
4
+ spec.authors = ["Brian Durand"]
5
+ spec.email = ["bbdurand@gmail.com"]
6
+
7
+ spec.summary = "Generic async HTTP connection pool for Ruby applications using Fiber-based concurrency"
8
+ spec.description = "This gem provides a dedicated async HTTP processor that uses Ruby's Fiber scheduler for non-blocking I/O. Application threads hand off HTTP requests to the processor and return immediately. The processor handles hundreds of concurrent HTTP connections using fibers, then notifies the application when responses arrive via a pluggable callback mechanism. This design keeps application threads free to do other work while HTTP requests are in flight."
9
+
10
+ spec.homepage = "https://github.com/bdurand/patient_http"
11
+ spec.license = "MIT"
12
+
13
+ spec.metadata = {
14
+ "homepage_uri" => spec.homepage,
15
+ "source_code_uri" => spec.homepage,
16
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
17
+ }
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ ignore_files = %w[
22
+ .
23
+ AGENTS.md
24
+ Appraisals
25
+ Gemfile
26
+ Gemfile.lock
27
+ Rakefile
28
+ docker-compose.yml
29
+ bin/
30
+ gemfiles/
31
+ spec/
32
+ test_app/
33
+ ]
34
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
35
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
36
+ end
37
+
38
+ spec.require_paths = ["lib"]
39
+
40
+ spec.required_ruby_version = ">= 3.2"
41
+
42
+ spec.add_dependency "async", "~> 2.0"
43
+ spec.add_dependency "async-http", "~> 0.60"
44
+ spec.add_dependency "concurrent-ruby", "~> 1.2"
45
+ spec.add_dependency "logger"
46
+
47
+ spec.add_development_dependency "bundler"
48
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: patient_http
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: async-http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.60'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.60'
40
+ - !ruby/object:Gem::Dependency
41
+ name: concurrent-ruby
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: logger
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: bundler
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: This gem provides a dedicated async HTTP processor that uses Ruby's Fiber
83
+ scheduler for non-blocking I/O. Application threads hand off HTTP requests to the
84
+ processor and return immediately. The processor handles hundreds of concurrent HTTP
85
+ connections using fibers, then notifies the application when responses arrive via
86
+ a pluggable callback mechanism. This design keeps application threads free to do
87
+ other work while HTTP requests are in flight.
88
+ email:
89
+ - bbdurand@gmail.com
90
+ executables: []
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - ARCHITECTURE.md
95
+ - CHANGELOG.md
96
+ - MIT-LICENSE
97
+ - README.md
98
+ - VERSION
99
+ - db/migrate/20250101000000_create_patient_http_payloads.rb
100
+ - lib/patient_http.rb
101
+ - lib/patient_http/callback_args.rb
102
+ - lib/patient_http/callback_validator.rb
103
+ - lib/patient_http/class_helper.rb
104
+ - lib/patient_http/client.rb
105
+ - lib/patient_http/client_pool.rb
106
+ - lib/patient_http/configuration.rb
107
+ - lib/patient_http/encryptor.rb
108
+ - lib/patient_http/error.rb
109
+ - lib/patient_http/external_storage.rb
110
+ - lib/patient_http/http_error.rb
111
+ - lib/patient_http/http_headers.rb
112
+ - lib/patient_http/lifecycle_manager.rb
113
+ - lib/patient_http/payload.rb
114
+ - lib/patient_http/payload_store.rb
115
+ - lib/patient_http/payload_store/active_record_store.rb
116
+ - lib/patient_http/payload_store/base.rb
117
+ - lib/patient_http/payload_store/file_store.rb
118
+ - lib/patient_http/payload_store/redis_store.rb
119
+ - lib/patient_http/payload_store/s3_store.rb
120
+ - lib/patient_http/processor.rb
121
+ - lib/patient_http/processor_observer.rb
122
+ - lib/patient_http/rails/engine.rb
123
+ - lib/patient_http/redirect_error.rb
124
+ - lib/patient_http/redirect_helper.rb
125
+ - lib/patient_http/request.rb
126
+ - lib/patient_http/request_error.rb
127
+ - lib/patient_http/request_helper.rb
128
+ - lib/patient_http/request_task.rb
129
+ - lib/patient_http/request_template.rb
130
+ - lib/patient_http/response.rb
131
+ - lib/patient_http/response_reader.rb
132
+ - lib/patient_http/synchronous_executor.rb
133
+ - lib/patient_http/task_handler.rb
134
+ - lib/patient_http/time_helper.rb
135
+ - patient_http.gemspec
136
+ homepage: https://github.com/bdurand/patient_http
137
+ licenses:
138
+ - MIT
139
+ metadata:
140
+ homepage_uri: https://github.com/bdurand/patient_http
141
+ source_code_uri: https://github.com/bdurand/patient_http
142
+ changelog_uri: https://github.com/bdurand/patient_http/blob/main/CHANGELOG.md
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '3.2'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 4.0.3
158
+ specification_version: 4
159
+ summary: Generic async HTTP connection pool for Ruby applications using Fiber-based
160
+ concurrency
161
+ test_files: []