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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5d2f470219dae6e479974adecec97231a42918686c415b3d601c1803274cdb06
4
+ data.tar.gz: b8af3ece14b9c58032153ee2870c7122ae30b460894aad31bbd018f879e79da3
5
+ SHA512:
6
+ metadata.gz: 9f78b1ade928fc188d3e48247fe6a6a2a3891470b820b24e912737cdea8ad8ce1bbb3b0324e3dc0ab7417cf0c5142db9045b3ee24064e7d4d54de45ea191b126
7
+ data.tar.gz: 8050fbbbaa96eb45217ef7df4c1fb554f3b79fe8440156ed09e31e42d290fdc5cf849ebf20be2eb03dcf4b35a72b827d740e41606412cd1a987ea60ce1333b87
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,496 @@
1
+ # Architecture
2
+
3
+ ## Overview
4
+
5
+ PatientHttp::Sidekiq provides a Sidekiq integration layer for the [patient_http](https://github.com/bdurand/patient_http) gem, enabling long-running HTTP requests to be offloaded from Sidekiq worker threads to a dedicated async I/O processor. This integration uses Sidekiq's job system for request enqueueing and callback invocation, while leveraging patient_http's Fiber-based concurrency to handle hundreds of concurrent HTTP requests without blocking worker threads.
6
+
7
+ ## Key Design Principles
8
+
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
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
+ 4. **Lifecycle Integration**: Processor lifecycle is tightly coupled with Sidekiq's startup, quiet, and shutdown events
13
+ 5. **Sidekiq-Native Task Handling**: Request lifecycle operations (enqueueing, callbacks, retries) use Sidekiq's job system
14
+
15
+ ## Core Components
16
+
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.
19
+
20
+ ### TaskHandler
21
+ Implements the patient_http's `TaskHandler` interface to integrate with Sidekiq's job system:
22
+ - Enqueues `CallbackWorker` jobs when requests complete or error
23
+ - Handles request retries via `Sidekiq::Client.push`
24
+ - Manages large payloads via `ExternalStorage` before enqueueing
25
+
26
+ ### RequestWorker
27
+ A Sidekiq worker that receives HTTP request specifications and submits them to the async processor. This allows HTTP requests to be made from anywhere in your code (Rails controllers, background jobs, rake tasks, etc.) by enqueueing a job.
28
+
29
+ ### CallbackWorker
30
+ A Sidekiq worker that invokes callback service methods (`on_complete` or `on_error`) when HTTP requests complete. Handles payload decryption and external storage retrieval.
31
+
32
+ ### LifecycleHooks
33
+ Registers Sidekiq server lifecycle hooks to automatically:
34
+ - Start the processor when Sidekiq starts (`:startup` event)
35
+ - Drain the processor when Sidekiq receives TSTP signal (`:quiet` event)
36
+ - Stop the processor gracefully when Sidekiq shuts down (`:shutdown` event)
37
+
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.
40
+
41
+ ### ProcessorObserver
42
+ Observes processor state changes and updates the TaskMonitor's Redis heartbeats, enabling distributed crash recovery.
43
+
44
+ ### TaskMonitor
45
+ Manages crash recovery by tracking in-flight requests in Redis:
46
+ - Maintains a sorted set of request IDs indexed by timestamp
47
+ - Stores request payloads with metadata for recovery
48
+ - Detects orphaned requests when processes crash
49
+ - Re-enqueues orphaned requests via Sidekiq
50
+
51
+ ### TaskMonitorThread
52
+ Background thread that periodically:
53
+ - Updates heartbeat timestamps for in-flight requests
54
+ - Scans for orphaned requests from crashed processes
55
+ - Performs garbage collection on stale Redis data
56
+
57
+ ### 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
+
60
+ ### 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
+
63
+ ### 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
69
+
70
+ ## Callback Service Pattern
71
+
72
+ When HTTP requests complete, the processor enqueues CallbackWorker jobs to invoke the appropriate callback service method:
73
+
74
+ - **Success callbacks**: The `on_complete` method receives a `Response` object with status, headers, body, and callback arguments
75
+ - **Error callbacks**: The `on_error` method receives an `Error` object with error details and callback arguments
76
+ - **Callback arguments** are passed via the `callback_args:` option and accessed via `response.callback_args[:key]` or `error.callback_args[:key]`
77
+
78
+ Example:
79
+ ```ruby
80
+ # Define a callback service class
81
+ class FetchDataCallback
82
+ def on_complete(response)
83
+ user_id = response.callback_args[:user_id]
84
+ User.find(user_id).update!(data: response.json)
85
+ end
86
+
87
+ def on_error(error)
88
+ user_id = error.callback_args[:user_id]
89
+ Rails.logger.error("Failed to fetch data for user #{user_id}: #{error.message}")
90
+ end
91
+ end
92
+
93
+ # Make a request from anywhere in your code
94
+ PatientHttp::Sidekiq.get(
95
+ "https://api.example.com/users/123",
96
+ callback: FetchDataCallback,
97
+ callback_args: {user_id: 123}
98
+ )
99
+ ```
100
+
101
+ ## Request Lifecycle
102
+
103
+ ```mermaid
104
+ sequenceDiagram
105
+ participant App as Application Code
106
+ participant Module as PatientHttp::Sidekiq
107
+ participant ReqWorker as RequestWorker
108
+ participant Processor as PatientHttp::Processor
109
+ participant Sidekiq as Sidekiq Queue
110
+ participant Handler as TaskHandler
111
+ participant CbWorker as CallbackWorker
112
+ participant Callback as Callback Service
113
+
114
+ App->>Module: get(url, callback: MyCallback)
115
+ Module->>Sidekiq: Enqueue RequestWorker
116
+ Sidekiq->>ReqWorker: Execute job
117
+ ReqWorker->>Processor: submit(request, handler)
118
+ activate Processor
119
+ Note over Processor: Request queued<br/>in memory
120
+ Processor-->>ReqWorker: Returns immediately
121
+ ReqWorker-->>Sidekiq: Job completes
122
+ deactivate Processor
123
+
124
+ Note over Sidekiq: Worker thread free<br/>to process other jobs
125
+
126
+ activate Processor
127
+ Processor->>Processor: Fiber reactor<br/>dequeues request
128
+ Processor->>Processor: Execute HTTP request<br/>(non-blocking with async)
129
+
130
+ alt HTTP Request Completes
131
+ Processor->>Handler: on_complete(response, callback)
132
+ Handler->>Sidekiq: Enqueue CallbackWorker
133
+ Sidekiq->>CbWorker: Execute job
134
+ CbWorker->>Callback: on_complete(response)
135
+ Callback->>Callback: Process response
136
+ else Error Raised
137
+ Processor->>Handler: on_error(error, callback)
138
+ Handler->>Sidekiq: Enqueue CallbackWorker
139
+ Sidekiq->>CbWorker: Execute job
140
+ CbWorker->>Callback: on_error(error)
141
+ Callback->>Callback: Handle error
142
+ end
143
+ deactivate Processor
144
+ ```
145
+
146
+ Key integration points:
147
+ 1. **RequestWorker** converts Sidekiq job args into patient_http Request objects
148
+ 2. **TaskHandler** converts processor callbacks into Sidekiq jobs
149
+ 3. **CallbackWorker** invokes the user's callback service methods
150
+ 4. **ExternalStorage** handles large payloads transparently at each step
151
+
152
+ ## Component Relationships
153
+
154
+ ```mermaid
155
+ erDiagram
156
+ PATIENT-HTTP-SIDEKIQ ||--|| PATIENT-HTTP : "integrates with"
157
+ PATIENT-HTTP-SIDEKIQ ||--|| SIDEKIQ-TASK-HANDLER : "provides"
158
+ PATIENT-HTTP-SIDEKIQ ||--|| REQUEST-WORKER : "defines"
159
+ PATIENT-HTTP-SIDEKIQ ||--|| CALLBACK-WORKER : "defines"
160
+ PATIENT-HTTP-SIDEKIQ ||--|| TASK-MONITOR : "manages"
161
+ PATIENT-HTTP-SIDEKIQ ||--|| LIFECYCLE-HOOKS : "registers"
162
+
163
+ PATIENT-HTTP ||--|| PROCESSOR : "provides"
164
+ PATIENT-HTTP ||--|| REQUEST : "defines"
165
+ PATIENT-HTTP ||--|| RESPONSE : "defines"
166
+ PATIENT-HTTP ||--|| ERROR : "defines"
167
+ PATIENT-HTTP ||--|| EXTERNAL-STORAGE : "provides"
168
+
169
+ PROCESSOR ||--|| TASK-HANDLER : "uses"
170
+ PROCESSOR ||--o{ REQUEST : "processes"
171
+
172
+ SIDEKIQ-TASK-HANDLER ||--|| CALLBACK-WORKER : "enqueues"
173
+ SIDEKIQ-TASK-HANDLER ||--|| EXTERNAL-STORAGE : "uses"
174
+
175
+ REQUEST-WORKER ||--|| PROCESSOR : "submits to"
176
+ REQUEST-WORKER ||--|| SIDEKIQ-TASK-HANDLER : "creates"
177
+
178
+ CALLBACK-WORKER ||--|| CALLBACK-SERVICE : "invokes"
179
+ CALLBACK-WORKER ||--o| RESPONSE : "receives"
180
+ CALLBACK-WORKER ||--o| ERROR : "receives"
181
+
182
+ TASK-MONITOR ||--|| TASK-MONITOR-THREAD : "runs"
183
+ TASK-MONITOR ||--|| REDIS : "tracks in"
184
+
185
+ LIFECYCLE-HOOKS ||--|| PROCESSOR : "controls"
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
+ RESPONSE {
202
+ int status
203
+ hash headers
204
+ string body
205
+ hash callback_args
206
+ }
207
+
208
+ ERROR {
209
+ string message
210
+ string error_class
211
+ hash callback_args
212
+ }
213
+
214
+ CALLBACK-SERVICE {
215
+ method on_complete
216
+ method on_error
217
+ }
218
+
219
+ TASK-MONITOR {
220
+ hash inflight_tasks
221
+ string redis_key
222
+ }
223
+ ```
224
+
225
+ ### Integration Points
226
+
227
+ **Sidekiq → patient_http:**
228
+ - `RequestWorker` converts Sidekiq job args to `PatientHttp::Request` objects
229
+ - `TaskHandler` implements `PatientHttp::TaskHandler` interface
230
+ - `Processor` is created and managed by the Sidekiq integration layer
231
+
232
+ **patient_http → Sidekiq:**
233
+ - Processor calls `TaskHandler#on_complete` and `TaskHandler#on_error` callbacks
234
+ - `TaskHandler` enqueues `CallbackWorker` jobs via Sidekiq
235
+ - Large payloads are stored via `ExternalStorage` before enqueueing
236
+
237
+ ## Process Model
238
+
239
+ Each Sidekiq process runs:
240
+ - Multiple worker threads (configured via Sidekiq concurrency)
241
+ - **One** async HTTP processor thread (from patient_http)
242
+ - **One** fiber reactor within the processor thread
243
+ - **One** task monitor thread for crash recovery
244
+
245
+ ```
246
+ ┌─────────────────────────────────────────────────────────────┐
247
+ │ Sidekiq Process │
248
+ │ │
249
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
250
+ │ │ Worker │ │ Worker │ │ Worker │ │
251
+ │ │ Thread 1 │ │ Thread 2 │ │ Thread N │ │
252
+ │ │ │ │ │ │ │ │
253
+ │ │ Executes: │ │ Executes: │ │ Executes: │ │
254
+ │ │ - RequestW. │ │ - CallbackW. │ │ - Other Jobs │ │
255
+ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
256
+ │ │ │ │ │
257
+ │ └──────────────────┼─────────────────┘ │
258
+ │ │ │
259
+ │ ▼ │
260
+ │ ┌─────────────────────────┐ │
261
+ │ │ PatientHttp │ │
262
+ │ │ Processor │ │
263
+ │ │ (Dedicated Thread) │ │
264
+ │ │ │ │
265
+ │ │ ┌───────────────────┐ │ │
266
+ │ │ │ Async Fiber │ │ │
267
+ │ │ │ Reactor │ │ │
268
+ │ │ │ ═════════════ │ │ │
269
+ │ │ │ - HTTP/2 pools │ │ │
270
+ │ │ │ - 100+ concurrent│ │ │
271
+ │ │ │ requests │ │ │
272
+ │ │ │ - Non-blocking │ │ │
273
+ │ │ │ I/O │ │ │
274
+ │ │ └───────────────────┘ │ │
275
+ │ └─────────────────────────┘ │
276
+ │ │ │
277
+ │ ┌────────────┴─────────────┐ │
278
+ │ │ TaskMonitorThread │ │
279
+ │ │ (Crash Recovery) │ │
280
+ │ │ │ │
281
+ │ │ - Heartbeat updates │ │
282
+ │ │ - Orphan detection │ │
283
+ │ │ - Redis GC │ │
284
+ │ └──────────────────────────┘ │
285
+ └─────────────────────────────────────────────────────────────┘
286
+
287
+
288
+ ┌───────────────┐
289
+ │ Redis │
290
+ │ │
291
+ │ - Job queues │
292
+ │ - Inflight │
293
+ │ tracking │
294
+ │ - Payloads │
295
+ └───────────────┘
296
+ ```
297
+
298
+ ### Architectural Layers
299
+
300
+ **Application Layer:**
301
+ - User code calls `PatientHttp::Sidekiq.get/post/etc`
302
+ - Or includes `PatientHttp::RequestHelper` for `async_get/async_post/etc` instance methods
303
+ - Callback services implement `on_complete` and `on_error`
304
+
305
+ **Sidekiq Integration Layer (this gem):**
306
+ - `RequestWorker` - Sidekiq job to submit requests
307
+ - `CallbackWorker` - Sidekiq job to invoke callbacks
308
+ - `TaskHandler` - Bridges processor callbacks to Sidekiq jobs
309
+ - `LifecycleHooks` - Manages processor lifecycle
310
+ - `TaskMonitor` - Crash recovery and inflight tracking
311
+
312
+ **HTTP Processing Layer (patient_http):**
313
+ - `Processor` - Main async I/O processor
314
+ - `Request/Response/Error` - Value objects
315
+ - `ExternalStorage` - Large payload handling
316
+ - Async fiber scheduler and HTTP/2 connection pools
317
+
318
+ ## Concurrency Model
319
+
320
+ The system uses multiple levels of concurrency:
321
+
322
+ ### Sidekiq Worker Threads
323
+ - Process Sidekiq jobs from queues
324
+ - Execute `RequestWorker` and `CallbackWorker` jobs
325
+ - Block only briefly while submitting requests to the processor
326
+
327
+ ### Async HTTP Processor Thread (from patient_http)
328
+ - Runs Ruby's Fiber scheduler (`async` gem) for non-blocking I/O
329
+ - Maintains HTTP/2 connection pools for efficient connection reuse
330
+ - Multiplexes hundreds of concurrent HTTP requests via fibers
331
+ - Each HTTP request runs in its own fiber (lightweight concurrency)
332
+
333
+ ### Task Monitor Thread
334
+ - Periodically updates Redis heartbeats for in-flight requests
335
+ - Scans for orphaned requests from crashed processes
336
+ - Performs garbage collection on stale data
337
+
338
+ **Benefits:**
339
+ 1. **Worker threads remain free** - submitting a request to the processor takes ~1ms
340
+ 2. **Fiber-based multiplexing** - handle hundreds of concurrent requests in a single thread
341
+ 3. **HTTP/2 connection reuse** - multiple requests share persistent connections
342
+ 4. **Non-blocking I/O** - fibers yield during network I/O, allowing other requests to progress
343
+
344
+ ## State Management
345
+
346
+ The processor (from patient_http) maintains state through its lifecycle, managed by Sidekiq lifecycle hooks:
347
+
348
+ - **stopped**: Initial state, not processing requests
349
+ - **starting**: Processor is initializing, reactor thread launching
350
+ - **running**: Actively processing requests
351
+ - **draining**: Not accepting new requests (triggered by Sidekiq's `:quiet` event), completing in-flight
352
+ - **stopping**: Shutting down (triggered by Sidekiq's `:shutdown` event), waiting for requests to finish
353
+
354
+ **Lifecycle Integration:**
355
+
356
+ ```ruby
357
+ # Registered automatically via LifecycleHooks
358
+ Sidekiq.configure_server do |config|
359
+ config.on(:startup) { PatientHttp::Sidekiq.start } # → processor state: running
360
+ config.on(:quiet) { PatientHttp::Sidekiq.quiet } # → processor state: draining
361
+ config.on(:shutdown) { PatientHttp::Sidekiq.stop } # → processor state: stopping
362
+ end
363
+ ```
364
+
365
+ ## Crash Recovery
366
+
367
+ In-flight requests are tracked in Redis to enable recovery when Sidekiq processes crash:
368
+
369
+ ### TaskMonitor
370
+ - Maintains a Redis sorted set of in-flight request IDs indexed by timestamp
371
+ - Stores request payloads with metadata (Sidekiq job, callback info)
372
+ - Each process has a unique process ID: `hostname:pid:hex`
373
+
374
+ ### TaskMonitorThread
375
+ - Runs in background, periodically updating heartbeat timestamps in Redis
376
+ - Scans for orphaned requests (no heartbeat update within threshold)
377
+ - Re-enqueues orphaned requests via `Sidekiq::Client.push`
378
+
379
+ ### Recovery Process
380
+ 1. `ProcessorObserver` notifies `TaskMonitor` when requests start/complete
381
+ 2. `TaskMonitorThread` updates heartbeat timestamps in Redis
382
+ 3. If a process crashes, heartbeat updates stop
383
+ 4. Other processes' monitor threads detect stale timestamps
384
+ 5. Orphaned requests are atomically removed and re-enqueued
385
+ 6. Prevents lost work during deployments or crashes
386
+
387
+ **Redis Keys:**
388
+ - `sidekiq:patient_http:inflight_index` - Sorted set of request IDs by timestamp
389
+ - `sidekiq:patient_http:inflight_jobs` - Hash of request payloads
390
+ - `sidekiq:patient_http:processes` - Set of active process IDs
391
+ - `sidekiq:patient_http:gc_lock` - Distributed lock for garbage collection
392
+ - `sidekiq:patient_http:gc_last_run` - Timestamp of last garbage collection run
393
+
394
+ ## Configuration
395
+
396
+ Configuration is split between Sidekiq-specific concerns and patient_http settings:
397
+
398
+ ### Sidekiq Integration Settings
399
+ ```ruby
400
+ PatientHttp::Sidekiq.configure do |config|
401
+ # Sidekiq worker options (applied to both RequestWorker and CallbackWorker)
402
+ config.sidekiq_options = {queue: "patient_http", retry: 5}
403
+
404
+ # Encryption (for sensitive data in Sidekiq jobs; inherited from PatientHttp::Configuration)
405
+ config.encryption_key = ENV["PATIENT_HTTP_ENCRYPTION_KEY"]
406
+
407
+ # External storage threshold (for large payloads)
408
+ config.payload_store_threshold = 100_000 # bytes
409
+
410
+ # Shutdown timeout
411
+ config.shutdown_timeout = 30 # seconds
412
+ end
413
+ ```
414
+
415
+ ### Async HTTP Pool Settings (delegated)
416
+ All patient_http configuration is accessible:
417
+ ```ruby
418
+ PatientHttp::Sidekiq.configure do |config|
419
+ # HTTP settings
420
+ config.request_timeout = 30
421
+ config.max_connections = 100
422
+
423
+ # Retry behavior
424
+ config.retries = 3
425
+
426
+ # Proxy settings
427
+ config.proxy_url = ENV["HTTP_PROXY"]
428
+ end
429
+ ```
430
+
431
+ The configuration object is passed to the `PatientHttp::Processor` on startup.
432
+
433
+ ## Web UI
434
+
435
+ Optional Sidekiq Web integration (via `WebUI` module) provides:
436
+
437
+ - Real-time processor state (running/draining/stopped)
438
+ - In-flight request counts by process
439
+ - Historical statistics from patient_http metrics
440
+ - Health indicators
441
+ - Max connection capacity per process
442
+
443
+ The Web UI reads from:
444
+ - `TaskMonitor` Redis keys for inflight counts
445
+ - `PatientHttp::Processor` stats for metrics
446
+ - Process registry for distributed monitoring
447
+
448
+ ## Data Flow
449
+
450
+ ### Making a Request
451
+
452
+ ```
453
+ Application Code
454
+ ↓ PatientHttp::Sidekiq.get(url, callback: MyCallback)
455
+ RequestWorker job enqueued
456
+ ↓ Sidekiq processes job
457
+ RequestWorker#perform
458
+ ↓ Creates Request, TaskHandler
459
+ PatientHttp::Processor.submit(request, handler)
460
+ ↓ Queued in memory
461
+ TaskMonitor.track(request_id, handler)
462
+ ↓ Stored in Redis
463
+ Fiber reactor processes request
464
+ ↓ Non-blocking HTTP I/O
465
+ Response/Error received
466
+ ```
467
+
468
+ ### Processing a Response
469
+
470
+ ```
471
+ Fiber completes with Response
472
+
473
+ TaskHandler#on_complete(response, callback)
474
+ ↓ Stores via ExternalStorage if large
475
+ CallbackWorker job enqueued
476
+ ↓ Sidekiq processes job
477
+ CallbackWorker#perform
478
+ ↓ Fetches from ExternalStorage if needed
479
+ ↓ Decrypts payload
480
+ MyCallback.new.on_complete(response)
481
+ ↓ User code executes
482
+ TaskMonitor.untrack(request_id)
483
+ ↓ Removed from Redis
484
+ ```
485
+
486
+ ## Thread Safety
487
+
488
+ - **Thread-safe submission**: `Processor` uses thread-safe queues for request submission
489
+ - **Atomic state changes**: Processor state managed with atomic operations
490
+ - **Redis-based coordination**: TaskMonitor uses Redis for distributed coordination
491
+ - **Immutable values**: Request/Response/Error objects are immutable once created
492
+ - **Sidekiq job isolation**: Each CallbackWorker runs in its own thread
493
+
494
+ ## Further Reading
495
+
496
+ - [README](README.md)
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
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
+ - Dedicated async HTTP processor thread for Sidekiq to avoid blocking worker threads during in-flight requests.
12
+ - `PatientHttp::Sidekiq` API with convenience methods for common HTTP verbs (`get`, `post`, `put`, `patch`, and `delete`).
13
+ - Callback-based completion and error handling via `on_complete` and `on_error`, executed as Sidekiq jobs.
14
+ - Support for callback context via `callback_args`, available from response and error objects.
15
+ - Built-in runtime visibility with task monitoring and a Sidekiq Web UI page for async HTTP activity.
16
+ - Crash/failure recovery with heartbeat-based orphan detection and automatic re-enqueue of interrupted requests.
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.