patient_http-sidekiq 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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +496 -0
  3. data/CHANGELOG.md +16 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +620 -0
  6. data/VERSION +1 -0
  7. data/lib/patient_http/sidekiq/callback_worker.rb +96 -0
  8. data/lib/patient_http/sidekiq/configuration.rb +175 -0
  9. data/lib/patient_http/sidekiq/context.rb +61 -0
  10. data/lib/patient_http/sidekiq/lifecycle_hooks.rb +42 -0
  11. data/lib/patient_http/sidekiq/processor_observer.rb +49 -0
  12. data/lib/patient_http/sidekiq/request_executor.rb +104 -0
  13. data/lib/patient_http/sidekiq/request_worker.rb +57 -0
  14. data/lib/patient_http/sidekiq/stats.rb +119 -0
  15. data/lib/patient_http/sidekiq/task_handler.rb +81 -0
  16. data/lib/patient_http/sidekiq/task_monitor.rb +542 -0
  17. data/lib/patient_http/sidekiq/task_monitor_thread.rb +154 -0
  18. data/lib/patient_http/sidekiq/web_ui/assets/patient-http/css/patient_http.css +249 -0
  19. data/lib/patient_http/sidekiq/web_ui/locales/ar.yml +26 -0
  20. data/lib/patient_http/sidekiq/web_ui/locales/cs.yml +26 -0
  21. data/lib/patient_http/sidekiq/web_ui/locales/da.yml +26 -0
  22. data/lib/patient_http/sidekiq/web_ui/locales/de.yml +26 -0
  23. data/lib/patient_http/sidekiq/web_ui/locales/el.yml +26 -0
  24. data/lib/patient_http/sidekiq/web_ui/locales/en.yml +26 -0
  25. data/lib/patient_http/sidekiq/web_ui/locales/es.yml +26 -0
  26. data/lib/patient_http/sidekiq/web_ui/locales/fa.yml +26 -0
  27. data/lib/patient_http/sidekiq/web_ui/locales/fr.yml +26 -0
  28. data/lib/patient_http/sidekiq/web_ui/locales/gd.yml +26 -0
  29. data/lib/patient_http/sidekiq/web_ui/locales/he.yml +26 -0
  30. data/lib/patient_http/sidekiq/web_ui/locales/hi.yml +26 -0
  31. data/lib/patient_http/sidekiq/web_ui/locales/it.yml +26 -0
  32. data/lib/patient_http/sidekiq/web_ui/locales/ja.yml +26 -0
  33. data/lib/patient_http/sidekiq/web_ui/locales/ko.yml +26 -0
  34. data/lib/patient_http/sidekiq/web_ui/locales/lt.yml +26 -0
  35. data/lib/patient_http/sidekiq/web_ui/locales/nb.yml +26 -0
  36. data/lib/patient_http/sidekiq/web_ui/locales/nl.yml +26 -0
  37. data/lib/patient_http/sidekiq/web_ui/locales/pl.yml +26 -0
  38. data/lib/patient_http/sidekiq/web_ui/locales/pt-BR.yml +26 -0
  39. data/lib/patient_http/sidekiq/web_ui/locales/pt.yml +26 -0
  40. data/lib/patient_http/sidekiq/web_ui/locales/ru.yml +26 -0
  41. data/lib/patient_http/sidekiq/web_ui/locales/sv.yml +26 -0
  42. data/lib/patient_http/sidekiq/web_ui/locales/ta.yml +26 -0
  43. data/lib/patient_http/sidekiq/web_ui/locales/tr.yml +26 -0
  44. data/lib/patient_http/sidekiq/web_ui/locales/uk.yml +26 -0
  45. data/lib/patient_http/sidekiq/web_ui/locales/ur.yml +26 -0
  46. data/lib/patient_http/sidekiq/web_ui/locales/vi.yml +26 -0
  47. data/lib/patient_http/sidekiq/web_ui/locales/zh-CN.yml +26 -0
  48. data/lib/patient_http/sidekiq/web_ui/locales/zh-TW.yml +26 -0
  49. data/lib/patient_http/sidekiq/web_ui/views/patient_http.html.erb +142 -0
  50. data/lib/patient_http/sidekiq/web_ui.rb +69 -0
  51. data/lib/patient_http/sidekiq.rb +328 -0
  52. data/lib/patient_http-sidekiq.rb +3 -0
  53. data/patient_http-sidekiq.gemspec +46 -0
  54. metadata +140 -0
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "patient_http"
5
+
6
+ # This gem provides a mechanism to offload long-running HTTP requests from Sidekiq workers
7
+ # to a dedicated async I/O processor running in the same process, freeing worker threads
8
+ # immediately while HTTP requests are in flight.
9
+ #
10
+ # == Usage
11
+ #
12
+ # Make HTTP requests from anywhere in your code:
13
+ #
14
+ # request = PatientHttp::Request.new(:get, "https://api.example.com/users/123")
15
+ # PatientHttp::Sidekiq.execute(
16
+ # request,
17
+ # callback: MyCallback,
18
+ # callback_args: {user_id: 123}
19
+ # )
20
+ #
21
+ # Define a callback service class with +on_complete+ and +on_error+ methods:
22
+ #
23
+ # class MyCallback
24
+ # def on_complete(response)
25
+ # user_id = response.callback_args[:user_id]
26
+ # User.find(user_id).update!(data: response.json)
27
+ # end
28
+ #
29
+ # def on_error(error)
30
+ # Rails.logger.error("Request failed: #{error.message}")
31
+ # end
32
+ # end
33
+ #
34
+ # == Key Features
35
+ #
36
+ # - Asynchronous HTTP processing using Ruby's Fiber scheduler
37
+ # - Non-blocking worker threads
38
+ # - Automatic connection pooling and HTTP/2 support
39
+ # - Comprehensive error handling and retry logic
40
+ # - Integration with Sidekiq's job lifecycle
41
+ # - Optional Web UI for monitoring
42
+ #
43
+ # = Singleton Processor Pattern
44
+ #
45
+ # This module maintains a single Processor instance at the module level (@processor).
46
+ # This is an intentional design decision driven by integration requirements with Sidekiq's
47
+ # lifecycle and practical operational considerations:
48
+ #
49
+ # == Rationale:
50
+ #
51
+ # 1. **Sidekiq Integration**: The processor lifecycle (start/quiet/stop) must align with
52
+ # Sidekiq's own lifecycle hooks. A single processor instance integrates cleanly with
53
+ # Sidekiq's startup and shutdown signals.
54
+ #
55
+ # 2. **Resource Management**: Running multiple async I/O reactors in a single process would
56
+ # create resource contention and complexity. A single reactor efficiently handles all
57
+ # HTTP requests using connection pooling and fiber-based concurrency.
58
+ #
59
+ # 3. **Configuration Simplicity**: A singleton processor means one configuration, one set
60
+ # of metrics, and one connection pool. Multiple processors would require complex
61
+ # coordination and resource allocation.
62
+ #
63
+ # 4. **Process Model**: Sidekiq's process model (multiple workers, single process) maps
64
+ # naturally to a single async processor per process. Each Sidekiq process gets one
65
+ # processor, workers within that process share it.
66
+ module PatientHttp
67
+ module Sidekiq
68
+ VERSION = File.read(File.expand_path("../../../VERSION", __FILE__)).strip
69
+
70
+ # Sidekiq-specific autoloads
71
+ autoload :CallbackWorker, File.join(__dir__, "sidekiq/callback_worker")
72
+ autoload :Configuration, File.join(__dir__, "sidekiq/configuration")
73
+ autoload :Context, File.join(__dir__, "sidekiq/context")
74
+ autoload :ProcessorObserver, File.join(__dir__, "sidekiq/processor_observer")
75
+ autoload :RequestExecutor, File.join(__dir__, "sidekiq/request_executor")
76
+ autoload :RequestWorker, File.join(__dir__, "sidekiq/request_worker")
77
+ autoload :LifecycleHooks, File.join(__dir__, "sidekiq/lifecycle_hooks")
78
+ autoload :TaskHandler, File.join(__dir__, "sidekiq/task_handler")
79
+ autoload :Stats, File.join(__dir__, "sidekiq/stats")
80
+ autoload :TaskMonitor, File.join(__dir__, "sidekiq/task_monitor")
81
+ autoload :TaskMonitorThread, File.join(__dir__, "sidekiq/task_monitor_thread")
82
+ autoload :WebUI, File.join(__dir__, "sidekiq/web_ui")
83
+
84
+ @processor = nil
85
+ @configuration = nil
86
+ @after_completion_callbacks = []
87
+ @after_error_callbacks = []
88
+ @external_storage = nil
89
+ @request_handler = nil
90
+
91
+ class << self
92
+ attr_writer :configuration
93
+
94
+ # Configure the gem with a block
95
+ # @yield [Configuration] the configuration object
96
+ # @return [Configuration]
97
+ def configure
98
+ configuration = Configuration.new
99
+ yield(configuration) if block_given?
100
+ @configuration = configuration
101
+ end
102
+
103
+ # Ensure configuration is initialized
104
+ # @return [Configuration]
105
+ def configuration
106
+ @configuration ||= Configuration.new
107
+ end
108
+
109
+ # Reset configuration to defaults (useful for testing)
110
+ # @return [Configuration]
111
+ def reset_configuration!
112
+ @configuration = nil
113
+ configuration
114
+ end
115
+
116
+ # Add a callback to be executed after a successful request completion.
117
+ #
118
+ # @yield [response] block to execute after an HTTP request completes
119
+ # @yieldparam response [PatientHttp::Response] the HTTP response
120
+ def after_completion(&block)
121
+ @after_completion_callbacks << block
122
+ end
123
+
124
+ # Add a callback to be executed after a request error.
125
+ #
126
+ # @yield [error] block to execute after an HTTP request errors
127
+ # @yieldparam error [PatientHttp::Error] information about the error that was raised
128
+ def after_error(&block)
129
+ @after_error_callbacks << block
130
+ end
131
+
132
+ # Add Sidekiq middleware for context handling. The middleware
133
+ # is already added during initialization. You can call this method again to
134
+ # append the middleware if needed to insert it after other middleware. If you need
135
+ # further control, you can manually add the `PatientHttp::Sidekiq::Context::Middleware`
136
+ # middleware yourself.
137
+ #
138
+ # @return [void]
139
+ def append_middleware
140
+ ::Sidekiq.configure_server do |config|
141
+ config.server_middleware do |chain|
142
+ chain.add PatientHttp::Sidekiq::Context::Middleware
143
+ end
144
+ end
145
+ end
146
+
147
+ # Check if the processor is running.
148
+ #
149
+ # @return [Boolean]
150
+ def running?
151
+ !!@processor&.running?
152
+ end
153
+
154
+ # Check if the processor is draining (not accepting new requests
155
+ # but still processing in-flight ones).
156
+ #
157
+ # @return [Boolean]
158
+ def draining?
159
+ !!@processor&.draining?
160
+ end
161
+
162
+ # Check if the processor is in the process of stopping.
163
+ #
164
+ # @return [Boolean]
165
+ def stopping?
166
+ !!@processor&.stopping?
167
+ end
168
+
169
+ # Check if the processor is stopped or has not been started.
170
+ #
171
+ # @return [Boolean]
172
+ def stopped?
173
+ @processor.nil? || @processor.stopped?
174
+ end
175
+
176
+ # Get an ExternalStorage instance for storing and fetching payloads.
177
+ #
178
+ # @return [PatientHttp::ExternalStorage]
179
+ # @api private
180
+ def external_storage
181
+ @external_storage ||= PatientHttp::ExternalStorage.new(configuration)
182
+ end
183
+
184
+ # Encrypt data using the configured encryptor.
185
+ #
186
+ # @param data [Hash] the data to encrypt
187
+ # @return [Hash] the encrypted data (or original if no encryption configured)
188
+ # @api private
189
+ def encrypt(data)
190
+ configuration.encryptor.encrypt(data)
191
+ end
192
+
193
+ # Decrypt data using the configured encryptor.
194
+ #
195
+ # @param data [Hash] the data to decrypt
196
+ # @return [Hash] the decrypted data (or original if not encrypted)
197
+ # @api private
198
+ def decrypt(data)
199
+ configuration.encryptor.decrypt(data)
200
+ end
201
+
202
+ # Execute an async HTTP request.
203
+ #
204
+ # @param request [PatientHttp::Request] the HTTP request to execute
205
+ # @param callback [Class, String] Callback service class with +on_complete+ and +on_error+
206
+ # instance methods, or its fully qualified class name.
207
+ # @param callback_args [#to_h, nil] Arguments to pass to callback via the
208
+ # PatientHttp::Response/PatientHttp::Error object. Must respond to +to_h+ and contain only JSON-native types
209
+ # (nil, true, false, String, Integer, Float, Array, Hash). All hash keys will be
210
+ # converted to strings for serialization. Access via +response.callback_args+ or
211
+ # +error.callback_args+ using symbol or string keys.
212
+ # @param raise_error_responses [Boolean] If true, treats non-2xx responses as errors
213
+ # and calls +on_error+ instead of +on_complete+. Defaults to false.
214
+ # @return [String] the request ID
215
+ def execute(request, callback:, callback_args: nil, raise_error_responses: false)
216
+ PatientHttp::CallbackValidator.validate!(callback)
217
+ callback_name = callback.is_a?(Class) ? callback.name : callback.to_s
218
+ callback_args = PatientHttp::CallbackValidator.validate_callback_args(callback_args)
219
+ request_id = SecureRandom.uuid
220
+
221
+ encrypted = encrypt(request.as_json)
222
+
223
+ data = if external_storage.enabled?
224
+ external_storage.store(encrypted, max_size: configuration.payload_store_threshold)
225
+ else
226
+ encrypted
227
+ end
228
+
229
+ RequestWorker.perform_async(data, callback_name, raise_error_responses, callback_args, request_id)
230
+
231
+ request_id
232
+ end
233
+
234
+ # Start the processor
235
+ #
236
+ # @return [void]
237
+ def start
238
+ return if running?
239
+
240
+ @processor = PatientHttp::Processor.new(configuration)
241
+ @processor.observe(ProcessorObserver.new(@processor))
242
+ configuration.observers.each do |observer|
243
+ @processor.observe(observer)
244
+ end
245
+ @processor.start
246
+
247
+ @request_handler ||= lambda do |request:, callback:, raise_error_responses:, callback_args:|
248
+ execute(
249
+ request,
250
+ callback: callback,
251
+ raise_error_responses: raise_error_responses,
252
+ callback_args: callback_args
253
+ )
254
+ end
255
+
256
+ PatientHttp.register_handler(@request_handler)
257
+ end
258
+
259
+ # Signal the processor to drain (stop accepting new requests)
260
+ #
261
+ # @return [void]
262
+ def quiet
263
+ return unless running?
264
+
265
+ @processor.drain
266
+ end
267
+
268
+ # Stop the processor gracefully
269
+ #
270
+ # @param timeout [Float, nil] maximum time to wait for in-flight requests to complete
271
+ # @return [void]
272
+ def stop(timeout: nil)
273
+ if @request_handler
274
+ PatientHttp.unregister_handler(@request_handler)
275
+ end
276
+
277
+ return unless @processor
278
+
279
+ @processor.stop(timeout: timeout)
280
+ @processor = nil
281
+ end
282
+
283
+ # Reset all state (useful for testing)
284
+ #
285
+ # @return [void]
286
+ # @api private
287
+ def reset!
288
+ @processor&.stop(timeout: 0)
289
+ @processor = nil
290
+ @configuration = nil
291
+ @external_storage = nil
292
+ @after_completion_callbacks = []
293
+ @after_error_callbacks = []
294
+ PatientHttp.unregister_handler(@request_handler) if @request_handler
295
+ end
296
+
297
+ # Invoke the registered completion callbacks
298
+ #
299
+ # @param response [PatientHttp::Response] the HTTP response
300
+ # @return [void]
301
+ # @api private
302
+ def invoke_completion_callbacks(response)
303
+ @after_completion_callbacks.each do |callback|
304
+ callback.call(response)
305
+ end
306
+ end
307
+
308
+ # Invoke the registered error callbacks
309
+ #
310
+ # @param error [PatientHttp::Error] information about the error that was raised
311
+ # @return [void]
312
+ # @api private
313
+ def invoke_error_callbacks(error)
314
+ @after_error_callbacks.each do |callback|
315
+ callback.call(error)
316
+ end
317
+ end
318
+
319
+ # Returns the processor instance (internal accessor)
320
+ #
321
+ # @return [PatientHttp::Processor, nil]
322
+ # @api private
323
+ attr_accessor :processor
324
+ end
325
+ end
326
+
327
+ PatientHttp::Sidekiq::LifecycleHooks.register
328
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "patient_http/sidekiq"
@@ -0,0 +1,46 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "patient_http-sidekiq"
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 = "Offload long-running HTTP requests from Sidekiq workers to a dedicated async I/O processor"
8
+ spec.description = "This gem provides a mechanism to offload long-running HTTP requests from Sidekiq workers to a dedicated async I/O processor running in the same process, freeing the worker thread immediately while the HTTP request is in flight."
9
+
10
+ spec.homepage = "https://github.com/bdurand/patient_http-sidekiq"
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 "sidekiq", ">= 7.0"
43
+ spec.add_dependency "patient_http"
44
+
45
+ spec.add_development_dependency "bundler"
46
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: patient_http-sidekiq
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: sidekiq
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: patient_http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bundler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: This gem provides a mechanism to offload long-running HTTP requests from
55
+ Sidekiq workers to a dedicated async I/O processor running in the same process,
56
+ freeing the worker thread immediately while the HTTP request is in flight.
57
+ email:
58
+ - bbdurand@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ARCHITECTURE.md
64
+ - CHANGELOG.md
65
+ - MIT-LICENSE
66
+ - README.md
67
+ - VERSION
68
+ - lib/patient_http-sidekiq.rb
69
+ - lib/patient_http/sidekiq.rb
70
+ - lib/patient_http/sidekiq/callback_worker.rb
71
+ - lib/patient_http/sidekiq/configuration.rb
72
+ - lib/patient_http/sidekiq/context.rb
73
+ - lib/patient_http/sidekiq/lifecycle_hooks.rb
74
+ - lib/patient_http/sidekiq/processor_observer.rb
75
+ - lib/patient_http/sidekiq/request_executor.rb
76
+ - lib/patient_http/sidekiq/request_worker.rb
77
+ - lib/patient_http/sidekiq/stats.rb
78
+ - lib/patient_http/sidekiq/task_handler.rb
79
+ - lib/patient_http/sidekiq/task_monitor.rb
80
+ - lib/patient_http/sidekiq/task_monitor_thread.rb
81
+ - lib/patient_http/sidekiq/web_ui.rb
82
+ - lib/patient_http/sidekiq/web_ui/assets/patient-http/css/patient_http.css
83
+ - lib/patient_http/sidekiq/web_ui/locales/ar.yml
84
+ - lib/patient_http/sidekiq/web_ui/locales/cs.yml
85
+ - lib/patient_http/sidekiq/web_ui/locales/da.yml
86
+ - lib/patient_http/sidekiq/web_ui/locales/de.yml
87
+ - lib/patient_http/sidekiq/web_ui/locales/el.yml
88
+ - lib/patient_http/sidekiq/web_ui/locales/en.yml
89
+ - lib/patient_http/sidekiq/web_ui/locales/es.yml
90
+ - lib/patient_http/sidekiq/web_ui/locales/fa.yml
91
+ - lib/patient_http/sidekiq/web_ui/locales/fr.yml
92
+ - lib/patient_http/sidekiq/web_ui/locales/gd.yml
93
+ - lib/patient_http/sidekiq/web_ui/locales/he.yml
94
+ - lib/patient_http/sidekiq/web_ui/locales/hi.yml
95
+ - lib/patient_http/sidekiq/web_ui/locales/it.yml
96
+ - lib/patient_http/sidekiq/web_ui/locales/ja.yml
97
+ - lib/patient_http/sidekiq/web_ui/locales/ko.yml
98
+ - lib/patient_http/sidekiq/web_ui/locales/lt.yml
99
+ - lib/patient_http/sidekiq/web_ui/locales/nb.yml
100
+ - lib/patient_http/sidekiq/web_ui/locales/nl.yml
101
+ - lib/patient_http/sidekiq/web_ui/locales/pl.yml
102
+ - lib/patient_http/sidekiq/web_ui/locales/pt-BR.yml
103
+ - lib/patient_http/sidekiq/web_ui/locales/pt.yml
104
+ - lib/patient_http/sidekiq/web_ui/locales/ru.yml
105
+ - lib/patient_http/sidekiq/web_ui/locales/sv.yml
106
+ - lib/patient_http/sidekiq/web_ui/locales/ta.yml
107
+ - lib/patient_http/sidekiq/web_ui/locales/tr.yml
108
+ - lib/patient_http/sidekiq/web_ui/locales/uk.yml
109
+ - lib/patient_http/sidekiq/web_ui/locales/ur.yml
110
+ - lib/patient_http/sidekiq/web_ui/locales/vi.yml
111
+ - lib/patient_http/sidekiq/web_ui/locales/zh-CN.yml
112
+ - lib/patient_http/sidekiq/web_ui/locales/zh-TW.yml
113
+ - lib/patient_http/sidekiq/web_ui/views/patient_http.html.erb
114
+ - patient_http-sidekiq.gemspec
115
+ homepage: https://github.com/bdurand/patient_http-sidekiq
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ homepage_uri: https://github.com/bdurand/patient_http-sidekiq
120
+ source_code_uri: https://github.com/bdurand/patient_http-sidekiq
121
+ changelog_uri: https://github.com/bdurand/patient_http-sidekiq/blob/main/CHANGELOG.md
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '3.2'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 4.0.3
137
+ specification_version: 4
138
+ summary: Offload long-running HTTP requests from Sidekiq workers to a dedicated async
139
+ I/O processor
140
+ test_files: []