patient_http-sidekiq 1.2.0 → 1.3.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 +31 -10
- data/CHANGELOG.md +10 -0
- data/README.md +21 -0
- data/VERSION +1 -1
- data/lib/patient_http/sidekiq/configuration.rb +27 -0
- data/lib/patient_http/sidekiq/direct_task_handler.rb +44 -0
- data/lib/patient_http/sidekiq/processor_observer.rb +16 -2
- data/lib/patient_http/sidekiq/request_executor.rb +7 -5
- data/lib/patient_http/sidekiq/task_monitor_thread.rb +7 -6
- data/lib/patient_http/sidekiq.rb +71 -4
- data/patient_http-sidekiq.gemspec +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47d12842c42be5d8953e2e54afcaa7f702ead028c6aef09c9f01bb16e48d0193
|
|
4
|
+
data.tar.gz: 1ab374564b701fd7201b36a208bdd31a24e60829eaa4522a4cdbcae27380721e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b26e488319bde0bc5d778e19281897b8244e7e70a82df90f8c7824329fe449b2d1cc52664ffb090d62c6671edd99e6c98c9e12c669c7888560ad082b8d1db2b6
|
|
7
|
+
data.tar.gz: 0ff5a1cfe9e6ce4ee41e4a3a50ce8af4b88ec47020be8d3d6117ebe96a0cb7b402f0ec77e0911b7dd2e803ea5b70fb1e1acd939dcd69b15e865855e4a1a35f42
|
data/ARCHITECTURE.md
CHANGED
|
@@ -112,14 +112,21 @@ sequenceDiagram
|
|
|
112
112
|
participant Callback as Callback Service
|
|
113
113
|
|
|
114
114
|
App->>Module: get(url, callback: MyCallback)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Processor
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
|
|
116
|
+
alt Processor running in this process (direct execution)
|
|
117
|
+
Module->>Processor: submit(request, handler)
|
|
118
|
+
Note over Module: DirectTaskHandler keeps the<br/>RequestWorker args for re-enqueue
|
|
119
|
+
Processor-->>Module: Returns immediately
|
|
120
|
+
else Processor not in this process
|
|
121
|
+
Module->>Sidekiq: Enqueue RequestWorker
|
|
122
|
+
Sidekiq->>ReqWorker: Execute job
|
|
123
|
+
ReqWorker->>Processor: submit(request, handler)
|
|
124
|
+
activate Processor
|
|
125
|
+
Note over Processor: Request queued<br/>in memory
|
|
126
|
+
Processor-->>ReqWorker: Returns immediately
|
|
127
|
+
ReqWorker-->>Sidekiq: Job completes
|
|
128
|
+
deactivate Processor
|
|
129
|
+
end
|
|
123
130
|
|
|
124
131
|
Note over Sidekiq: Worker thread free<br/>to process other jobs
|
|
125
132
|
|
|
@@ -149,6 +156,8 @@ Key integration points:
|
|
|
149
156
|
3. **CallbackWorker** invokes the user's callback service methods
|
|
150
157
|
4. **ExternalStorage** handles large payloads transparently at each step
|
|
151
158
|
|
|
159
|
+
Direct execution (enabled by default with `config.direct_execution`) skips the `RequestWorker` enqueue when the processor runs in the current process. The request gets a `DirectTaskHandler` that holds the `RequestWorker` job arguments, so every re-enqueue path (processor shutdown, crash recovery, and the at-capacity fallback) can enqueue the request as a normal `RequestWorker` job. The handler exposes a minimal job record for the crash-recovery registry because the orphan sweep pushes the stored record from another process. Requests made in a `with_sidekiq_options` block and requests made while `Sidekiq::Testing` is enabled always go through the queue, so that Sidekiq applies the options. Options set with `config.sidekiq_options` (including a `queue`) do not apply to direct-executed requests, because no Sidekiq job is created; set `config.direct_execution = false` to route every request through the configured queue. A failure to write the crash-recovery registry entry rejects the request and raises to the caller, the same as a failed enqueue.
|
|
160
|
+
|
|
152
161
|
## Component Relationships
|
|
153
162
|
|
|
154
163
|
```mermaid
|
|
@@ -377,13 +386,15 @@ In-flight requests are tracked in Redis to enable recovery when Sidekiq processe
|
|
|
377
386
|
- Re-enqueues orphaned requests via `Sidekiq::Client.push`
|
|
378
387
|
|
|
379
388
|
### Recovery Process
|
|
380
|
-
1. `ProcessorObserver`
|
|
381
|
-
2. `TaskMonitorThread` updates heartbeat timestamps in Redis
|
|
389
|
+
1. `ProcessorObserver` registers a request with `TaskMonitor` when the processor accepts it (before `Processor#enqueue` returns) and unregisters it when the request completes or when a Sidekiq job owns the request again (rejected or re-enqueued)
|
|
390
|
+
2. `TaskMonitorThread` updates heartbeat timestamps in Redis for all tracked requests (queued, pending, and in-flight)
|
|
382
391
|
3. If a process crashes, heartbeat updates stop
|
|
383
392
|
4. Other processes' monitor threads detect stale timestamps
|
|
384
393
|
5. Orphaned requests are atomically removed and re-enqueued
|
|
385
394
|
6. Prevents lost work during deployments or crashes
|
|
386
395
|
|
|
396
|
+
Recovery gives at-least-once delivery. A crash between a re-enqueue and the removal of the registry entry can execute a request more than once, so callbacks must be idempotent. A request is durable once the submitting call returns; a crash during the call behaves like a failed enqueue.
|
|
397
|
+
|
|
387
398
|
**Redis Keys:**
|
|
388
399
|
- `sidekiq:patient_http:inflight_index` - Sorted set of request IDs by timestamp
|
|
389
400
|
- `sidekiq:patient_http:inflight_jobs` - Hash of request payloads
|
|
@@ -401,6 +412,9 @@ PatientHttp::Sidekiq.configure do |config|
|
|
|
401
412
|
# Sidekiq worker options (applied to both RequestWorker and CallbackWorker)
|
|
402
413
|
config.sidekiq_options = {queue: "patient_http", retry: 5}
|
|
403
414
|
|
|
415
|
+
# Skip the Sidekiq queue when the processor runs in the current process
|
|
416
|
+
config.direct_execution = true
|
|
417
|
+
|
|
404
418
|
# Encryption (for sensitive data in Sidekiq jobs; inherited from PatientHttp::Configuration)
|
|
405
419
|
config.encryption_key = ENV["PATIENT_HTTP_ENCRYPTION_KEY"]
|
|
406
420
|
|
|
@@ -465,6 +479,13 @@ Fiber reactor processes request
|
|
|
465
479
|
Response/Error received
|
|
466
480
|
```
|
|
467
481
|
|
|
482
|
+
With direct execution (the default), a request made in a process with a running
|
|
483
|
+
processor skips the enqueue and the `RequestWorker#perform` steps. A
|
|
484
|
+
`DirectTaskHandler` holds the `RequestWorker` job arguments, and the request
|
|
485
|
+
goes straight to the processor. The rest of the flow is identical, and the
|
|
486
|
+
re-enqueue paths use the handler to enqueue a normal `RequestWorker` job when
|
|
487
|
+
needed.
|
|
488
|
+
|
|
468
489
|
### Processing a Response
|
|
469
490
|
|
|
470
491
|
```
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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.3.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Requests made in a process with a running processor now go straight to the processor instead of being enqueued through Sidekiq. The request can always be re-enqueued as a normal `RequestWorker` job, so all of the re-enqueue paths (processor shutdown, crash recovery, and the at-capacity fallback) behave the same as the enqueued path. Requests made in a `with_sidekiq_options` block always go through the Sidekiq queue, so that Sidekiq applies the options; options set with `config.sidekiq_options` do not apply to direct-executed requests because no Sidekiq job is created. The new `direct_execution` configuration option (default: `true`) turns this behavior off.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Requests are now registered in the crash-recovery registry when the processor accepts them, before the enqueue call returns, instead of when the request starts processing. A request handed to the processor is durable from that point on, and heartbeats now cover queued requests as well as in-flight ones. The entry is removed when the request completes or when a Sidekiq job owns the request again. A failure to write the registry entry rejects the request and raises to the caller, the same as a failed enqueue. This requires patient_http 1.4.0.
|
|
16
|
+
|
|
7
17
|
## 1.2.0
|
|
8
18
|
|
|
9
19
|
### Added
|
data/README.md
CHANGED
|
@@ -246,6 +246,19 @@ The options are applied with Sidekiq's `set` method, so any Sidekiq job option (
|
|
|
246
246
|
|
|
247
247
|
- If the options include a `queue`, the callback job that invokes your `on_complete`/`on_error` methods is enqueued on that queue as well, so the whole request keeps one priority end to end.
|
|
248
248
|
- Nested blocks merge their options, and the innermost values take precedence. The previous options are restored when the block exits, even if the block raises an error.
|
|
249
|
+
- Requests made in the block always go through the Sidekiq queue, even when the processor runs in the current process, so that Sidekiq applies the options (see Direct Execution below).
|
|
250
|
+
|
|
251
|
+
### Direct Execution
|
|
252
|
+
|
|
253
|
+
When a request is made in a process where the processor is running (normally a Sidekiq server process), the request skips the Sidekiq queue and goes straight to the processor. This removes a round trip through Redis. The behavior is the same as the enqueued path:
|
|
254
|
+
|
|
255
|
+
- The request can always be re-enqueued. It is registered in the crash-recovery registry before the call returns, so if the processor shuts down or the process crashes, the request is enqueued as a normal `RequestWorker` job. If the registry entry cannot be written (for example, Redis is unavailable), the call raises, the same as a failed enqueue.
|
|
256
|
+
- If the processor is at max capacity or stops accepting requests, the request is enqueued through Sidekiq instead, and the normal Sidekiq retry behavior applies from there.
|
|
257
|
+
- Requests made in a `with_sidekiq_options` block always go through the Sidekiq queue, so that Sidekiq applies the options (queue routing, scheduling, retry). Use this to route specific requests to a dedicated Sidekiq process.
|
|
258
|
+
- Options set with `config.sidekiq_options` (including a `queue`) do not apply to direct-executed requests, because no Sidekiq job is created. If every request must go through the configured queue (for example, to run all requests on a dedicated Sidekiq process), set `config.direct_execution = false`.
|
|
259
|
+
- Direct execution is disabled when `Sidekiq::Testing` is enabled, so tests can observe enqueued jobs as usual.
|
|
260
|
+
|
|
261
|
+
You can turn this off with `config.direct_execution = false`. Do this if you route all requests to a dedicated queue with `config.sidekiq_options`, if you need Sidekiq client or server middleware to run for every request, or if you want every request to be visible as an enqueued job in Sidekiq metrics and the Web UI.
|
|
249
262
|
|
|
250
263
|
### Using Request Templates
|
|
251
264
|
|
|
@@ -464,6 +477,12 @@ PatientHttp::Sidekiq.configure do |config|
|
|
|
464
477
|
# (use PatientHttp::Sidekiq.with_sidekiq_options to override per request)
|
|
465
478
|
config.sidekiq_options = {queue: "patient_http", retry: 5}
|
|
466
479
|
|
|
480
|
+
# Whether requests made in a process with a running processor skip the
|
|
481
|
+
# Sidekiq queue and go straight to the processor (default: true).
|
|
482
|
+
# Sidekiq options, including a queue, do not apply to direct-executed
|
|
483
|
+
# requests; set this to false to route every request through the queue.
|
|
484
|
+
config.direct_execution = true
|
|
485
|
+
|
|
467
486
|
# Handler called when a callback job exhausts all Sidekiq retries
|
|
468
487
|
config.on_retries_exhausted { |error| MyAlertService.notify(error) }
|
|
469
488
|
|
|
@@ -588,6 +607,8 @@ The gem includes crash recovery to handle process failures:
|
|
|
588
607
|
|
|
589
608
|
This ensures that if a Sidekiq process crashes, its in-flight requests will be retried by another process.
|
|
590
609
|
|
|
610
|
+
Crash recovery gives at-least-once delivery. If a process crashes at the wrong moment (for example, between a re-enqueue and the removal of the registry entry), a request can execute more than once and its callback can fire more than once. Make your callbacks idempotent. A request is durable once the call that submits it returns; a crash during the call behaves like a failed enqueue, and the caller never received an acknowledgment.
|
|
611
|
+
|
|
591
612
|
## Testing
|
|
592
613
|
|
|
593
614
|
The gem supports `Sidekiq::Testing.inline!` mode for synchronous testing. When in inline mode, async HTTP requests are executed immediately within the worker thread, blocking until completion. This allows you to write tests that verify the full request/response cycle without needing the async processor to be running.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.3.0
|
|
@@ -24,6 +24,10 @@ module PatientHttp
|
|
|
24
24
|
# @return [Hash, nil] Sidekiq options to apply to RequestWorker and CallbackWorker
|
|
25
25
|
attr_reader :sidekiq_options
|
|
26
26
|
|
|
27
|
+
# @return [Boolean] Whether requests execute directly on a processor running in the
|
|
28
|
+
# current process instead of being enqueued through Sidekiq
|
|
29
|
+
attr_reader :direct_execution
|
|
30
|
+
|
|
27
31
|
# @return [#call, nil] Handler invoked when a CallbackWorker job exhausts all retries.
|
|
28
32
|
# @overload on_retries_exhausted
|
|
29
33
|
# Returns the current handler.
|
|
@@ -51,6 +55,8 @@ module PatientHttp
|
|
|
51
55
|
# @param orphan_threshold [Integer] Age threshold for detecting orphaned requests in seconds
|
|
52
56
|
# @param sidekiq_options [Hash, nil] Sidekiq options to apply to RequestWorker and CallbackWorker
|
|
53
57
|
# @param on_retries_exhausted [#call, nil] Handler called when a CallbackWorker job exhausts retries
|
|
58
|
+
# @param direct_execution [Boolean] Whether requests execute directly on a processor
|
|
59
|
+
# running in the current process instead of being enqueued through Sidekiq
|
|
54
60
|
# @param pool_options [Hash] Options passed through to PatientHttp::Configuration.
|
|
55
61
|
# Sidekiq-aware defaults are applied for shutdown_timeout and logger
|
|
56
62
|
# if not explicitly provided.
|
|
@@ -60,6 +66,7 @@ module PatientHttp
|
|
|
60
66
|
sidekiq_options: nil,
|
|
61
67
|
payload_store_threshold: DEFAULT_PAYLOAD_STORE_THRESHOLD,
|
|
62
68
|
on_retries_exhausted: nil,
|
|
69
|
+
direct_execution: true,
|
|
63
70
|
**pool_options
|
|
64
71
|
)
|
|
65
72
|
pool_options[:shutdown_timeout] ||= (::Sidekiq.default_configuration[:timeout] || 25) - 2
|
|
@@ -73,6 +80,7 @@ module PatientHttp
|
|
|
73
80
|
self.orphan_threshold = orphan_threshold
|
|
74
81
|
self.payload_store_threshold = payload_store_threshold || DEFAULT_PAYLOAD_STORE_THRESHOLD
|
|
75
82
|
self.on_retries_exhausted = on_retries_exhausted
|
|
83
|
+
self.direct_execution = direct_execution
|
|
76
84
|
end
|
|
77
85
|
|
|
78
86
|
# Set the on_retries_exhausted handler.
|
|
@@ -144,6 +152,24 @@ module PatientHttp
|
|
|
144
152
|
apply_sidekiq_options(options)
|
|
145
153
|
end
|
|
146
154
|
|
|
155
|
+
# Set whether requests execute directly on a processor running in the current
|
|
156
|
+
# process. When enabled, requests made in a process with a running processor
|
|
157
|
+
# skip the Sidekiq queue and go straight to the processor. The value is
|
|
158
|
+
# coerced to a boolean.
|
|
159
|
+
#
|
|
160
|
+
# @param value [Boolean] true to enable direct execution
|
|
161
|
+
# @return [void]
|
|
162
|
+
def direct_execution=(value)
|
|
163
|
+
@direct_execution = !!value
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check if direct execution is enabled.
|
|
167
|
+
#
|
|
168
|
+
# @return [Boolean]
|
|
169
|
+
def direct_execution?
|
|
170
|
+
@direct_execution
|
|
171
|
+
end
|
|
172
|
+
|
|
147
173
|
# Convert to hash for inspection
|
|
148
174
|
# @return [Hash] hash representation with string keys
|
|
149
175
|
def to_h
|
|
@@ -152,6 +178,7 @@ module PatientHttp
|
|
|
152
178
|
"heartbeat_interval" => heartbeat_interval,
|
|
153
179
|
"orphan_threshold" => orphan_threshold,
|
|
154
180
|
"sidekiq_options" => sidekiq_options,
|
|
181
|
+
"direct_execution" => direct_execution,
|
|
155
182
|
"on_retries_exhausted" => on_retries_exhausted ? "defined" : nil
|
|
156
183
|
)
|
|
157
184
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
module Sidekiq
|
|
5
|
+
# TaskHandler for requests executed directly on the local processor
|
|
6
|
+
# without an enqueued Sidekiq job. Requests with scoped Sidekiq options
|
|
7
|
+
# always go through the queue, so the handler only deals with the
|
|
8
|
+
# default RequestWorker options.
|
|
9
|
+
#
|
|
10
|
+
# Retry enqueues a normal RequestWorker job with the original arguments,
|
|
11
|
+
# so the fail-back behavior matches the enqueued path. The sidekiq_job
|
|
12
|
+
# hash is a minimal job record kept for the crash-recovery registry; it
|
|
13
|
+
# has no jid because no Sidekiq job exists until the request is
|
|
14
|
+
# re-enqueued, so job_id returns nil.
|
|
15
|
+
class DirectTaskHandler < TaskHandler
|
|
16
|
+
# @param args [Array] the RequestWorker job arguments
|
|
17
|
+
def initialize(args)
|
|
18
|
+
@args = args
|
|
19
|
+
super(minimal_job_record)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Re-enqueue the request as a normal RequestWorker job.
|
|
23
|
+
#
|
|
24
|
+
# @return [String] the job ID
|
|
25
|
+
def retry
|
|
26
|
+
RequestWorker.perform_async(*@args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Minimal pushable job record for the crash-recovery registry.
|
|
32
|
+
# TaskMonitor serializes it to Redis and the orphan GC pushes it
|
|
33
|
+
# verbatim, possibly from another process, so it cannot enqueue
|
|
34
|
+
# through this handler. The worker options are included because
|
|
35
|
+
# Sidekiq::Client.push does not apply them when "class" is a String.
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash]
|
|
38
|
+
def minimal_job_record
|
|
39
|
+
RequestWorker.get_sidekiq_options
|
|
40
|
+
.merge("class" => RequestWorker.name, "args" => @args)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -4,6 +4,12 @@ module PatientHttp
|
|
|
4
4
|
module Sidekiq
|
|
5
5
|
# Procesor Observer that collect stats in Redis for the WebUI and
|
|
6
6
|
# monitors for crashed processes in order to re-enqueue workers.
|
|
7
|
+
#
|
|
8
|
+
# Tasks are registered in the crash-recovery registry when the processor
|
|
9
|
+
# accepts them, before Processor#enqueue returns, so a request always has
|
|
10
|
+
# a durable record from the moment the caller hands it off. The entry is
|
|
11
|
+
# removed when the request completes or when a Sidekiq job owns the
|
|
12
|
+
# request again (the task was rejected or re-enqueued).
|
|
7
13
|
class ProcessorObserver < PatientHttp::ProcessorObserver
|
|
8
14
|
attr_reader :task_monitor
|
|
9
15
|
|
|
@@ -14,7 +20,7 @@ module PatientHttp
|
|
|
14
20
|
@monitor_thread = TaskMonitorThread.new(
|
|
15
21
|
processor.config,
|
|
16
22
|
@task_monitor,
|
|
17
|
-
-> { @processor.
|
|
23
|
+
-> { @processor.tracked_request_ids }
|
|
18
24
|
)
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -31,10 +37,18 @@ module PatientHttp
|
|
|
31
37
|
@stats.record_capacity_exceeded
|
|
32
38
|
end
|
|
33
39
|
|
|
34
|
-
def
|
|
40
|
+
def request_enqueued(request_task)
|
|
35
41
|
task_monitor.register(request_task)
|
|
36
42
|
end
|
|
37
43
|
|
|
44
|
+
def request_rejected(request_task)
|
|
45
|
+
task_monitor.unregister(request_task)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def request_requeued(request_task)
|
|
49
|
+
task_monitor.unregister(request_task)
|
|
50
|
+
end
|
|
51
|
+
|
|
38
52
|
def request_end(request_task)
|
|
39
53
|
task_monitor.unregister(request_task)
|
|
40
54
|
@stats.record_request(request_task.response&.status, request_task.duration)
|
|
@@ -7,9 +7,8 @@ module PatientHttp
|
|
|
7
7
|
class << self
|
|
8
8
|
# Execute the request directly on the async processor.
|
|
9
9
|
#
|
|
10
|
-
# This method enqueues the request directly to the async processor.
|
|
11
|
-
#
|
|
12
|
-
# Used internally by RequestWorker.
|
|
10
|
+
# This method enqueues the request directly to the async processor. Used internally
|
|
11
|
+
# by RequestWorker.
|
|
13
12
|
#
|
|
14
13
|
# When the request completes, the callback's +on_complete+ method is called with
|
|
15
14
|
# a Response object. If an error occurs (network error, timeout, or non-2xx response
|
|
@@ -22,6 +21,9 @@ module PatientHttp
|
|
|
22
21
|
# If not provided, uses PatientHttp::Sidekiq::Context.current_job.
|
|
23
22
|
# This requires the PatientHttp::Sidekiq::Context::Middleware to be added
|
|
24
23
|
# to the Sidekiq server middleware chain.
|
|
24
|
+
# @param task_handler [PatientHttp::TaskHandler, nil] A prebuilt task handler.
|
|
25
|
+
# When provided, the sidekiq_job parameter is ignored and no handler is
|
|
26
|
+
# built from it. Used for direct execution on the local processor.
|
|
25
27
|
# @param synchronous [Boolean] If true, runs the request inline (for testing).
|
|
26
28
|
# @param callback_args [#to_h, nil] Arguments to pass to callback via the
|
|
27
29
|
# Response/Error object. Must respond to +to_h+ and contain only JSON-native types
|
|
@@ -38,14 +40,14 @@ module PatientHttp
|
|
|
38
40
|
request,
|
|
39
41
|
callback:,
|
|
40
42
|
sidekiq_job: nil,
|
|
43
|
+
task_handler: nil,
|
|
41
44
|
synchronous: false,
|
|
42
45
|
callback_args: nil,
|
|
43
46
|
raise_error_responses: false,
|
|
44
47
|
request_id: nil
|
|
45
48
|
)
|
|
46
|
-
|
|
49
|
+
task_handler ||= TaskHandler.new(validate_sidekiq_job(sidekiq_job))
|
|
47
50
|
config = PatientHttp::Sidekiq.configuration
|
|
48
|
-
task_handler = TaskHandler.new(sidekiq_job)
|
|
49
51
|
|
|
50
52
|
task = PatientHttp::RequestTask.new(
|
|
51
53
|
request: request,
|
|
@@ -20,12 +20,13 @@ module PatientHttp
|
|
|
20
20
|
#
|
|
21
21
|
# @param config [Configuration] the configuration object
|
|
22
22
|
# @param task_monitor [TaskMonitor] the inflight request registry
|
|
23
|
-
# @param
|
|
23
|
+
# @param tracked_ids_callback [Proc] callback to get the IDs of all requests the
|
|
24
|
+
# processor is tracking (queued, pending, and in-flight)
|
|
24
25
|
# @return [void]
|
|
25
|
-
def initialize(config, task_monitor,
|
|
26
|
+
def initialize(config, task_monitor, tracked_ids_callback)
|
|
26
27
|
@config = config
|
|
27
28
|
@task_monitor = task_monitor
|
|
28
|
-
@
|
|
29
|
+
@tracked_ids_callback = tracked_ids_callback
|
|
29
30
|
@thread = nil
|
|
30
31
|
@running = Concurrent::AtomicBoolean.new(false)
|
|
31
32
|
@stop_signal = Concurrent::Event.new
|
|
@@ -107,16 +108,16 @@ module PatientHttp
|
|
|
107
108
|
@config.logger&.info("[PatientHttp::Sidekiq] Monitor thread stopped")
|
|
108
109
|
end
|
|
109
110
|
|
|
110
|
-
# Update heartbeats for all
|
|
111
|
+
# Update heartbeats for all tracked requests.
|
|
111
112
|
#
|
|
112
113
|
# @return [void]
|
|
113
114
|
def update_heartbeats
|
|
114
|
-
request_ids = @
|
|
115
|
+
request_ids = @tracked_ids_callback.call
|
|
115
116
|
return if request_ids.empty?
|
|
116
117
|
|
|
117
118
|
@task_monitor.update_heartbeats(request_ids)
|
|
118
119
|
|
|
119
|
-
@config.logger&.debug("[PatientHttp::Sidekiq] Updated heartbeats for #{request_ids.size}
|
|
120
|
+
@config.logger&.debug("[PatientHttp::Sidekiq] Updated heartbeats for #{request_ids.size} tracked requests")
|
|
120
121
|
rescue => e
|
|
121
122
|
@config.logger&.error("[PatientHttp::Sidekiq] Failed to update heartbeats: #{e.class} - #{e.message}")
|
|
122
123
|
raise if PatientHttp.testing?
|
data/lib/patient_http/sidekiq.rb
CHANGED
|
@@ -77,6 +77,7 @@ module PatientHttp
|
|
|
77
77
|
autoload :CallbackWorker, File.join(__dir__, "sidekiq/callback_worker")
|
|
78
78
|
autoload :Configuration, File.join(__dir__, "sidekiq/configuration")
|
|
79
79
|
autoload :Context, File.join(__dir__, "sidekiq/context")
|
|
80
|
+
autoload :DirectTaskHandler, File.join(__dir__, "sidekiq/direct_task_handler")
|
|
80
81
|
autoload :ProcessorObserver, File.join(__dir__, "sidekiq/processor_observer")
|
|
81
82
|
autoload :RequestExecutor, File.join(__dir__, "sidekiq/request_executor")
|
|
82
83
|
autoload :RequestWorker, File.join(__dir__, "sidekiq/request_worker")
|
|
@@ -220,7 +221,9 @@ module PatientHttp
|
|
|
220
221
|
# Nested calls merge options with the innermost values taking precedence.
|
|
221
222
|
# If the options include a queue, the callback job for the request is
|
|
222
223
|
# enqueued on that queue as well. Options only apply to requests enqueued
|
|
223
|
-
# in the same fiber as the block.
|
|
224
|
+
# in the same fiber as the block. Requests made in the block always go
|
|
225
|
+
# through the Sidekiq queue, even when direct execution is enabled, so
|
|
226
|
+
# that Sidekiq applies the options. This method has no effect
|
|
224
227
|
# when jobs run inline with Sidekiq::Testing.inline!.
|
|
225
228
|
#
|
|
226
229
|
# @param options [Hash] Sidekiq job options (symbol or string keys)
|
|
@@ -260,7 +263,8 @@ module PatientHttp
|
|
|
260
263
|
callback_args = PatientHttp::CallbackValidator.validate_callback_args(callback_args)
|
|
261
264
|
request_id = SecureRandom.uuid
|
|
262
265
|
|
|
263
|
-
|
|
266
|
+
request_json = request.as_json
|
|
267
|
+
encrypted = encrypt(request_json)
|
|
264
268
|
|
|
265
269
|
data = if external_storage.enabled?
|
|
266
270
|
external_storage.store(encrypted, max_size: configuration.payload_store_threshold)
|
|
@@ -272,9 +276,22 @@ module PatientHttp
|
|
|
272
276
|
if options&.any?
|
|
273
277
|
queue = options["queue"]
|
|
274
278
|
options = options.merge("patient_http_callback_queue" => queue.to_s) if queue
|
|
275
|
-
|
|
279
|
+
end
|
|
280
|
+
args = [data, callback_name, raise_error_responses, callback_args, request_id]
|
|
281
|
+
|
|
282
|
+
if direct_execution?(options)
|
|
283
|
+
execute_on_local_processor(
|
|
284
|
+
request_json,
|
|
285
|
+
args,
|
|
286
|
+
callback_name: callback_name,
|
|
287
|
+
raise_error_responses: raise_error_responses,
|
|
288
|
+
callback_args: callback_args,
|
|
289
|
+
request_id: request_id
|
|
290
|
+
)
|
|
291
|
+
elsif options&.any?
|
|
292
|
+
RequestWorker.set(options).perform_async(*args)
|
|
276
293
|
else
|
|
277
|
-
RequestWorker.perform_async(
|
|
294
|
+
RequestWorker.perform_async(*args)
|
|
278
295
|
end
|
|
279
296
|
|
|
280
297
|
request_id
|
|
@@ -395,6 +412,56 @@ module PatientHttp
|
|
|
395
412
|
def current_sidekiq_options
|
|
396
413
|
Thread.current[:patient_http_sidekiq_options]
|
|
397
414
|
end
|
|
415
|
+
|
|
416
|
+
# Check if the request can go directly to a processor running in the current
|
|
417
|
+
# process. Requests made in a with_sidekiq_options block always go through
|
|
418
|
+
# the queue so that Sidekiq applies the options (queue routing, scheduling,
|
|
419
|
+
# retry), and Sidekiq testing modes must keep their normal enqueue semantics.
|
|
420
|
+
#
|
|
421
|
+
# @param options [Hash, nil] scoped Sidekiq options for the request
|
|
422
|
+
# @return [Boolean]
|
|
423
|
+
def direct_execution?(options)
|
|
424
|
+
return false unless options.nil?
|
|
425
|
+
return false unless configuration.direct_execution?
|
|
426
|
+
return false unless running?
|
|
427
|
+
return false if defined?(::Sidekiq::Testing) && ::Sidekiq::Testing.enabled?
|
|
428
|
+
|
|
429
|
+
true
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Hand the request to the processor running in the current process. If the
|
|
433
|
+
# processor cannot accept the request (at capacity or shutting down), the
|
|
434
|
+
# request is enqueued as a normal RequestWorker job instead through the
|
|
435
|
+
# same retry path the processor uses when it drains.
|
|
436
|
+
#
|
|
437
|
+
# @param request_json [Hash] the serialized request
|
|
438
|
+
# @param args [Array] the RequestWorker job arguments
|
|
439
|
+
# @param callback_name [String] the callback service class name
|
|
440
|
+
# @param raise_error_responses [Boolean] whether non-2xx responses are errors
|
|
441
|
+
# @param callback_args [Hash, nil] arguments to pass to the callback
|
|
442
|
+
# @param request_id [String] unique request ID
|
|
443
|
+
# @return [void]
|
|
444
|
+
def execute_on_local_processor(request_json, args, callback_name:, raise_error_responses:, callback_args:, request_id:)
|
|
445
|
+
task_handler = DirectTaskHandler.new(args)
|
|
446
|
+
|
|
447
|
+
begin
|
|
448
|
+
# Reload the request from its serialized form so the direct path
|
|
449
|
+
# processes the same reconstructed request a RequestWorker job would.
|
|
450
|
+
RequestExecutor.execute(
|
|
451
|
+
PatientHttp::Request.load(request_json),
|
|
452
|
+
callback: callback_name,
|
|
453
|
+
raise_error_responses: raise_error_responses,
|
|
454
|
+
callback_args: callback_args,
|
|
455
|
+
task_handler: task_handler,
|
|
456
|
+
request_id: request_id
|
|
457
|
+
)
|
|
458
|
+
rescue PatientHttp::NotRunningError, PatientHttp::MaxCapacityError => e
|
|
459
|
+
configuration.logger&.info(
|
|
460
|
+
"[PatientHttp::Sidekiq] Falling back to enqueuing request: #{e.message}"
|
|
461
|
+
)
|
|
462
|
+
task_handler.retry
|
|
463
|
+
end
|
|
464
|
+
end
|
|
398
465
|
end
|
|
399
466
|
end
|
|
400
467
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: patient_http-sidekiq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.
|
|
32
|
+
version: 1.4.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 1.
|
|
39
|
+
version: 1.4.0
|
|
40
40
|
description: This gem provides a mechanism to offload long-running HTTP requests from
|
|
41
41
|
Sidekiq workers to a dedicated async I/O processor running in the same process,
|
|
42
42
|
freeing the worker thread immediately while the HTTP request is in flight.
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/patient_http/sidekiq/callback_worker.rb
|
|
57
57
|
- lib/patient_http/sidekiq/configuration.rb
|
|
58
58
|
- lib/patient_http/sidekiq/context.rb
|
|
59
|
+
- lib/patient_http/sidekiq/direct_task_handler.rb
|
|
59
60
|
- lib/patient_http/sidekiq/lifecycle_hooks.rb
|
|
60
61
|
- lib/patient_http/sidekiq/processor_observer.rb
|
|
61
62
|
- lib/patient_http/sidekiq/request_executor.rb
|