patient_http-sidekiq 1.3.0 → 1.4.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/CHANGELOG.md +22 -0
- data/README.md +105 -2
- data/VERSION +1 -1
- data/lib/patient_http/sidekiq/configuration.rb +231 -1
- data/lib/patient_http/sidekiq/direct_task_handler.rb +3 -1
- data/lib/patient_http/sidekiq/processor_observer.rb +151 -24
- data/lib/patient_http/sidekiq/redis_pool.rb +88 -0
- data/lib/patient_http/sidekiq/request_executor.rb +33 -5
- data/lib/patient_http/sidekiq/request_worker.rb +5 -2
- data/lib/patient_http/sidekiq/stats.rb +230 -33
- data/lib/patient_http/sidekiq/task_handler.rb +9 -3
- data/lib/patient_http/sidekiq/task_monitor.rb +410 -121
- data/lib/patient_http/sidekiq/task_monitor_thread.rb +36 -3
- data/lib/patient_http/sidekiq/web_ui/assets/patient-http/css/patient_http.css +29 -71
- data/lib/patient_http/sidekiq/web_ui/locales/ar.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/cs.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/da.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/de.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/el.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/en.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/es.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/fa.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/fr.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/gd.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/he.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/hi.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/it.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ja.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ko.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/lt.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/nb.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/nl.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/pl.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/pt-BR.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/pt.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ru.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/sv.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ta.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/tr.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/uk.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ur.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/vi.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/zh-CN.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/zh-TW.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/views/patient_http.html.erb +99 -44
- data/lib/patient_http/sidekiq/web_ui.rb +53 -1
- data/lib/patient_http/sidekiq.rb +236 -31
- data/patient_http-sidekiq.gemspec +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f463cc156486ac04ea624baf032e3ebd37d219149a720fd5044a864a9abf201
|
|
4
|
+
data.tar.gz: 0b294c6cb0df108ca53fd872c71d284fd82ab0a42284769eb2af7f04ca25bd76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9ab7cebbc773fb92baba7d58212a8fd3733392b4a93d995bdfa7ad3b03d70e1cd3637d8ebddc68c7178ca400b1adc1219ab65ef8cd3c7b9e4b852a0245b11e3
|
|
7
|
+
data.tar.gz: 994e51c5f8d7df504f6be4ee32bdd1e32607e62672bf7f0d228d9758af8f2e1b473844063c20951726b4d2ccc37f5eae7549ce4a3796633004195a82af1bcc23
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.4.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Dedicated Redis connection pool for the gem's own threads (`redis_pool_size`, default automatic; `redis_pool_timeout`, default 5 seconds). Registry writes, stats, and job pushes made from the processor's completion worker threads and the monitor thread no longer go through Sidekiq's small internal pool, which was a serialization point under load and could time out and lose completions.
|
|
12
|
+
- Named processors: declare profiles with `config.processor(:llm, max_connections: 200)` and route requests with `PatientHttp::Sidekiq.execute(request, processor: :llm)`, a `processor:` option on the request itself, or `with_sidekiq_options("processor" => "llm")`. Each profile runs as an independent processor with its own capacity, timeouts, and threads, so one workload class cannot starve another. The processor name is serialized into the job arguments, so retries and crash recovery keep their routing. Jobs from older gem versions run on the `:default` processor.
|
|
13
|
+
- Local stats aggregation (`stats_flush_interval`, default 5 seconds; 0 restores synchronous writes). Request metrics accumulate in memory and flush to Redis in one pipelined write per interval, removing the per-request write to the shared totals hash key. `get_totals` reports per-processor requests, duration, errors, and capacity rejections when more than one profile is configured.
|
|
14
|
+
- A result that can never be delivered no longer keeps its crash-recovery record. Delivery is retried and then recovered as before, but a failure that means the result cannot be serialized (`JSON::GeneratorError` and the `Encoding` errors, in the failure or in its cause chain) is permanent: the request is counted as an `undeliverable_result` error and its job is moved to the Sidekiq dead set. Such a request previously stayed in the registry, counted as in flight, and was re-enqueued and failed again after every process restart.
|
|
15
|
+
- The Web UI dashboard reports the high-water mark of requests in flight for each processor. The count only rises when a processor accepts a request, so the mark is recorded exactly rather than sampled, and it costs one comparison in memory. It is the most requests one process held at once, so it compares with `max_connections`, which is also per process. Like the other statistics it covers everything since they were last cleared.
|
|
16
|
+
- The Web UI dashboard lists the requests that have been in flight the longest, with the URL, HTTP method, processor, and age of each. The details are written next to the crash-recovery record, so a request left behind by a process that died stays listed until the orphan collector re-enqueues it. The URL is sanitized first: the user name, password, query string, and fragment are removed. Use `config.inflight_url_sanitizer` to redact more, or `config.inflight_details = false` to record nothing.
|
|
17
|
+
- The Web UI dashboard breaks the statistics down by processor when more than one profile is configured, reporting each processor's inflight requests, capacity, utilization, requests, errors, capacity rejections, and average duration. Each process publishes the capacity of its processors with its heartbeat, which the monitor thread now sends on every pass so the inflight counts stay current.
|
|
18
|
+
- Capacity fast path: requests are rejected with a cheap in-memory capacity check before any Redis registration. A rejection previously cost three Redis round trips (register, unregister, stat); it now costs none.
|
|
19
|
+
- The `completion_failed` processor event is handled by keeping the crash-recovery registry entry, so a request whose result could not be delivered is re-enqueued by the orphan collector instead of being silently lost, unless the failure is permanent (see above). Requires patient_http 1.5.0.
|
|
20
|
+
- A warning is logged at startup when the hiredis Redis driver is detected, because its blocking I/O can stall the reactor thread if application code calls Redis from processor callbacks.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The orphan collector removes orphans in batches of 100 with a single Lua call per batch (invoked by `EVALSHA`), instead of one `EVAL` with the full script body per orphan. Releasing the collector's lock is now a single compare-and-delete script call instead of a WATCH/GET/MULTI sequence.
|
|
25
|
+
- The shutdown re-enqueue path no longer unregisters a task twice or records a completion stat for a request that never completed.
|
|
26
|
+
- The task monitor thread and stats are now shared across all processors in the process; `ProcessorObserver.new` takes `stats:` and `task_monitor:` keyword arguments.
|
|
27
|
+
- The `patient_http` dependency floor is now 1.5.0.
|
|
28
|
+
|
|
7
29
|
## 1.3.0
|
|
8
30
|
|
|
9
31
|
### Added
|
data/README.md
CHANGED
|
@@ -260,6 +260,39 @@ When a request is made in a process where the processor is running (normally a S
|
|
|
260
260
|
|
|
261
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.
|
|
262
262
|
|
|
263
|
+
### Named Processors
|
|
264
|
+
|
|
265
|
+
By default all requests share one processor and one `max_connections` cap. When one process serves workload classes with very different profiles (for example, large slow API calls and small fast webhook deliveries), a burst of one class can consume all of the capacity the other class needs. Named processor profiles isolate them:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
PatientHttp::Sidekiq.configure do |config|
|
|
269
|
+
config.processor(:llm, max_connections: 200, request_timeout: 120)
|
|
270
|
+
config.processor(:webhooks, max_connections: 64, request_timeout: 10)
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Each profile runs as an independent processor in the process, with its own capacity, timeouts, and threads. Profile options override the top-level configuration; anything not overridden (secrets, preprocessors, payload stores, encryption, logger) is shared. The `:default` processor always exists; declare `config.processor(:default, ...)` to override its options.
|
|
275
|
+
|
|
276
|
+
Route a request to a processor in any of these ways:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# Explicit option on execute
|
|
280
|
+
PatientHttp::Sidekiq.execute(request, callback: MyCallback, processor: :llm)
|
|
281
|
+
|
|
282
|
+
# On the request itself (survives serialization, retries, and crash recovery)
|
|
283
|
+
request = PatientHttp::Request.new(:get, url, processor: :llm)
|
|
284
|
+
|
|
285
|
+
# Through a request template
|
|
286
|
+
template = PatientHttp::RequestTemplate.new(base_url: url, processor: :llm)
|
|
287
|
+
|
|
288
|
+
# Scoped for a block
|
|
289
|
+
PatientHttp::Sidekiq.with_sidekiq_options("processor" => "webhooks") do
|
|
290
|
+
PatientHttp::Sidekiq.execute(request, callback: MyCallback)
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The processor name is serialized into the job arguments, so Sidekiq retries and crash recovery keep their routing. A job that names a processor that is not configured in the executing process raises `PatientHttp::UnknownProcessorError` and goes through the normal Sidekiq retry mechanism; this makes new profile names safe to roll out gradually. Jobs enqueued by older gem versions run on the `:default` processor.
|
|
295
|
+
|
|
263
296
|
### Using Request Templates
|
|
264
297
|
|
|
265
298
|
For repeated requests to the same API, use `PatientHttp::RequestTemplate` to share configuration:
|
|
@@ -477,12 +510,42 @@ PatientHttp::Sidekiq.configure do |config|
|
|
|
477
510
|
# (use PatientHttp::Sidekiq.with_sidekiq_options to override per request)
|
|
478
511
|
config.sidekiq_options = {queue: "patient_http", retry: 5}
|
|
479
512
|
|
|
513
|
+
# Whether the URL, HTTP method, and processor of each in-flight request are
|
|
514
|
+
# recorded so the Web UI can list them (default: true).
|
|
515
|
+
config.inflight_details = true
|
|
516
|
+
|
|
517
|
+
# Sanitizer applied to a URL before it is recorded. Without one, the user
|
|
518
|
+
# name, password, query string, and fragment are removed.
|
|
519
|
+
config.inflight_url_sanitizer { |url| url.sub(%r{/users/\d+}, "/users/:id") }
|
|
520
|
+
|
|
480
521
|
# Whether requests made in a process with a running processor skip the
|
|
481
522
|
# Sidekiq queue and go straight to the processor (default: true).
|
|
482
523
|
# Sidekiq options, including a queue, do not apply to direct-executed
|
|
483
524
|
# requests; set this to false to route every request through the queue.
|
|
484
525
|
config.direct_execution = true
|
|
485
526
|
|
|
527
|
+
# Size of the gem's dedicated Redis pool used by its own threads
|
|
528
|
+
# (default: nil, sized automatically from completion_threads with a
|
|
529
|
+
# floor of 10)
|
|
530
|
+
config.redis_pool_size = nil
|
|
531
|
+
|
|
532
|
+
# Checkout timeout in seconds for the dedicated Redis pool (default: 5)
|
|
533
|
+
config.redis_pool_timeout = 5
|
|
534
|
+
|
|
535
|
+
# Seconds between flushes of locally aggregated stats to Redis
|
|
536
|
+
# (default: 5; 0 writes every event synchronously)
|
|
537
|
+
config.stats_flush_interval = 5
|
|
538
|
+
|
|
539
|
+
# Number of threads that decode responses and deliver results (default: 2)
|
|
540
|
+
config.completion_threads = 2
|
|
541
|
+
|
|
542
|
+
# Maximum connections per host (default: nil, unlimited)
|
|
543
|
+
config.max_connections_per_host = 32
|
|
544
|
+
|
|
545
|
+
# Named processor profiles for workload isolation (see Named Processors)
|
|
546
|
+
config.processor(:llm, max_connections: 200, request_timeout: 120)
|
|
547
|
+
config.processor(:webhooks, max_connections: 64, request_timeout: 10)
|
|
548
|
+
|
|
486
549
|
# Handler called when a callback job exhausts all Sidekiq retries
|
|
487
550
|
config.on_retries_exhausted { |error| MyAlertService.notify(error) }
|
|
488
551
|
|
|
@@ -507,6 +570,13 @@ See the [Configuration](lib/patient_http/sidekiq/configuration.rb) class for all
|
|
|
507
570
|
- `connection_timeout`: Set this if you need to fail fast on connection establishment. Useful for detecting network issues quickly.
|
|
508
571
|
- `retries`: Number of times to retry a failed request before calling the error callback.
|
|
509
572
|
- `max_response_size`: Set this to limit the maximum size of HTTP responses. This helps prevent excessive memory usage from unexpectedly large responses. Responses need to be serialized to Redis as Sidekiq jobs and very large responses may cause performance issues in Redis. If a response body is text content, it will be compressed to save space in Redis. However, binary content needs to be Base64 encoded which increases size by ~33%.
|
|
573
|
+
- `max_connections_per_host`: Bounds sockets per host. Verify the process file descriptor limit covers `max_connections` plus pooled idle host connections plus the application's own connections; raise the limit if needed.
|
|
574
|
+
- `shutdown_timeout`: Must be below the process supervisor's termination window so the drain finishes before a hard kill. The default derives it from Sidekiq's own shutdown timeout; check any additional supervisor (container orchestrator, init system) stop timeout as well.
|
|
575
|
+
- `completion_threads`: Increase when result callbacks do heavier work (serialization, encryption) and completions back up behind them.
|
|
576
|
+
- `redis_pool_size`: The automatic size covers the gem's own threads. Increase it when a high request rate makes registration or completion pushes wait on checkouts.
|
|
577
|
+
|
|
578
|
+
> [!WARNING]
|
|
579
|
+
> Do not install `hiredis-client` in processes that run the async processor. The hiredis driver performs blocking I/O that does not yield to the fiber scheduler, so a Redis call made on the reactor thread (for example, from a custom processor observer) stalls every in-flight HTTP request. The gem logs a warning at startup when it detects the hiredis driver.
|
|
510
580
|
|
|
511
581
|
> [!IMPORTANT]
|
|
512
582
|
>
|
|
@@ -530,10 +600,41 @@ mount Sidekiq::Web => "/sidekiq"
|
|
|
530
600
|
```
|
|
531
601
|
|
|
532
602
|
The Web UI shows:
|
|
533
|
-
- Total requests, errors, and
|
|
534
|
-
-
|
|
603
|
+
- Total requests, errors, average duration, and current capacity utilization
|
|
604
|
+
- Per-processor capacity, utilization, requests, errors, average duration, and the high-water mark of
|
|
605
|
+
requests in flight, when more than one [named processor](#named-processors) is configured
|
|
606
|
+
- The requests that have been in flight the longest, with their URL, HTTP method, processor, and age
|
|
535
607
|
- Per-process inflight request counts
|
|
536
608
|
|
|
609
|
+
The per-processor numbers come from the capacity each process publishes with its heartbeat, so they
|
|
610
|
+
cover the processes that are currently running and can lag by a few seconds.
|
|
611
|
+
|
|
612
|
+
The high-water mark is the most requests one process held on that processor at once, so compare it
|
|
613
|
+
with `max_connections`, which is also per process, rather than with the capacity column, which is the
|
|
614
|
+
sum across the running processes. The count only rises when a processor accepts a request, so the
|
|
615
|
+
mark is exact rather than sampled. It covers everything since the statistics were last cleared.
|
|
616
|
+
|
|
617
|
+
#### In-Flight Requests
|
|
618
|
+
|
|
619
|
+
The URL, HTTP method, and processor of each in-flight request are recorded next to its
|
|
620
|
+
crash-recovery record, and the dashboard lists the 50 oldest. A request left behind by a process
|
|
621
|
+
that died stays listed until the orphan collector re-enqueues it, so this is also where you see what
|
|
622
|
+
a process was working on when it stopped.
|
|
623
|
+
|
|
624
|
+
The URL is sanitized before it is recorded: the user name, password, query string, and fragment are
|
|
625
|
+
removed and the scheme, host, and path are kept. Paths can still carry identifiers, so you can
|
|
626
|
+
redact more, or record nothing at all:
|
|
627
|
+
|
|
628
|
+
```ruby
|
|
629
|
+
PatientHttp::Sidekiq.configure do |config|
|
|
630
|
+
# Redact more of the URL.
|
|
631
|
+
config.inflight_url_sanitizer { |url| url.sub(%r{/users/\d+}, "/users/:id") }
|
|
632
|
+
|
|
633
|
+
# Or keep URLs out of Redis entirely.
|
|
634
|
+
config.inflight_details = false
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
537
638
|
### Callbacks for Custom Monitoring
|
|
538
639
|
|
|
539
640
|
You can register callbacks to integrate with your monitoring system using the `after_completion` and `after_error` hooks:
|
|
@@ -609,6 +710,8 @@ This ensures that if a Sidekiq process crashes, its in-flight requests will be r
|
|
|
609
710
|
|
|
610
711
|
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
712
|
|
|
713
|
+
A request whose result cannot be handed to a callback job keeps its registry entry as well, so the same recovery re-enqueues it. When the failure is one that trying again cannot fix, because the result cannot be serialized, the request is not kept: it is counted as an `undeliverable_result` error and its job is moved to the Sidekiq dead set, where you can inspect it and retry it by hand.
|
|
714
|
+
|
|
612
715
|
## Testing
|
|
613
716
|
|
|
614
717
|
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.4.0
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
3
5
|
module PatientHttp
|
|
4
6
|
module Sidekiq
|
|
5
7
|
# Configuration for the Sidekiq integration.
|
|
@@ -28,6 +30,21 @@ module PatientHttp
|
|
|
28
30
|
# current process instead of being enqueued through Sidekiq
|
|
29
31
|
attr_reader :direct_execution
|
|
30
32
|
|
|
33
|
+
# @return [Boolean] Whether the URL, HTTP method, and processor of each in-flight
|
|
34
|
+
# request are recorded so the Web UI can list them
|
|
35
|
+
attr_reader :inflight_details
|
|
36
|
+
|
|
37
|
+
# @return [Integer, nil] Size of the gem's dedicated Redis pool; nil sizes it
|
|
38
|
+
# automatically from completion_threads
|
|
39
|
+
attr_reader :redis_pool_size
|
|
40
|
+
|
|
41
|
+
# @return [Numeric] Checkout timeout in seconds for the gem's dedicated Redis pool
|
|
42
|
+
attr_reader :redis_pool_timeout
|
|
43
|
+
|
|
44
|
+
# @return [Numeric] Seconds between flushes of locally aggregated stats to Redis
|
|
45
|
+
# (0 flushes synchronously on every recorded event)
|
|
46
|
+
attr_reader :stats_flush_interval
|
|
47
|
+
|
|
31
48
|
# @return [#call, nil] Handler invoked when a CallbackWorker job exhausts all retries.
|
|
32
49
|
# @overload on_retries_exhausted
|
|
33
50
|
# Returns the current handler.
|
|
@@ -67,6 +84,10 @@ module PatientHttp
|
|
|
67
84
|
payload_store_threshold: DEFAULT_PAYLOAD_STORE_THRESHOLD,
|
|
68
85
|
on_retries_exhausted: nil,
|
|
69
86
|
direct_execution: true,
|
|
87
|
+
redis_pool_size: nil,
|
|
88
|
+
redis_pool_timeout: 5,
|
|
89
|
+
stats_flush_interval: 5,
|
|
90
|
+
inflight_details: true,
|
|
70
91
|
**pool_options
|
|
71
92
|
)
|
|
72
93
|
pool_options[:shutdown_timeout] ||= (::Sidekiq.default_configuration[:timeout] || 25) - 2
|
|
@@ -75,12 +96,17 @@ module PatientHttp
|
|
|
75
96
|
super(**pool_options)
|
|
76
97
|
|
|
77
98
|
@observers = []
|
|
99
|
+
@processor_profiles = {default: {}}
|
|
78
100
|
self.sidekiq_options = sidekiq_options
|
|
79
101
|
self.heartbeat_interval = heartbeat_interval
|
|
80
102
|
self.orphan_threshold = orphan_threshold
|
|
81
103
|
self.payload_store_threshold = payload_store_threshold || DEFAULT_PAYLOAD_STORE_THRESHOLD
|
|
82
104
|
self.on_retries_exhausted = on_retries_exhausted
|
|
83
105
|
self.direct_execution = direct_execution
|
|
106
|
+
self.redis_pool_size = redis_pool_size
|
|
107
|
+
self.redis_pool_timeout = redis_pool_timeout
|
|
108
|
+
self.stats_flush_interval = stats_flush_interval
|
|
109
|
+
self.inflight_details = inflight_details
|
|
84
110
|
end
|
|
85
111
|
|
|
86
112
|
# Set the on_retries_exhausted handler.
|
|
@@ -170,6 +196,161 @@ module PatientHttp
|
|
|
170
196
|
@direct_execution
|
|
171
197
|
end
|
|
172
198
|
|
|
199
|
+
# Set the size of the gem's dedicated Redis pool.
|
|
200
|
+
#
|
|
201
|
+
# @param value [Integer, nil] pool size; nil sizes the pool automatically
|
|
202
|
+
# @return [void]
|
|
203
|
+
def redis_pool_size=(value)
|
|
204
|
+
if value.nil?
|
|
205
|
+
@redis_pool_size = nil
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
validate_positive_integer(:redis_pool_size, value)
|
|
210
|
+
@redis_pool_size = value
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Set the checkout timeout for the gem's dedicated Redis pool.
|
|
214
|
+
#
|
|
215
|
+
# @param value [Numeric] timeout in seconds
|
|
216
|
+
# @return [void]
|
|
217
|
+
def redis_pool_timeout=(value)
|
|
218
|
+
unless value.is_a?(Numeric) && value.positive?
|
|
219
|
+
raise ArgumentError.new("redis_pool_timeout must be a positive number, got: #{value.inspect}")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
@redis_pool_timeout = value
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Set the flush interval for locally aggregated stats.
|
|
226
|
+
#
|
|
227
|
+
# @param value [Numeric] seconds between flushes; 0 flushes synchronously on
|
|
228
|
+
# every recorded event
|
|
229
|
+
# @return [void]
|
|
230
|
+
def stats_flush_interval=(value)
|
|
231
|
+
unless value.is_a?(Numeric) && value >= 0
|
|
232
|
+
raise ArgumentError.new("stats_flush_interval must be a non-negative number, got: #{value.inspect}")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
@stats_flush_interval = value
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Set whether the details of each in-flight request are recorded.
|
|
239
|
+
#
|
|
240
|
+
# The URL (see +inflight_url_sanitizer+), HTTP method, and processor name are
|
|
241
|
+
# written next to the crash-recovery record so the Web UI can list the requests
|
|
242
|
+
# that are in flight. Turn this off to keep URLs out of Redis entirely.
|
|
243
|
+
#
|
|
244
|
+
# @param value [Boolean] true to record the details
|
|
245
|
+
# @return [void]
|
|
246
|
+
def inflight_details=(value)
|
|
247
|
+
@inflight_details = !!value
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Check if in-flight request details are recorded.
|
|
251
|
+
#
|
|
252
|
+
# @return [Boolean]
|
|
253
|
+
def inflight_details?
|
|
254
|
+
@inflight_details
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Set the sanitizer applied to a request URL before it is recorded for the
|
|
258
|
+
# Web UI, or read the current one.
|
|
259
|
+
#
|
|
260
|
+
# The sanitizer receives the full URL and returns what to display. Without
|
|
261
|
+
# one, the user name, password, query string, and fragment are removed and
|
|
262
|
+
# the scheme, host, and path are kept. Use it to redact more, such as an
|
|
263
|
+
# identifier in the path.
|
|
264
|
+
#
|
|
265
|
+
# @example
|
|
266
|
+
# config.inflight_url_sanitizer { |url| url.sub(%r{/users/\d+}, "/users/:id") }
|
|
267
|
+
#
|
|
268
|
+
# @overload inflight_url_sanitizer
|
|
269
|
+
# @return [#call, nil] the current sanitizer
|
|
270
|
+
# @overload inflight_url_sanitizer(&block)
|
|
271
|
+
# @yield [url] block that returns the URL to display
|
|
272
|
+
# @yieldparam url [String] the full request URL
|
|
273
|
+
def inflight_url_sanitizer(&block)
|
|
274
|
+
if block
|
|
275
|
+
@inflight_url_sanitizer = block
|
|
276
|
+
else
|
|
277
|
+
@inflight_url_sanitizer
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Set the sanitizer applied to a request URL before it is recorded.
|
|
282
|
+
#
|
|
283
|
+
# @param value [#call, nil] a callable object, or nil to use the default
|
|
284
|
+
# @raise [ArgumentError] If value is not callable and not nil
|
|
285
|
+
def inflight_url_sanitizer=(value)
|
|
286
|
+
if value && !value.respond_to?(:call)
|
|
287
|
+
raise ArgumentError.new("inflight_url_sanitizer must respond to #call, got: #{value.class}")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
@inflight_url_sanitizer = value
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Declare a named processor profile, or read one back.
|
|
294
|
+
#
|
|
295
|
+
# Each profile becomes an independent processor with its own capacity,
|
|
296
|
+
# timeouts, and threads. Options are overrides applied on top of this
|
|
297
|
+
# configuration's HTTP pool options. Requests select a processor with the
|
|
298
|
+
# +processor:+ option on +PatientHttp::Sidekiq.execute+ (or on the
|
|
299
|
+
# request itself). The +:default+ profile always exists; declaring it
|
|
300
|
+
# overrides options for the default processor.
|
|
301
|
+
#
|
|
302
|
+
# @example
|
|
303
|
+
# PatientHttp::Sidekiq.configure do |config|
|
|
304
|
+
# config.processor(:llm, max_connections: 200, request_timeout: 120)
|
|
305
|
+
# config.processor(:webhooks, max_connections: 64, request_timeout: 10)
|
|
306
|
+
# end
|
|
307
|
+
#
|
|
308
|
+
# @param name [Symbol, String] the processor name
|
|
309
|
+
# @param options [Hash] overrides for PatientHttp::Configuration options
|
|
310
|
+
# @return [Hash] the stored options for the profile
|
|
311
|
+
def processor(name, **options)
|
|
312
|
+
key = normalize_processor_name(name)
|
|
313
|
+
|
|
314
|
+
if options.any?
|
|
315
|
+
validate_profile_options!(options)
|
|
316
|
+
@processor_profiles[key] = options
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
@processor_profiles[key]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# All declared processor profiles. Always includes :default.
|
|
323
|
+
#
|
|
324
|
+
# @return [Hash{Symbol => Hash}] profile options by processor name
|
|
325
|
+
def processor_profiles
|
|
326
|
+
@processor_profiles.dup
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Whether more than one processor profile is declared.
|
|
330
|
+
#
|
|
331
|
+
# @return [Boolean]
|
|
332
|
+
def multiple_processors?
|
|
333
|
+
@processor_profiles.size > 1
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Build the effective configuration for a named processor. The default
|
|
337
|
+
# profile with no overrides is this configuration itself; other profiles
|
|
338
|
+
# get a view of this configuration with their overrides applied, so they
|
|
339
|
+
# share secrets, preprocessors, payload stores, and encryption.
|
|
340
|
+
#
|
|
341
|
+
# @param name [Symbol, String] the processor name
|
|
342
|
+
# @return [PatientHttp::Configuration] the configuration for the processor
|
|
343
|
+
# @raise [ArgumentError] if the profile is not declared
|
|
344
|
+
def processor_config(name)
|
|
345
|
+
key = normalize_processor_name(name)
|
|
346
|
+
profile = @processor_profiles[key]
|
|
347
|
+
raise ArgumentError.new("Unknown processor profile: #{name.inspect}") unless profile
|
|
348
|
+
|
|
349
|
+
return self if profile.empty?
|
|
350
|
+
|
|
351
|
+
ProfileConfiguration.new(self, profile)
|
|
352
|
+
end
|
|
353
|
+
|
|
173
354
|
# Convert to hash for inspection
|
|
174
355
|
# @return [Hash] hash representation with string keys
|
|
175
356
|
def to_h
|
|
@@ -179,12 +360,61 @@ module PatientHttp
|
|
|
179
360
|
"orphan_threshold" => orphan_threshold,
|
|
180
361
|
"sidekiq_options" => sidekiq_options,
|
|
181
362
|
"direct_execution" => direct_execution,
|
|
182
|
-
"on_retries_exhausted" => on_retries_exhausted ? "defined" : nil
|
|
363
|
+
"on_retries_exhausted" => on_retries_exhausted ? "defined" : nil,
|
|
364
|
+
"redis_pool_size" => redis_pool_size,
|
|
365
|
+
"redis_pool_timeout" => redis_pool_timeout,
|
|
366
|
+
"stats_flush_interval" => stats_flush_interval,
|
|
367
|
+
"inflight_details" => inflight_details,
|
|
368
|
+
"processor_profiles" => processor_profiles.keys.map(&:to_s)
|
|
183
369
|
)
|
|
184
370
|
end
|
|
185
371
|
|
|
372
|
+
# View of a base configuration with a profile's option overrides applied.
|
|
373
|
+
# Everything not overridden (secrets, preprocessors, payload stores,
|
|
374
|
+
# logging) delegates to the base configuration, so all processors share
|
|
375
|
+
# those registries.
|
|
376
|
+
#
|
|
377
|
+
# Overrides are applied through a real PatientHttp::Configuration so that
|
|
378
|
+
# each option's writer performs its own normalization. Readers whose
|
|
379
|
+
# value a writer derives from an overridden option are taken from that
|
|
380
|
+
# configuration as well, so an override cannot be half applied.
|
|
381
|
+
class ProfileConfiguration < SimpleDelegator
|
|
382
|
+
# Readers populated as a side effect of another option's writer.
|
|
383
|
+
DERIVED_READERS = {
|
|
384
|
+
encryption_key: [:encryption, :decryption, :encryptor]
|
|
385
|
+
}.freeze
|
|
386
|
+
|
|
387
|
+
def initialize(base_configuration, overrides)
|
|
388
|
+
super(base_configuration)
|
|
389
|
+
normalized = PatientHttp::Configuration.new(**overrides)
|
|
390
|
+
readers = overrides.keys.flat_map { |key| [key.to_sym, *DERIVED_READERS[key.to_sym]] }.uniq
|
|
391
|
+
readers.each do |reader|
|
|
392
|
+
next unless normalized.respond_to?(reader)
|
|
393
|
+
|
|
394
|
+
define_singleton_method(reader) do |*args, &block|
|
|
395
|
+
normalized.public_send(reader, *args, &block)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
186
401
|
private
|
|
187
402
|
|
|
403
|
+
# Profile options must be valid PatientHttp::Configuration options. A
|
|
404
|
+
# throwaway configuration exercises each option's own validation.
|
|
405
|
+
def validate_profile_options!(options)
|
|
406
|
+
PatientHttp::Configuration.new(**options)
|
|
407
|
+
rescue ArgumentError => e
|
|
408
|
+
raise ArgumentError.new("Invalid processor profile options: #{e.message}")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def normalize_processor_name(name)
|
|
412
|
+
key = name.to_s
|
|
413
|
+
raise ArgumentError.new("processor name cannot be empty") if key.empty?
|
|
414
|
+
|
|
415
|
+
key.to_sym
|
|
416
|
+
end
|
|
417
|
+
|
|
188
418
|
def apply_sidekiq_options(options)
|
|
189
419
|
PatientHttp::Sidekiq::RequestWorker.sidekiq_options(options)
|
|
190
420
|
PatientHttp::Sidekiq::CallbackWorker.sidekiq_options(options)
|