patient_http-sidekiq 1.4.1 → 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e20cb0bf0d79d325dd9a236f246ceec4d9ab8c5cc948208ae33692d8e4d8b3cb
4
- data.tar.gz: e3bb8edde618556e2a05e1f4d4269b1b2a547dd4df3d64d8ac18b5582a4dbdf4
3
+ metadata.gz: 291c7b4b76f26bd5b3b267974b123064c300c0fa47d6ff8a9aec69879e25f219
4
+ data.tar.gz: 8049104ef54e8913af08316018ae9ec81fe9cb9ea29589674c819d39ed78a4e3
5
5
  SHA512:
6
- metadata.gz: ce53d2907c8f793ec468f02f747d9fb09b1386cdbaf47499158bfd5df14807d0d5599736da57b82b4c6d7d25422224fdfb92a338ae43e116479547911128d55d
7
- data.tar.gz: 7e936ff57e3f6e71ee4bea13741d5e361b7d0f640187e33ce7309e08c18f605c5ec870c389cdf34dcbf53b2d97a3975fd543800cece6d30bb2e2ca90a338cd4e
6
+ metadata.gz: 19dd19680806e9e90809fc319e35416229ab52691d3ef2bc15c53137a6bbf971b910974e7fd408708cccbc08b1693a7f1710a12046ca99b1445ae2f64a582d4d
7
+ data.tar.gz: 192aa1d0a65551e1d6e6c243f5a46ac398bd6fae3cd96c2ee920f152d5614b322f224466507a9d7a612b397491b5146743875d4f78674f807114c02f0ce97400
data/ARCHITECTURE.md CHANGED
@@ -7,7 +7,7 @@ PatientHttp::Sidekiq provides a Sidekiq integration layer for the [patient_http]
7
7
  ## Key Design Principles
8
8
 
9
9
  1. **Non-blocking Workers**: Worker threads enqueue HTTP requests via Sidekiq jobs and immediately return, freeing them to process other jobs
10
- 2. **Singleton Processor**: One async I/O processor (from patient_http) per Sidekiq process handles all HTTP requests using Fiber-based concurrency
10
+ 2. **Processor per Profile**: Each Sidekiq process runs one async I/O processor (from patient_http) for each configured processor profile. Most applications use only the default processor.
11
11
  3. **Callback Service Pattern**: HTTP responses are processed by callback service classes with `on_complete` and `on_error` methods, invoked via Sidekiq jobs
12
12
  4. **Lifecycle Integration**: Processor lifecycle is tightly coupled with Sidekiq's startup, quiet, and shutdown events
13
13
  5. **Sidekiq-Native Task Handling**: Request lifecycle operations (enqueueing, callbacks, retries) use Sidekiq's job system
@@ -15,7 +15,7 @@ PatientHttp::Sidekiq provides a Sidekiq integration layer for the [patient_http]
15
15
  ## Core Components
16
16
 
17
17
  ### PatientHttp::Processor (from patient_http gem)
18
- The heart of the system - a singleton that runs in a dedicated thread with its own Fiber reactor. Manages the async HTTP request queue and handles concurrent request execution using Ruby's `async` gem with HTTP/2 connection pooling.
18
+ The heart of the system. Each processor runs in a dedicated thread with its own Fiber reactor. It manages the async HTTP request queue and handles concurrent request execution using Ruby's `async` gem with HTTP/2 connection pooling. `PatientHttp::Sidekiq` starts one processor for each processor profile when the Sidekiq server starts.
19
19
 
20
20
  ### TaskHandler
21
21
  Implements the patient_http's `TaskHandler` interface to integrate with Sidekiq's job system:
@@ -36,10 +36,10 @@ Registers Sidekiq server lifecycle hooks to automatically:
36
36
  - Stop the processor gracefully when Sidekiq shuts down (`:shutdown` event)
37
37
 
38
38
  ### RequestHelper Handler Registration
39
- On startup, the integration automatically registers a handler via `PatientHttp.register_handler` so that classes including the `RequestHelper` module can use `async_get`, `async_post`, etc. The handler translates those calls into `PatientHttp::Sidekiq.execute` invocations. The handler is unregistered on shutdown.
39
+ When the gem loads, it registers a handler with `PatientHttp.register_handler`, so that the `PatientHttp` module methods and the `RequestHelper` `async_*` methods work in every process that loads the gem. The handler translates those calls into `PatientHttp::Sidekiq.execute` invocations. The handler stays registered for the life of the process. After the processors stop, requests are enqueued in Redis for another process to run.
40
40
 
41
41
  ### ProcessorObserver
42
- Observes processor state changes and updates the TaskMonitor's Redis heartbeats, enabling distributed crash recovery.
42
+ Adds each request to the `TaskMonitor` crash-recovery registry when a processor accepts it, and removes it when the request finishes or a Sidekiq job owns the request again. It also records request stats for the Web UI.
43
43
 
44
44
  ### TaskMonitor
45
45
  Manages crash recovery by tracking in-flight requests in Redis:
@@ -53,19 +53,21 @@ Background thread that periodically:
53
53
  - Updates heartbeat timestamps for in-flight requests
54
54
  - Scans for orphaned requests from crashed processes
55
55
  - Performs garbage collection on stale Redis data
56
+ - Publishes this process's capacity and flushes local stats to Redis
56
57
 
57
58
  ### Request/Response/Error (from patient_http gem)
58
- Immutable value objects representing HTTP requests and their results. All are JSON-serializable for passing through Sidekiq jobs.
59
+ Value objects representing HTTP requests and their results. All are JSON-serializable for passing through Sidekiq jobs.
59
60
 
60
61
  ### ExternalStorage (from patient_http gem)
61
- Handles storage and retrieval of large payloads (requests, responses, errors) to Redis or disk when they exceed the payload size threshold, preventing Sidekiq job serialization issues.
62
+ Stores large payloads (requests, responses, errors) in the registered payload store when they exceed `payload_store_threshold`, so that Sidekiq job arguments stay small.
62
63
 
63
64
  ### Configuration
64
- Sidekiq-specific configuration including:
65
- - Callback queue names
66
- - Encryption for sensitive data
67
- - External storage settings
68
- - Integration with patient_http's configuration
65
+ `PatientHttp::Sidekiq::Configuration` extends patient_http's configuration with Sidekiq-specific options, including:
66
+ - Sidekiq job options for `RequestWorker` and `CallbackWorker`
67
+ - Direct execution
68
+ - Crash-recovery heartbeat and orphan intervals
69
+ - In-flight request details for the Web UI
70
+ - Named processor profiles
69
71
 
70
72
  ## Callback Service Pattern
71
73
 
@@ -91,7 +93,7 @@ class FetchDataCallback
91
93
  end
92
94
 
93
95
  # Make a request from anywhere in your code
94
- PatientHttp::Sidekiq.get(
96
+ PatientHttp.get(
95
97
  "https://api.example.com/users/123",
96
98
  callback: FetchDataCallback,
97
99
  callback_args: {user_id: 123}
@@ -111,16 +113,16 @@ sequenceDiagram
111
113
  participant CbWorker as CallbackWorker
112
114
  participant Callback as Callback Service
113
115
 
114
- App->>Module: get(url, callback: MyCallback)
116
+ App->>Module: PatientHttp.get(url, callback: MyCallback)
115
117
 
116
118
  alt Processor running in this process (direct execution)
117
- Module->>Processor: submit(request, handler)
119
+ Module->>Processor: enqueue(task)
118
120
  Note over Module: DirectTaskHandler keeps the<br/>RequestWorker args for re-enqueue
119
121
  Processor-->>Module: Returns immediately
120
122
  else Processor not in this process
121
123
  Module->>Sidekiq: Enqueue RequestWorker
122
124
  Sidekiq->>ReqWorker: Execute job
123
- ReqWorker->>Processor: submit(request, handler)
125
+ ReqWorker->>Processor: enqueue(task)
124
126
  activate Processor
125
127
  Note over Processor: Request queued<br/>in memory
126
128
  Processor-->>ReqWorker: Returns immediately
@@ -246,10 +248,10 @@ erDiagram
246
248
  ## Process Model
247
249
 
248
250
  Each Sidekiq process runs:
249
- - Multiple worker threads (configured via Sidekiq concurrency)
250
- - **One** async HTTP processor thread (from patient_http)
251
- - **One** fiber reactor within the processor thread
252
- - **One** task monitor thread for crash recovery
251
+ - Multiple worker threads (configured with Sidekiq concurrency)
252
+ - **One** async HTTP processor thread (from patient_http) for each processor profile, with one fiber reactor in each
253
+ - Completion worker threads for each processor (`completion_threads`, default 2) that decode responses and deliver results
254
+ - **One** task monitor thread for crash recovery, shared by all processors
253
255
 
254
256
  ```
255
257
  ┌─────────────────────────────────────────────────────────────┐
@@ -307,7 +309,7 @@ Each Sidekiq process runs:
307
309
  ### Architectural Layers
308
310
 
309
311
  **Application Layer:**
310
- - User code calls `PatientHttp::Sidekiq.get/post/etc`
312
+ - User code calls `PatientHttp.get`, `PatientHttp.post`, and the other module methods
311
313
  - Or includes `PatientHttp::RequestHelper` for `async_get/async_post/etc` instance methods
312
314
  - Callback services implement `on_complete` and `on_error`
313
315
 
@@ -345,7 +347,7 @@ The system uses multiple levels of concurrency:
345
347
  - Performs garbage collection on stale data
346
348
 
347
349
  **Benefits:**
348
- 1. **Worker threads remain free** - submitting a request to the processor takes ~1ms
350
+ 1. **Worker threads remain free** - handing a request to the processor returns without waiting for the response
349
351
  2. **Fiber-based multiplexing** - handle hundreds of concurrent requests in a single thread
350
352
  3. **HTTP/2 connection reuse** - multiple requests share persistent connections
351
353
  4. **Non-blocking I/O** - fibers yield during network I/O, allowing other requests to progress
@@ -398,9 +400,12 @@ Recovery gives at-least-once delivery. A crash between a re-enqueue and the remo
398
400
  **Redis Keys:**
399
401
  - `sidekiq:patient_http:inflight_index` - Sorted set of request IDs by timestamp
400
402
  - `sidekiq:patient_http:inflight_jobs` - Hash of request payloads
403
+ - `sidekiq:patient_http:inflight_details` - Hash of the URL, HTTP method, and processor of each in-flight request, for the Web UI
404
+ - `sidekiq:patient_http:inflight_details_index` - Sorted set of the in-flight request details by start time
401
405
  - `sidekiq:patient_http:processes` - Set of active process IDs
402
406
  - `sidekiq:patient_http:gc_lock` - Distributed lock for garbage collection
403
407
  - `sidekiq:patient_http:gc_last_run` - Timestamp of last garbage collection run
408
+ - `sidekiq:patient_http:totals` - Aggregated request stats for the Web UI
404
409
 
405
410
  ## Configuration
406
411
 
@@ -408,7 +413,7 @@ Configuration is split between Sidekiq-specific concerns and patient_http settin
408
413
 
409
414
  ### Sidekiq Integration Settings
410
415
  ```ruby
411
- PatientHttp::Sidekiq.configure do |config|
416
+ PatientHttp.configure do |config|
412
417
  # Sidekiq worker options (applied to both RequestWorker and CallbackWorker)
413
418
  config.sidekiq_options = {queue: "patient_http", retry: 5}
414
419
 
@@ -421,15 +426,15 @@ PatientHttp::Sidekiq.configure do |config|
421
426
  # External storage threshold (for large payloads)
422
427
  config.payload_store_threshold = 100_000 # bytes
423
428
 
424
- # Shutdown timeout
425
- config.shutdown_timeout = 30 # seconds
429
+ # Shutdown timeout in seconds. Must be less than Sidekiq's shutdown timeout.
430
+ config.shutdown_timeout = 23
426
431
  end
427
432
  ```
428
433
 
429
434
  ### Async HTTP Pool Settings (delegated)
430
435
  All patient_http configuration is accessible:
431
436
  ```ruby
432
- PatientHttp::Sidekiq.configure do |config|
437
+ PatientHttp.configure do |config|
433
438
  # HTTP settings
434
439
  config.request_timeout = 30
435
440
  config.max_connections = 100
@@ -442,22 +447,21 @@ PatientHttp::Sidekiq.configure do |config|
442
447
  end
443
448
  ```
444
449
 
445
- The configuration object is passed to the `PatientHttp::Processor` on startup.
450
+ The configuration object is passed to each `PatientHttp::Processor` on startup. A named processor profile gets a view of the configuration with its own overrides applied.
446
451
 
447
452
  ## Web UI
448
453
 
449
- Optional Sidekiq Web integration (via `WebUI` module) provides:
454
+ Optional Sidekiq Web integration (the `WebUI` module) provides:
450
455
 
451
- - Real-time processor state (running/draining/stopped)
452
- - In-flight request counts by process
453
- - Historical statistics from patient_http metrics
454
- - Health indicators
455
- - Max connection capacity per process
456
+ - Total requests, errors, times at capacity, average duration, and capacity utilization
457
+ - The same stats for each processor, with the in-flight high-water mark, when more than one processor profile is configured
458
+ - The oldest in-flight requests, with their URL, HTTP method, processor, and age
459
+ - In-flight request counts and capacity for each process
456
460
 
457
461
  The Web UI reads from:
458
- - `TaskMonitor` Redis keys for inflight counts
459
- - `PatientHttp::Processor` stats for metrics
460
- - Process registry for distributed monitoring
462
+ - `Stats` Redis keys for request totals
463
+ - `TaskMonitor` Redis keys for in-flight requests and details
464
+ - The process set, where each process publishes its capacity with its heartbeat
461
465
 
462
466
  ## Data Flow
463
467
 
@@ -465,15 +469,17 @@ The Web UI reads from:
465
469
 
466
470
  ```
467
471
  Application Code
468
- ↓ PatientHttp::Sidekiq.get(url, callback: MyCallback)
472
+ ↓ PatientHttp.get(url, callback: MyCallback)
469
473
  RequestWorker job enqueued
470
474
  ↓ Sidekiq processes job
471
475
  RequestWorker#perform
472
- ↓ Creates Request, TaskHandler
473
- PatientHttp::Processor.submit(request, handler)
474
- ↓ Queued in memory
475
- TaskMonitor.track(request_id, handler)
476
- ↓ Stored in Redis
476
+ ↓ Creates RequestTask with a TaskHandler
477
+ PatientHttp::Processor#enqueue(task)
478
+ ↓ ProcessorObserver#request_enqueued
479
+ TaskMonitor#register
480
+ ↓ Stored in Redis before the task is queued
481
+ Task queued in memory
482
+ ↓
477
483
  Fiber reactor processes request
478
484
  ↓ Non-blocking HTTP I/O
479
485
  Response/Error received
@@ -494,14 +500,16 @@ Fiber completes with Response
494
500
  TaskHandler#on_complete(response, callback)
495
501
  ↓ Stores via ExternalStorage if large
496
502
  CallbackWorker job enqueued
497
- ↓ Sidekiq processes job
503
+ ↓ ProcessorObserver#request_end
504
+ TaskMonitor#unregister
505
+ ↓ Removed from Redis
506
+ Sidekiq processes CallbackWorker job
507
+ ↓
498
508
  CallbackWorker#perform
499
509
  ↓ Fetches from ExternalStorage if needed
500
510
  ↓ Decrypts payload
501
511
  MyCallback.new.on_complete(response)
502
512
  ↓ User code executes
503
- TaskMonitor.untrack(request_id)
504
- ↓ Removed from Redis
505
513
  ```
506
514
 
507
515
  ## Thread Safety
@@ -509,8 +517,7 @@ TaskMonitor.untrack(request_id)
509
517
  - **Thread-safe submission**: `Processor` uses thread-safe queues for request submission
510
518
  - **Atomic state changes**: Processor state managed with atomic operations
511
519
  - **Redis-based coordination**: TaskMonitor uses Redis for distributed coordination
512
- - **Immutable values**: Request/Response/Error objects are immutable once created
513
- - **Sidekiq job isolation**: Each CallbackWorker runs in its own thread
520
+ - **Sidekiq job isolation**: Each CallbackWorker job runs on a Sidekiq worker thread
514
521
 
515
522
  ## Further Reading
516
523
 
data/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ 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.5.0
8
+
9
+ ### Added
10
+
11
+ - Install generator: `rails generate patient_http:sidekiq:install` writes a commented `config/initializers/patient_http.rb` covering the options applications actually set. It is a convenience, not an install step, since the gem works with no initializer at all.
12
+
13
+ ### Changed
14
+
15
+ - **No setup call is required.** The request handler is registered when the gem is loaded, so `PatientHttp.get` and the other request methods work in every process that requires the gem, configured or not. Previously `PatientHttp::Sidekiq.configure` or `register_handler` had to be called first or requests raised, which made a missing or unloaded initializer fail at dispatch time.
16
+ - `PatientHttp.configure` and `PatientHttp.configuration` resolve to this gem's configuration, so application code never has to name the integration to configure it. `PatientHttp::Sidekiq.configure` remains equivalent.
17
+ - **`configure` now accumulates instead of replacing.** It yields the one configuration object for the process rather than building a new one each time, so several initializers can each contribute options and a second call no longer discards what an earlier one registered. Code that relied on `configure` resetting the configuration should call `reset_configuration!` first.
18
+ - The configuration is created on first use and published to `PatientHttp` at that moment. Previously it was published only inside `configure`, so a process that started a processor without calling `configure` never received module level secrets registered with `PatientHttp.register_secret`.
19
+ - The request handler stays registered for the life of the process. `stop` no longer unregisters it, so a request made while the process is shutting down is enqueued to Redis for another process to run instead of raising.
20
+ - The default `shutdown_timeout` and `logger` are read from Sidekiq when they're used instead of when the configuration is built, so Sidekiq settings made in a later initializer, such as a `Sidekiq.configure_server` block, apply.
21
+ - `payload_store_threshold` moved to `PatientHttp::Configuration`, next to `register_payload_store`. It is inherited, so `config.payload_store_threshold` is unchanged. `PatientHttp::Sidekiq::Configuration::DEFAULT_PAYLOAD_STORE_THRESHOLD` now points at the base gem's constant and is deprecated.
22
+ - A request that names a processor profile that isn't declared in the process making the request raises `PatientHttp::UnknownProcessorError` instead of being enqueued. The profile's `raise_error_responses` and `payload_store_threshold` options are applied when the request is enqueued, so a request enqueued from a process without the profile used the base options. Declare processor profiles in every process that makes requests.
23
+ - `config.processor(name)` with no options declares the profile with every option taken from the base configuration, and declaring a profile again replaces its options. Previously a call with no options returned the profile's options and declared nothing. Use `config.processor_options(name)` to read a profile's options.
24
+ - Minimum Ruby version is 3.3.
25
+
26
+ ### Fixed
27
+
28
+ - `config.raise_error_responses = true` now applies to requests made through the `PatientHttp` module methods. Those methods pass `nil` when the caller does not ask for a specific behavior, and `execute` treated `nil` as `false` instead of falling back to the configuration, so a non-2xx response was delivered to `on_complete` rather than `on_error`. An explicit `raise_error_responses:` argument still wins. Jobs already enqueued without the option also fall back to the configuration of the processor that runs them.
29
+ - A processor profile's `payload_store_threshold` override applies to requests routed to that profile. Previously the request payload and the callback result used the base configuration's `payload_store_threshold`, whatever processor ran the request.
30
+
7
31
  ## 1.4.1
8
32
 
9
33
  ### Fixed