patient_http 1.6.1 → 1.8.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +18 -8
  3. data/CHANGELOG.md +43 -0
  4. data/README.md +564 -508
  5. data/VERSION +1 -1
  6. data/lib/patient_http/callback_args.rb +53 -48
  7. data/lib/patient_http/callback_validator.rb +11 -7
  8. data/lib/patient_http/class_helper.rb +6 -7
  9. data/lib/patient_http/client.rb +53 -26
  10. data/lib/patient_http/client_pool.rb +134 -39
  11. data/lib/patient_http/completion_executor.rb +20 -20
  12. data/lib/patient_http/configuration.rb +405 -119
  13. data/lib/patient_http/connection_endpoint.rb +150 -0
  14. data/lib/patient_http/encryptor.rb +28 -18
  15. data/lib/patient_http/error.rb +24 -18
  16. data/lib/patient_http/external_storage.rb +42 -38
  17. data/lib/patient_http/http_error.rb +30 -26
  18. data/lib/patient_http/http_headers.rb +35 -29
  19. data/lib/patient_http/immediate_retries.rb +98 -0
  20. data/lib/patient_http/inline_task_handler.rb +15 -10
  21. data/lib/patient_http/lifecycle_manager.rb +39 -40
  22. data/lib/patient_http/outgoing_request.rb +25 -23
  23. data/lib/patient_http/payload.rb +28 -26
  24. data/lib/patient_http/payload_store/active_record_store.rb +31 -34
  25. data/lib/patient_http/payload_store/base.rb +42 -46
  26. data/lib/patient_http/payload_store/file_store.rb +22 -26
  27. data/lib/patient_http/payload_store/redis_store.rb +28 -34
  28. data/lib/patient_http/payload_store/s3_store.rb +25 -28
  29. data/lib/patient_http/payload_store.rb +2 -0
  30. data/lib/patient_http/processor.rb +111 -79
  31. data/lib/patient_http/processor_observer.rb +65 -59
  32. data/lib/patient_http/rails/engine.rb +13 -8
  33. data/lib/patient_http/redirect_error.rb +50 -41
  34. data/lib/patient_http/redirect_helper.rb +38 -38
  35. data/lib/patient_http/request.rb +117 -46
  36. data/lib/patient_http/request_error.rb +47 -42
  37. data/lib/patient_http/request_helper.rb +178 -119
  38. data/lib/patient_http/request_preparer.rb +13 -10
  39. data/lib/patient_http/request_task.rb +115 -84
  40. data/lib/patient_http/request_template.rb +119 -64
  41. data/lib/patient_http/response.rb +58 -52
  42. data/lib/patient_http/response_reader.rb +166 -118
  43. data/lib/patient_http/secret_manager.rb +34 -30
  44. data/lib/patient_http/secret_reference.rb +33 -26
  45. data/lib/patient_http/synchronous_executor.rb +84 -95
  46. data/lib/patient_http/task_handler.rb +23 -19
  47. data/lib/patient_http/time_helper.rb +8 -8
  48. data/lib/patient_http.rb +327 -186
  49. data/patient_http.gemspec +3 -2
  50. metadata +21 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b3bb6335d47143a8f6e8123be222265651c3e3cee1c8a0084f3aae02b6f6444
4
- data.tar.gz: 9c0a549ff00b9861073fd9853d82a6736c26bf34fca3ac689acf14008968abd5
3
+ metadata.gz: c4e10ada7964f607e81e169e4f88086d3dd32967fd6bf6ed48025615a133d5fd
4
+ data.tar.gz: 704ca986fb7622f6e977d8d02d76e198c98abdd1c5ea808d85c8af25bdd3f72f
5
5
  SHA512:
6
- metadata.gz: c13e5e339088c84e7977ba82c6484780a890076af083d2dbf2754bf1ff0eb05d6a28d585be4d56b13bf5a052d00b643b2c8d44b90a5766fab9cd426c03375830
7
- data.tar.gz: cdf38eeac4823f3cb922001541c3d9425c993cc4ce075ca80ff427d2a79a6d38928999518cd690181aaa8d4064554ea087666a54bb426a829809135dc0c4bf35
6
+ metadata.gz: 442991bea3a29c334866d9136bca7f9ada37d68326b537b31571bae9331f93387e3a617ff970e1433f289fbb078346b33a9394a22afc9eea01e7347233476fe9
7
+ data.tar.gz: baee6575b4bdf5f7798548cc17419d33234f255088bd612d41daec3bceafd56a7a96acb7f86f991d2b6f1a9e31f316100dee6b243e14b8b56831935614b46055
data/ARCHITECTURE.md CHANGED
@@ -20,13 +20,13 @@ The heart of the system - runs in a dedicated thread with its own Fiber reactor.
20
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
21
 
22
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).
23
+ `Request` is a value object representing an HTTP request. `RequestTemplate` provides a builder for creating requests with shared configuration (base URL, headers, timeout).
24
24
 
25
25
  ### RequestTask
26
26
  Wraps a `Request` with execution context: the `TaskHandler`, callback class name, and callback arguments. This is what gets enqueued to the processor.
27
27
 
28
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.
29
+ Value object representing an HTTP response. Includes status, headers, body, and callback arguments. Designed to be serializable for passing through job queues.
30
30
 
31
31
  ### Error Classes
32
32
  Typed error classes (`HttpError`, `RequestError`, `RedirectError`) that are also serializable. Include context about the failed request and callback arguments.
@@ -44,7 +44,7 @@ Manages processor state transitions (stopped → starting → running → draini
44
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
45
 
46
46
  ### ExternalStorage/PayloadStore
47
- Optional external storage for large request/response payloads. Supports file, Redis, S3, and custom adapters.
47
+ Optional external storage for large request/response payloads. Supports file, Redis, S3, ActiveRecord, and custom adapters.
48
48
 
49
49
  ## TaskHandler Pattern
50
50
 
@@ -110,12 +110,13 @@ Example:
110
110
  PatientHttp.register_handler do |request:, callback:, callback_args: nil, raise_error_responses: nil|
111
111
  task = PatientHttp::RequestTask.new(
112
112
  request: request,
113
- task_handler: MyTaskHandler.new,
113
+ task_handler: MyTaskHandler.new(MyJobSystem.current_job_id),
114
114
  callback: callback,
115
115
  callback_args: callback_args,
116
116
  raise_error_responses: raise_error_responses
117
117
  )
118
118
  processor.enqueue(task)
119
+ task.id
119
120
  end
120
121
 
121
122
  # Use in your application code
@@ -205,6 +206,8 @@ erDiagram
205
206
  hash headers
206
207
  string body
207
208
  float timeout
209
+ float read_timeout
210
+ int max_response_size
208
211
  }
209
212
 
210
213
  REQUEST-TASK {
@@ -282,6 +285,12 @@ The processor uses Ruby's Fiber scheduler (`async` gem) for non-blocking I/O:
282
285
  4. **Completion worker threads** decode response bodies (join, inflate, charset), build responses, and execute TaskHandler callbacks and `request_end` observers off the reactor. Decoding is CPU-bound and has no fiber yield point, so running it inline on the reactor would stop every other in-flight request until it finished. On a worker thread the Ruby scheduler can preempt it, and Zlib releases the GVL for part of the inflate
283
286
  5. **Delivery is concurrent, not serialized.** Callbacks and completion-time observers run on any of the `completion_threads` workers, so they must be thread-safe. Completions are also bounded: a task stays in the capacity count until a worker claims its result, so the completion backlog can never exceed `max_connections`
284
287
 
288
+ ## Timeouts and Response Size
289
+
290
+ A request can have two timeouts. The request timeout limits the full exchange. The read timeout limits each wait for data: the wait for the response headers, and then the wait for each body chunk. The first timer starts before the connection opens, so it also covers opening the connection, sending the request, and the immediate retries of a refused connection. Each wait has its own timer, so a slow but steady stream continues until the request timeout stops it. The read timeout raises `ReadTimeoutError`, a subclass of `Async::TimeoutError`. The immediate retries never retry either timeout.
291
+
292
+ The response size limit comes from the request, or from the configuration if the request doesn't set one. The reactor applies the limit to the declared body length before the read starts and to the raw bytes as they arrive. The raw response data that goes to the completion worker includes the limit, so the worker applies the same limit to the inflated body.
293
+
285
294
  ## State Management
286
295
 
287
296
  The processor maintains state through its lifecycle:
@@ -296,8 +305,8 @@ The processor maintains state through its lifecycle:
296
305
 
297
306
  When the processor is stopped with in-flight requests:
298
307
 
299
- 1. The processor stops accepting new requests (drain state)
300
- 2. In-flight requests are given time to complete (configurable timeout)
308
+ 1. The processor stops accepting new requests (stopping state). Call `drain` first to stop accepting requests before the stop begins.
309
+ 2. In-flight requests are given time to complete (`shutdown_timeout`, or the `timeout` passed to `stop`)
301
310
  3. Any requests still pending when the timeout expires trigger `TaskHandler#retry`
302
311
  4. The application's job system can re-enqueue these requests for later processing
303
312
 
@@ -306,7 +315,8 @@ When the processor is stopped with in-flight requests:
306
315
  All behavior is controlled through a central `Configuration` object:
307
316
 
308
317
  - Maximum concurrent connections
309
- - Request timeouts
318
+ - Request timeouts and read timeouts
319
+ - Response size limits
310
320
  - Connection pool settings
311
321
  - Retry policies
312
322
  - Proxy configuration
@@ -327,7 +337,7 @@ For large request/response payloads, the `ExternalStorage` class provides option
327
337
  - **Thread-safe queues**: `Thread::Queue` for request enqueueing
328
338
  - **Atomic operations**: `Concurrent::AtomicReference` for state
329
339
  - **Synchronized access**: Mutexes protect shared data structures
330
- - **Immutable values**: Request/Response are immutable once created
340
+ - **Value objects**: Request and Response have no setters for their attributes
331
341
 
332
342
  ## Further Reading
333
343
 
data/CHANGELOG.md CHANGED
@@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.8.0
8
+
9
+ ### Added
10
+
11
+ - `read_timeout` request option and `Configuration#read_timeout` default (default `nil`, no limit): the maximum number of seconds to wait for data from the server. The first timer starts before the connection opens and covers opening the connection, sending the request, and waiting for the response headers. The timer starts again for each body chunk, so a streamed response that keeps sending data can run longer than the read timeout while a stalled stream fails fast. The request timeout still limits the full exchange. When the read timeout fires, the error is a `PatientHttp::ReadTimeoutError`, a subclass of `Async::TimeoutError`, so `RequestError#error_type` is `:timeout`. It is never retried. Don't use it for requests where the server does all of its work before it sends the headers.
12
+ - `max_response_size` request option: a per-request override of `Configuration#max_response_size`. It applies to the declared body length, to the bytes read, and to the inflated body.
13
+ - `RequestTemplate` and `RequestHelper.request_template` accept `read_timeout` and `max_response_size` defaults. The `RequestHelper` `async_*` methods and the `PatientHttp` request methods accept both options.
14
+ - Both options are serialized with the request only when they are set. Requests serialized by earlier versions load with both options unset.
15
+
16
+ ### Fixed
17
+
18
+ - The response size check before the body is read now works on HTTP/1 connections. The HTTP/1 parser removes `Content-Length` from the response headers, so the check only ran when the header was present, as it is in test stubs. The declared length of the body is now used when the header is missing.
19
+ - The response body is closed when the size check before the read fails. Before, `SynchronousExecutor` left the connection checked out and waited forever when it closed its client pool.
20
+
21
+ ## 1.7.0
22
+
23
+ ### Added
24
+
25
+ - `PatientHttp.configure` is a single entry point for configuration regardless of which job system is in use. When an integration gem (patient_http-sidekiq, patient_http-solid_queue) is loaded it yields that integration's configuration; otherwise it yields a plain `Configuration` used for inline execution. The same object is yielded on every call, so options accumulate and several initializers can each contribute without overwriting one another. Application code no longer has to name the integration to configure it.
26
+ - `PatientHttp.configuration` returns that configuration, creating it on first use and applying any secrets registered with `PatientHttp.register_secret`. There is no longer a boot order to get right.
27
+ - `PatientHttp.register_configuration_provider` lets an integration gem supply the configuration class that `configure` and `configuration` build. The configuration object itself is stored here, so there is exactly one in a process no matter which module it is reached through. `PatientHttp.configuration_provider` returns the registered provider. Loading two integrations in one process warns instead of silently taking the last one.
28
+ - `Configuration#payload_store_threshold` (default 64KB): the size above which a serialized payload is written to the registered payload store. This moves the option next to `register_payload_store`, where the store it applies to is registered. The integration gems inherit it, and their own `payload_store_threshold` accessors continue to work.
29
+ - A request that fails before any response byte arrives is retried at once when the failure is known to be safe to retry: the server refused the request before processing it (`Protocol::HTTP::RefusedError`, raised for an HTTP/2 GOAWAY, a pooled connection that closed after it was acquired, or a refused stream), or the request method is idempotent (`Request#idempotent?`: GET, HEAD, PUT, DELETE, QUERY) and the connection failed. A connection failure evicts the host's pooled client first, so the retry uses a new connection. A request is retried up to `ImmediateRetries::IMMEDIATE_RETRY_LIMIT` times, or `retries - 1` times when the `retries` setting is higher. A POST or PATCH that fails with `EOFError`, `ECONNRESET`, `ECONNABORTED`, `EPIPE`, or `ETIMEDOUT` before a response is not retried, because the server may have processed it.
30
+ - `Configuration#tcp_keepalive` enables TCP keepalive on pooled connections, as an idle time in seconds or a Hash with `:idle`, `:interval`, and `:count`. Keepalive probes keep NAT and firewall mappings alive while a connection is idle and let the kernel detect a dead peer, so the connection is retired before a request is sent on it.
31
+ - `Configuration#tcp_user_timeout` sets `TCP_USER_TIMEOUT` (Linux only) on pooled connections: the seconds transmitted data may stay unacknowledged before the kernel aborts the connection with `ETIMEDOUT`. A request sent on a connection whose peer has silently gone away fails after this long instead of waiting for `request_timeout`. Acknowledged data is not affected, so a slow response is never cut short. `ETIMEDOUT` from the socket is retried at once for idempotent requests like the other connection failures; the request timeout itself is still never retried.
32
+
33
+ ### Changed
34
+
35
+ - `PatientHttp.default_configuration` is now the configuration created by `PatientHttp.configuration` rather than a separate slot an integration had to remember to assign. It still returns nil until a configuration exists, and assigning nil discards the configuration so the next read builds a fresh one. Assigning a configuration still applies module level secrets to it.
36
+ - The separate configuration that was lazily created for inline execution is gone. Inline requests run against `PatientHttp.configuration`, so they see the same secrets, payload stores, and preprocessors as queued requests instead of a parallel set.
37
+ - Retries are applied by patient_http alone. The async-http clients make a single attempt per request, so the async-http retries and the immediate retries no longer stack.
38
+ - Requires async-http 0.99 or later and declares protocol-http 0.66 or later as a direct dependency, since its error classes are referenced directly.
39
+ - **`RequestTemplate` no longer defaults to a 30 second timeout.** A template without a `timeout` leaves the request timeout unset, so the configured `request_timeout` applies. The same is true for `request_template` in `RequestHelper`. Templates that need 30 seconds must now set `timeout: 30`.
40
+ - `PatientHttp.execute_inline` and `SynchronousExecutor` make their connections through a `ClientPool` that lives for the one execution, so inline requests honor `connection_timeout`, `protocol`, `tcp_keepalive`, `tcp_user_timeout`, and the immediate retry rules exactly as processor-backed requests do. Previously the connection timeout capped inline response reads and no immediate retries applied inline.
41
+ - Minimum Ruby version is 3.3.
42
+
43
+ ### Fixed
44
+
45
+ - `RequestTemplate#request` and the `RequestHelper` `async_*` methods accept `max_redirects`, like the `PatientHttp` module methods.
46
+ - `connection_timeout` now bounds only the TCP connect and TLS handshake. It was applied as the socket's IO timeout and so also capped every read and write for the life of the connection, including reads on reused keep-alive connections. A server that paused longer than the connection timeout before or between response bytes failed with `IO::TimeoutError` ("read timeout") even when `request_timeout` allowed more time. Once a connection is established the request timeout alone governs the exchange.
47
+ - `IO::TimeoutError` is classified as a `:timeout` error. It was reported as `:connection` because it inherits from `IOError`.
48
+ - Evicting a host's pooled client no longer stalls the processor. The eviction closed the client while holding the client pool lock, and closing waits for the host's in-flight requests to finish, so one request timing out blocked dispatch of every queued request, to any host, until the slowest in-flight request to that host completed. The client is now removed from the pool immediately and closed in a separate task once its requests finish.
49
+
7
50
  ## 1.6.1
8
51
 
9
52
  ### Fixed