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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c5cda06fbc0db9bc2beb2264a718afb1dedc8c2b27fee4f8e8bd9c417086a6b2
4
+ data.tar.gz: edaa46086b479884d618fc144aeb5bd231196424df7061c71fa242c61de4a564
5
+ SHA512:
6
+ metadata.gz: 617a2dbbdb9df31eb19f9b48d852cfb15f1c43ed26e4d552f0674fcced748c88b90cbbc7d47e7b8e2a0b7b88b8b4285c3cbca5dd09fec6fbee91e6e7b050378c
7
+ data.tar.gz: e8ea8de4be59c91cb686b1e4249d57a5c4dc50bc1ccf94e6c0dfd8a091d9b3ceb94544eb580924578d845b074d7a478fddce8a1e694706a814e62bd3d03fd604
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,322 @@
1
+ # Architecture
2
+
3
+ ## Overview
4
+
5
+ PatientHttp provides a mechanism to offload HTTP requests from application threads to a dedicated async I/O processor. The gem uses Ruby's Fiber-based concurrency to handle hundreds of concurrent HTTP requests without blocking application threads.
6
+
7
+ ## Key Design Principles
8
+
9
+ 1. **Non-blocking Threads**: Application threads enqueue HTTP requests and immediately return, freeing them to do other work
10
+ 2. **Fiber-based Concurrency**: A dedicated processor thread uses the `async` gem to multiplex hundreds of concurrent HTTP connections
11
+ 3. **Callback Pattern**: HTTP responses are delivered via a pluggable `TaskHandler` that integrates with any job system
12
+ 4. **Serializable Objects**: Response and error objects are designed to be serialized and passed through job queues
13
+
14
+ ## Core Components
15
+
16
+ ### Processor
17
+ The heart of the system - runs in a dedicated thread with its own Fiber reactor. Manages the async HTTP request queue and handles concurrent request execution using the `async` gem.
18
+
19
+ ### TaskHandler
20
+ Abstract base class that defines the integration point between the pool and your application. Implementations handle completion callbacks, error callbacks, and job retry operations. Concrete implementations (such as those in the `patient_http-sidekiq` or `patient_http-solid_queue` gems) are responsible for using `Configuration#encryptor` to encrypt serialized `Response`/`Error` data before passing it to the job queue. The base class itself does not call encrypt or decrypt; that responsibility belongs to the implementation. This abstraction allows the pool to work with any job system (Sidekiq, SolidQueue, custom queues, etc.).
21
+
22
+ ### Request/RequestTemplate
23
+ `Request` is an immutable value object representing an HTTP request. `RequestTemplate` provides a builder for creating requests with shared configuration (base URL, headers, timeout).
24
+
25
+ ### RequestTask
26
+ Wraps a `Request` with execution context: the `TaskHandler`, callback class name, and callback arguments. This is what gets enqueued to the processor.
27
+
28
+ ### Response
29
+ Immutable value object representing an HTTP response. Includes status, headers, body, and callback arguments. Designed to be serializable for passing through job queues.
30
+
31
+ ### Error Classes
32
+ Typed error classes (`HttpError`, `RequestError`, `RedirectError`) that are also serializable. Include context about the failed request and callback arguments.
33
+
34
+ ### RequestHelper
35
+ A mixin module that provides a simplified interface for making async HTTP requests. Allows applications to use the same request interface while swapping out the underlying queueing mechanism. By registering a custom handler, applications can integrate with any job queue system (Sidekiq, Solid Queue, etc.) without changing request-making code. This decouples the request interface from the async processing infrastructure.
36
+
37
+ ### Client/ClientPool
38
+ Internal HTTP client that handles connection pooling, HTTP/2 support, and request execution within the Fiber reactor.
39
+
40
+ ### LifecycleManager
41
+ Manages processor state transitions (stopped → starting → running → draining → stopping) with thread-safe state machines.
42
+
43
+ ### Encryptor
44
+ Handles encryption and decryption of serialized payloads at the job queue boundary. Wraps user-provided encryption/decryption callables (which operate on raw bytes) with JSON serialization and Base64 encoding. Instantiated from `Configuration#encryptor`. The `Encryptor` is a helper: concrete `TaskHandler` implementations are responsible for calling `encryptor.encrypt`/`encryptor.decrypt` at every serialization boundary. Encrypted data is enveloped as `{"__encrypted__" => true, "value" => "<base64>"}` to allow transparent no-op pass-through when no encryption is configured.
45
+
46
+ ### ExternalStorage/PayloadStore
47
+ Optional external storage for large request/response payloads. Supports file, Redis, S3, and custom adapters.
48
+
49
+ ## TaskHandler Pattern
50
+
51
+ The `TaskHandler` abstract class defines how the processor communicates results back to your application:
52
+
53
+ - **on_complete(response, callback)**: Called when an HTTP request succeeds. Your implementation should enqueue the response for processing (e.g., via a background job).
54
+ - **on_error(error, callback)**: Called when an HTTP request fails. Your implementation should enqueue the error for handling.
55
+ - **retry**: Called when the processor shuts down with in-flight requests. Your implementation should re-enqueue the original job.
56
+
57
+ > **Important:** TaskHandler callbacks run on the processor's reactor thread. They should be lightweight and fast -- typically just enqueuing a message for another system to pick up. Doing heavy processing in a callback will block the reactor and delay other in-flight requests.
58
+
59
+ Example:
60
+ ```ruby
61
+ class MyTaskHandler < PatientHttp::TaskHandler
62
+ def initialize(job_id)
63
+ @job_id = job_id
64
+ end
65
+
66
+ def on_complete(response, callback)
67
+ MyJobSystem.enqueue(callback, :on_complete, response.as_json)
68
+ end
69
+
70
+ def on_error(error, callback)
71
+ MyJobSystem.enqueue(callback, :on_error, error.as_json)
72
+ end
73
+
74
+ def retry
75
+ MyJobSystem.enqueue_job(@job_id)
76
+ end
77
+ end
78
+
79
+ # Enqueue a request
80
+ task = PatientHttp::RequestTask.new(
81
+ request: PatientHttp::Request.new(:get, "https://api.example.com/data"),
82
+ task_handler: MyTaskHandler.new("job-123"),
83
+ callback: "ProcessDataCallback",
84
+ callback_args: {user_id: 123}
85
+ )
86
+ processor.enqueue(task)
87
+ ```
88
+
89
+ ## RequestHelper Integration Pattern
90
+
91
+ The `RequestHelper` module provides an alternative, higher-level API for making async HTTP requests. Instead of manually building `RequestTask` objects and enqueuing them to a processor, you can include the module and use convenience methods.
92
+
93
+ Key benefits:
94
+ - **Interface stability**: Application code making HTTP requests stays the same even when changing job queue systems
95
+ - **Reduced boilerplate**: No need to manually construct `Request` and `RequestTask` objects
96
+ - **Handler abstraction**: The registered handler encapsulates the processor/job queue integration
97
+
98
+ Example:
99
+ ```ruby
100
+ # Register a handler once (typically in an initializer)
101
+ PatientHttp.register_handler do |request:, callback:, callback_args: nil, raise_error_responses: nil|
102
+ task = PatientHttp::RequestTask.new(
103
+ request: request,
104
+ task_handler: MyTaskHandler.new,
105
+ callback: callback,
106
+ callback_args: callback_args,
107
+ raise_error_responses: raise_error_responses
108
+ )
109
+ processor.enqueue(task)
110
+ end
111
+
112
+ # Use in your application code
113
+ class ApiClient
114
+ include PatientHttp::RequestHelper
115
+
116
+ request_template(
117
+ base_url: "https://api.example.com",
118
+ headers: {"Authorization" => "Bearer token"},
119
+ timeout: 60
120
+ )
121
+
122
+ def fetch_user(user_id)
123
+ async_get(
124
+ "/users/#{user_id}",
125
+ callback: FetchUserCallback,
126
+ callback_args: {user_id: user_id}
127
+ )
128
+ end
129
+ end
130
+ ```
131
+
132
+ The `RequestHelper` delegates to the registered handler, passing the request details as keyword arguments. The handler translates these into whatever format your job system needs. This allows you to:
133
+ - Switch from Sidekiq to Solid Queue without changing `ApiClient`
134
+ - Use different queue systems in different environments (inline processing in tests, background jobs in production)
135
+ - Test request logic independently of the queue mechanism
136
+
137
+ ## Request Lifecycle
138
+
139
+ ```mermaid
140
+ sequenceDiagram
141
+ participant App as Application Code
142
+ participant Processor as Async Processor
143
+ participant Handler as TaskHandler
144
+ participant Callback as Callback Service
145
+
146
+ App->>Processor: enqueue(task)
147
+ activate Processor
148
+ Note over Processor: Task stored<br/>in queue
149
+ Processor-->>App: Returns immediately
150
+
151
+ Note over App: Application thread free<br/>to do other work
152
+
153
+ Processor->>Processor: Fiber reactor<br/>dequeues task
154
+ Processor->>Processor: Execute HTTP request<br/>(non-blocking)
155
+
156
+ alt HTTP Request Completes
157
+ Processor->>Handler: on_complete(response, callback)
158
+ Handler->>Handler: Enqueue callback job
159
+ Note over Callback: Job system invokes callback
160
+ Callback->>Callback: Process response
161
+ else Error Raised
162
+ Processor->>Handler: on_error(error, callback)
163
+ Handler->>Handler: Enqueue error job
164
+ Note over Callback: Job system invokes callback
165
+ Callback->>Callback: Handle error
166
+ end
167
+ deactivate Processor
168
+ ```
169
+
170
+ ## Component Relationships
171
+
172
+ ```mermaid
173
+ erDiagram
174
+ PROCESSOR ||--o{ REQUEST-TASK : "manages queue of"
175
+ PROCESSOR ||--|| LIFECYCLE-MANAGER : "state managed by"
176
+ PROCESSOR ||--|| CLIENT : "uses"
177
+ PROCESSOR ||--|| CONFIGURATION : "configured by"
178
+
179
+ REQUEST-TASK ||--|| REQUEST : "contains"
180
+ REQUEST-TASK ||--|| TASK-HANDLER : "uses"
181
+ REQUEST-TASK ||--|| RESPONSE : "yields"
182
+
183
+ TASK-HANDLER ||--|| CALLBACK-SERVICE : "invokes"
184
+
185
+ EXTERNAL-STORAGE ||--o{ PAYLOAD-STORE : "uses"
186
+
187
+ PROCESSOR {
188
+ string state
189
+ int queue_size
190
+ thread reactor_thread
191
+ }
192
+
193
+ REQUEST {
194
+ string http_method
195
+ string url
196
+ hash headers
197
+ string body
198
+ float timeout
199
+ }
200
+
201
+ REQUEST-TASK {
202
+ Request request
203
+ TaskHandler task_handler
204
+ string callback
205
+ hash callback_args
206
+ }
207
+
208
+ RESPONSE {
209
+ int status
210
+ hash headers
211
+ string body
212
+ string http_method
213
+ string url
214
+ hash callback_args
215
+ }
216
+
217
+ TASK-HANDLER {
218
+ method on_complete
219
+ method on_error
220
+ method retry
221
+ }
222
+
223
+ LIFECYCLE-MANAGER {
224
+ string state
225
+ method start
226
+ method stop
227
+ method drain
228
+ }
229
+ ```
230
+
231
+ ## Process Model
232
+
233
+ Each application process can run:
234
+ - Multiple application threads
235
+ - **One** async HTTP processor thread
236
+ - **One** fiber reactor within the processor thread
237
+
238
+ ```
239
+ ┌─────────────────────────────────────────────────────────────┐
240
+ │ Application Process │
241
+ │ │
242
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
243
+ │ │ Application │ │ Application │ │ Application │ │
244
+ │ │ Thread 1 │ │ Thread 2 │ │ Thread N │ │
245
+ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
246
+ │ │ │ │ │
247
+ │ └──────────────────┼─────────────────┘ │
248
+ │ │ │
249
+ │ ▼ │
250
+ │ ┌─────────────────────────┐ │
251
+ │ │ Async HTTP Processor │ │
252
+ │ │ (Dedicated Thread) │ │
253
+ │ │ │ │
254
+ │ │ ┌───────────────────┐ │ │
255
+ │ │ │ Fiber Reactor │ │ │
256
+ │ │ │ ═════════════ │ │ │
257
+ │ │ │ 100+ concurrent │ │ │
258
+ │ │ │ HTTP requests │ │ │
259
+ │ │ └───────────────────┘ │ │
260
+ │ └─────────────────────────┘ │
261
+ └─────────────────────────────────────────────────────────────┘
262
+ ```
263
+
264
+ ## Concurrency Model
265
+
266
+ The processor uses Ruby's Fiber scheduler (`async` gem) for non-blocking I/O:
267
+
268
+ 1. **Application threads** remain free while HTTP requests execute
269
+ 2. **Fiber reactor** multiplexes hundreds of HTTP connections
270
+ 3. **Connection pooling** and HTTP/2 reuse connections efficiently
271
+ 4. **TaskHandler callbacks** execute on the reactor thread and should be lightweight
272
+
273
+ ## State Management
274
+
275
+ The processor maintains state through its lifecycle:
276
+
277
+ - **stopped**: Initial state, not processing requests
278
+ - **starting**: Processor is initializing, reactor thread launching
279
+ - **running**: Actively processing requests
280
+ - **draining**: Not accepting new requests, completing in-flight
281
+ - **stopping**: Shutting down, waiting for requests to finish
282
+
283
+ ## Graceful Shutdown
284
+
285
+ When the processor is stopped with in-flight requests:
286
+
287
+ 1. The processor stops accepting new requests (drain state)
288
+ 2. In-flight requests are given time to complete (configurable timeout)
289
+ 3. Any requests still pending when the timeout expires trigger `TaskHandler#retry`
290
+ 4. The application's job system can re-enqueue these requests for later processing
291
+
292
+ ## Configuration
293
+
294
+ All behavior is controlled through a central `Configuration` object:
295
+
296
+ - Maximum concurrent connections
297
+ - Request timeouts
298
+ - Connection pool settings
299
+ - Retry policies
300
+ - Proxy configuration
301
+ - Logging
302
+ - Payload stores for external storage
303
+
304
+ ## External Storage
305
+
306
+ For large request/response payloads, the `ExternalStorage` class provides optional external storage:
307
+
308
+ - **PayloadStore adapters**: File, Redis, S3, ActiveRecord, or custom implementations
309
+ - **Automatic threshold**: Payloads exceeding a size limit are stored externally
310
+ - **Reference-based**: Stored payloads are replaced with lightweight references
311
+ - **On-demand fetch**: Original payloads are fetched when needed
312
+
313
+ ## Thread Safety
314
+
315
+ - **Thread-safe queues**: `Thread::Queue` for request enqueueing
316
+ - **Atomic operations**: `Concurrent::AtomicReference` for state
317
+ - **Synchronized access**: Mutexes protect shared data structures
318
+ - **Immutable values**: Request/Response are immutable once created
319
+
320
+ ## Further Reading
321
+
322
+ - [README](README.md)
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 1.0.0
8
+
9
+ ### Added
10
+
11
+ - Async HTTP processor that runs in a dedicated thread with a Fiber-based reactor, allowing hundreds of concurrent HTTP requests without blocking application threads.
12
+ - Pluggable `TaskHandler` interface for integrating with any job system or application framework.
13
+ - Callback system with `on_complete` and `on_error` handlers for processing HTTP responses and errors.
14
+ - `Request` and `RequestTemplate` classes for building HTTP requests with support for all HTTP methods (GET, POST, PUT, PATCH, DELETE).
15
+ - `RequestTemplate` for repeated requests to the same API with shared configuration (base URL, headers, timeouts).
16
+ - JSON-serializable `Response` and error objects for safe passing through job queues and across process boundaries.
17
+ - Automatic redirect following with configurable maximum redirects.
18
+ - HTTP/2 support via the async-http gem.
19
+ - Connection pooling with configurable pool size for efficient reuse of connections across hosts.
20
+ - External payload storage system with adapters for File, Redis, and S3 to handle large request/response payloads.
21
+ - Configurable response size limits to bound memory usage.
22
+ - Proxy support for HTTP/HTTPS proxies with authentication.
23
+ - Automatic retry support for failed requests.
24
+ - Graceful shutdown with configurable timeout and automatic retry of incomplete requests via `TaskHandler#retry`.
25
+ - `ProcessorObserver` interface for monitoring processor events (request start/end, errors, capacity exceeded).
26
+ - `SynchronousExecutor` for testing without starting the async processor.
27
+ - Configurable connection limits, timeouts, response size limits, and User-Agent headers.
28
+ - Typed error classes (`HttpError`, `ClientError`, `ServerError`, `RedirectError`, `RequestError`) for precise error handling.
29
+ - Optional treatment of non-2xx HTTP responses as errors via `raise_error_responses` configuration.
30
+ - `CallbackArgs` for passing custom data through the request/response cycle.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2026 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.