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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +496 -0
- data/CHANGELOG.md +16 -0
- data/MIT-LICENSE +20 -0
- data/README.md +620 -0
- data/VERSION +1 -0
- data/lib/patient_http/sidekiq/callback_worker.rb +96 -0
- data/lib/patient_http/sidekiq/configuration.rb +175 -0
- data/lib/patient_http/sidekiq/context.rb +61 -0
- data/lib/patient_http/sidekiq/lifecycle_hooks.rb +42 -0
- data/lib/patient_http/sidekiq/processor_observer.rb +49 -0
- data/lib/patient_http/sidekiq/request_executor.rb +104 -0
- data/lib/patient_http/sidekiq/request_worker.rb +57 -0
- data/lib/patient_http/sidekiq/stats.rb +119 -0
- data/lib/patient_http/sidekiq/task_handler.rb +81 -0
- data/lib/patient_http/sidekiq/task_monitor.rb +542 -0
- data/lib/patient_http/sidekiq/task_monitor_thread.rb +154 -0
- data/lib/patient_http/sidekiq/web_ui/assets/patient-http/css/patient_http.css +249 -0
- data/lib/patient_http/sidekiq/web_ui/locales/ar.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/cs.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/da.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/de.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/el.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/en.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/es.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/fa.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/fr.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/gd.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/he.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/hi.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/it.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/ja.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/ko.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/lt.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/nb.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/nl.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/pl.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/pt-BR.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/pt.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/ru.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/sv.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/ta.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/tr.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/uk.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/ur.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/vi.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/zh-CN.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/locales/zh-TW.yml +26 -0
- data/lib/patient_http/sidekiq/web_ui/views/patient_http.html.erb +142 -0
- data/lib/patient_http/sidekiq/web_ui.rb +69 -0
- data/lib/patient_http/sidekiq.rb +328 -0
- data/lib/patient_http-sidekiq.rb +3 -0
- data/patient_http-sidekiq.gemspec +46 -0
- 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.
|