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 +4 -4
- data/ARCHITECTURE.md +53 -46
- data/CHANGELOG.md +24 -0
- data/README.md +286 -282
- data/VERSION +1 -1
- data/lib/generators/patient_http/sidekiq/install_generator.rb +42 -0
- data/lib/generators/patient_http/sidekiq/templates/initializer.rb +96 -0
- data/lib/patient_http/sidekiq/callback_worker.rb +16 -13
- data/lib/patient_http/sidekiq/configuration.rb +261 -153
- data/lib/patient_http/sidekiq/context.rb +21 -14
- data/lib/patient_http/sidekiq/direct_task_handler.rb +26 -20
- data/lib/patient_http/sidekiq/lifecycle_hooks.rb +13 -9
- data/lib/patient_http/sidekiq/processor_observer.rb +94 -50
- data/lib/patient_http/sidekiq/redis_pool.rb +33 -28
- data/lib/patient_http/sidekiq/request_executor.rb +54 -37
- data/lib/patient_http/sidekiq/request_worker.rb +23 -26
- data/lib/patient_http/sidekiq/stats.rb +79 -54
- data/lib/patient_http/sidekiq/task_handler.rb +49 -36
- data/lib/patient_http/sidekiq/task_monitor.rb +248 -215
- data/lib/patient_http/sidekiq/task_monitor_thread.rb +25 -21
- data/lib/patient_http/sidekiq/web.rb +2 -2
- data/lib/patient_http/sidekiq/web_ui.rb +31 -17
- data/lib/patient_http/sidekiq.rb +417 -255
- data/patient_http-sidekiq.gemspec +2 -2
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 291c7b4b76f26bd5b3b267974b123064c300c0fa47d6ff8a9aec69879e25f219
|
|
4
|
+
data.tar.gz: 8049104ef54e8913af08316018ae9ec81fe9cb9ea29589674c819d39ed78a4e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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. **
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
250
|
-
- **One** async HTTP processor thread (from patient_http)
|
|
251
|
-
-
|
|
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
|
|
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** -
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
454
|
+
Optional Sidekiq Web integration (the `WebUI` module) provides:
|
|
450
455
|
|
|
451
|
-
-
|
|
452
|
-
-
|
|
453
|
-
-
|
|
454
|
-
-
|
|
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
|
-
- `
|
|
459
|
-
- `
|
|
460
|
-
-
|
|
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
|
|
472
|
+
↓ PatientHttp.get(url, callback: MyCallback)
|
|
469
473
|
RequestWorker job enqueued
|
|
470
474
|
↓ Sidekiq processes job
|
|
471
475
|
RequestWorker#perform
|
|
472
|
-
↓ Creates
|
|
473
|
-
PatientHttp::Processor
|
|
474
|
-
↓
|
|
475
|
-
TaskMonitor
|
|
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
|
-
↓
|
|
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
|
-
- **
|
|
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
|